From 8d668ec16ca2a0084aaf98220b6b3721e76bcc2e Mon Sep 17 00:00:00 2001 From: Mason Le <43195241+MasonLe2497@users.noreply.github.com> Date: Fri, 11 Mar 2022 15:36:33 +0700 Subject: [PATCH 1/2] add reanimated lib --- example/babel.config.js | 1 + example/package.json | 1 + example/src/App.tsx | 113 +++++++++++++++++++++------------------- example/yarn.lock | 48 ++++++++++++++++- package.json | 4 +- src/lib/ModalStack.tsx | 89 ++++++++++++++++++------------- src/lib/StackItem.tsx | 104 +++++++++++++++++++----------------- src/types.ts | 26 ++++----- src/utils/index.ts | 4 +- yarn.lock | 53 ++++++++++++++++++- 10 files changed, 286 insertions(+), 157 deletions(-) diff --git a/example/babel.config.js b/example/babel.config.js index 3316845..c49c05b 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -17,6 +17,7 @@ module.exports = function (api) { }, }, ], + ['react-native-reanimated/plugin'], ], } } diff --git a/example/package.json b/example/package.json index e28752f..7d8c4b7 100644 --- a/example/package.json +++ b/example/package.json @@ -17,6 +17,7 @@ "react": "17.0.1", "react-dom": "17.0.1", "react-native": "0.64.3", + "react-native-reanimated": "~2.3.1", "react-native-unimodules": "~0.15.0", "react-native-web": "0.17.1" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index ccbfa6b..bfa4c6a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ModalOptions, ModalProvider, ModalStackConfig, createModalStack } from 'react-native-modalfy' -import { ImageBackground, Dimensions, StyleSheet, StatusBar, Easing, Text, Animated, Platform } from 'react-native' - +import { ImageBackground, Dimensions, StyleSheet, StatusBar, Text, Platform } from 'react-native' +import Animated, { interpolate, runOnJS, withSpring, Easing } from 'react-native-reanimated' import DemoModal from './components/DemoModal' import IntroModal from './components/IntroModal' import IntroButton from './components/IntroButton' @@ -39,71 +39,76 @@ const config: ModalStackConfig = { easing: Easing.inOut(Easing.exp), duration: 500, }, - transitionOptions: (animatedValue) => ({ - transform: [ - { - translateY: animatedValue.interpolate({ - inputRange: [0, 1, 2], - outputRange: [height, 0, height], - }), - }, - ], - }), + transitionOptions: (animatedValue) => { + 'worklet' + return { + transform: [ + { + translateY: interpolate(animatedValue.value, [0, 1, 2], [height, 0, height]), + }, + ], + } + }, }, + // IntroModal: IntroModal, ModalA: DemoModal, ModalB: DemoModal, ModalC: DemoModal, } -const animate = (animatedValue: Animated.Value, toValue: number, callback?: () => void) => { - Animated.spring(animatedValue, { +const animate = (animatedValue: Animated.SharedValue, toValue: number, callback?: () => void) => { + animatedValue.value = withSpring( toValue, - damping: 10, - mass: 0.35, - stiffness: 100, - overshootClamping: true, - restSpeedThreshold: 0.001, - restDisplacementThreshold: 0.001, - useNativeDriver: true, - }).start(({ finished }) => { - if (finished) callback?.() - }) + { + damping: 10, + mass: 0.35, + stiffness: 100, + overshootClamping: true, + restSpeedThreshold: 0.001, + restDisplacementThreshold: 0.001, + }, + (finished) => { + if (finished) { + if (typeof callback === 'function') { + runOnJS(callback)() + } + } + }, + ) + // Animated.spring(animatedValue, { + // toValue, + + // useNativeDriver: true, + // }).start(({ finished }) => { + // if (finished) callback?.() + // }) } const defaultOptions: ModalOptions = { backdropOpacity: 0.4, animationIn: animate, animationOut: animate, - transitionOptions: (animatedValue) => ({ - opacity: animatedValue.interpolate({ - inputRange: [0, 1, 2], - outputRange: [0, 1, 0.9], - }), - transform: [ - { perspective: 2000 }, - { - translateX: animatedValue.interpolate({ - inputRange: [0, 1, 2], - outputRange: [-width / 1.5, 0, width / 1.5], - extrapolate: 'clamp', - }), - }, - { - rotateY: animatedValue.interpolate({ - inputRange: [0, 1, 2], - outputRange: ['90deg', '0deg', '-90deg'], - extrapolate: 'clamp', - }), - }, - { - scale: animatedValue.interpolate({ - inputRange: [0, 1, 2], - outputRange: [1.2, 1, 0.9], - extrapolate: 'clamp', - }), - }, - ], - }), + transitionOptions: (animatedValue) => { + 'worklet' + return { + opacity: interpolate(animatedValue.value, [0, 1, 2], [0, 1, 0.9]), + transform: [ + { perspective: 2000 }, + { + translateX: interpolate(animatedValue.value, [0, 1, 2], [-width / 1.5, 0, 10]), + }, + // { + // rotateY: interpolate(animatedValue.value, [0, 1, 2], [90, 0, -90]) + 'deg', + // }, + { + rotate: interpolate(animatedValue.value, [0, 1, 2], [45, 0, 45]) + 'deg', + }, + // { + // scale: interpolate(animatedValue.value, [0, 1, 2], [1.2, 1, 0.9]), + // }, + ], + } + }, } const stack = createModalStack(config, defaultOptions) diff --git a/example/yarn.lock b/example/yarn.lock index a97e3f7..beb5211 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -825,7 +825,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-object-assign@^7.0.0": +"@babel/plugin-transform-object-assign@^7.0.0", "@babel/plugin-transform-object-assign@^7.10.4": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.16.7.tgz#5fe08d63dccfeb6a33aa2638faf98e5c584100f8" integrity sha512-R8mawvm3x0COTJtveuoqZIjNypn2FjfvXZr4pSQ8VhEFBuQGBz4XhHasZtHXjgXU4XptZ4HtGof3NoYc93ZH9Q== @@ -1962,6 +1962,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/invariant@^2.2.35": + version "2.2.35" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" + integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -7848,6 +7853,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mockdate@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb" + integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -9399,6 +9409,11 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== +react-freeze@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.0.tgz#b21c65fe1783743007c8c9a2952b1c8879a77354" + integrity sha512-yQaiOqDmoKqks56LN9MTgY06O0qQHgV4FUrikH357DydArSZHQhl0BJFqGKIZoTqi8JizF9Dxhuk1FIZD6qCaw== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -9429,6 +9444,27 @@ react-native-gesture-handler@~2.1.0: lodash "^4.17.21" prop-types "^15.7.2" +react-native-reanimated@~2.3.1: + version "2.3.3" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.3.3.tgz#73de8ea495e59a091d848741e7037ac55d0235c4" + integrity sha512-uQofwsWUoKLY4QDgSdNbRxnqQDaQEPLLBNO9SP64JfQ2fDRJD5rjb4d3S29F0z9FqTnsWEwTL2Sl0spdx9xvHA== + dependencies: + "@babel/plugin-transform-object-assign" "^7.10.4" + "@types/invariant" "^2.2.35" + invariant "^2.2.4" + lodash.isequal "^4.5.0" + mockdate "^3.0.2" + react-native-screens "^3.4.0" + string-hash-64 "^1.0.3" + +react-native-screens@^3.4.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.13.1.tgz#b3b1c5788dca25a71668909f66d87fb35c5c5241" + integrity sha512-xcrnuUs0qUrGpc2gOTDY4VgHHADQwp80mwR1prU/Q0JqbZN5W3koLhuOsT6FkSRKjR5t40l+4LcjhHdpqRB2HA== + dependencies: + react-freeze "^1.0.0" + warn-once "^0.1.0" + react-native-unimodules@~0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/react-native-unimodules/-/react-native-unimodules-0.15.0.tgz#8d4fdb14f18490d13a4c422c9f49aa090204d878" @@ -10516,6 +10552,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +string-hash-64@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" + integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -11389,6 +11430,11 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" +warn-once@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.0.tgz#4f58d89b84f968d0389176aa99e0cf0f14ffd4c8" + integrity sha512-recZTSvuaH/On5ZU5ywq66y99lImWqzP93+AiUo9LUwG8gXHW+LJjhOd6REJHm7qb0niYqrEQJvbHSQfuJtTqA== + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" diff --git a/package.json b/package.json index 065e839..945e17d 100644 --- a/package.json +++ b/package.json @@ -76,13 +76,15 @@ "react-native": "0.66.4", "react-native-builder-bob": "^0.18.2", "react-native-gesture-handler": "^1.10.3", + "react-native-reanimated": "^2.4.1", "release-it": "^14.2.2", "typescript": "^4.5.4" }, "peerDependencies": { "react": ">=16.8.3", "react-native": ">=0.59.0", - "react-native-gesture-handler": ">=1.9.0" + "react-native-gesture-handler": ">=1.9.0", + "react-native-reanimated": ">=2.3.1" }, "husky": { "hooks": { diff --git a/src/lib/ModalStack.tsx b/src/lib/ModalStack.tsx index 9a34fb4..565af84 100644 --- a/src/lib/ModalStack.tsx +++ b/src/lib/ModalStack.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'use-memo-one' import React, { useEffect, useState, memo } from 'react' -import { Easing, Animated, StyleSheet, TouchableWithoutFeedback, Platform } from 'react-native' - +import { StyleSheet, TouchableWithoutFeedback, Platform } from 'react-native' +import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming, interpolate } from 'react-native-reanimated' import type { SharedProps, ModalfyParams, ModalStackItem, ModalPendingClosingAction } from '../types' import StackItem from './StackItem' @@ -19,13 +19,16 @@ const ModalStack =

(props: Props

) => { const [openActionCallbacks, setOpenActionCallbacks] = useState([]) - const { opacity, translateY } = useMemo( - () => ({ - opacity: new Animated.Value(0), - translateY: new Animated.Value(sh(100)), - }), - [], - ) + const opacity = useSharedValue(0) + const translateY = useSharedValue(sh(100)) + const fullHeight = useMemo(() => sh(100), []) + // const { opacity, translateY } = useMemo( + // () => ({ + // opacity: new Animated.Value(0), + // translateY: new Animated.Value(sh(100)), + // }), + // [], + // ) const { backBehavior, backdropColor, backdropOpacity } = useMemo( () => getStackItemOptions(Array.from(stack.openedItems).pop(), stack), @@ -41,22 +44,28 @@ const ModalStack =

(props: Props

) => { useEffect(() => { const scrollY = Platform.OS === 'web' ? window.scrollY ?? document.documentElement.scrollTop : 0 if (stack.openedItemsSize) { - translateY.setValue(scrollY) - Animated.timing(opacity, { - toValue: 1, - easing: Easing.in(Easing.ease), - duration: 300, - useNativeDriver: true, - }).start() + translateY.value = scrollY + opacity.value = withTiming(1, { easing: Easing.in(Easing.ease), duration: 300 }) + // Animated.timing(opacity, { + // toValue: 1, + // easing: Easing.in(Easing.ease), + // duration: 300, + // useNativeDriver: true, + // }).start() } else { - Animated.timing(opacity, { - toValue: 0, - easing: Easing.inOut(Easing.ease), - duration: 300, - useNativeDriver: true, - }).start(({ finished }) => { - if (finished) translateY.setValue(sh(100)) + opacity.value = withTiming(0, { easing: Easing.inOut(Easing.ease), duration: 300 }, (finished) => { + if (finished) { + translateY.value = fullHeight + } }) + // Animated.timing(opacity, { + // toValue: 0, + // easing: Easing.inOut(Easing.ease), + // duration: 300, + // useNativeDriver: true, + // }).start(({ finished }) => { + // if (finished) translateY.setValue(sh(100)) + // }) } }, [opacity, stack.openedItemsSize, translateY]) @@ -97,19 +106,32 @@ const ModalStack =

(props: Props

) => { const currentItem = [...stack.openedItems].slice(-1)[0] if (stack.openedItemsSize === 1) { - Animated.timing(opacity, { - toValue: 0, - easing: Easing.inOut(Easing.ease), - duration: 300, - useNativeDriver: true, - }).start(({ finished }) => { - if (finished) translateY.setValue(sh(100)) + opacity.value = withTiming(0, { easing: Easing.inOut(Easing.ease), duration: 300 }, (finished) => { + if (finished) { + translateY.value = fullHeight + } }) + // Animated.timing(opacity, { + // toValue: 0, + // easing: Easing.inOut(Easing.ease), + // duration: 300, + // useNativeDriver: true, + // }).start(({ finished }) => { + // if (finished) translateY.setValue(sh(100)) + // }) } setBackdropClosedItems([...backdropClosedItems, currentItem?.hash]) } + const backdropReStyle = useAnimatedStyle(() => ({ + opacity: interpolate(opacity.value, [0, 1], [0, backdropOpacity ?? 0.6]), + })) + const containerReStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ translateY: translateY.value }], + })) + const renderBackdrop = () => { const onPress = () => onBackdropPress() const backgroundColor = @@ -122,11 +144,8 @@ const ModalStack =

(props: Props

) => { styles.backdrop, { backgroundColor, - opacity: opacity.interpolate({ - inputRange: [0, 1], - outputRange: [0, backdropOpacity ?? 0.6], - }), }, + backdropReStyle, ]} /> @@ -137,7 +156,7 @@ const ModalStack =

(props: Props

) => { {renderBackdrop()} diff --git a/src/lib/StackItem.tsx b/src/lib/StackItem.tsx index 05d3247..822ba21 100644 --- a/src/lib/StackItem.tsx +++ b/src/lib/StackItem.tsx @@ -1,6 +1,13 @@ -import { Animated, StyleSheet } from 'react-native' +import { StyleSheet } from 'react-native' +import Animated, { + useSharedValue, + useAnimatedReaction, + useAnimatedStyle, + runOnJS, + withTiming, +} from 'react-native-reanimated' import { useMemo, useCallback } from 'use-memo-one' -import React, { ReactNode, useEffect, useRef, memo } from 'react' +import React, { ReactNode, useEffect, memo, useRef, MutableRefObject } from 'react' import { State, Directions, FlingGestureHandler } from 'react-native-gesture-handler' import type { @@ -42,15 +49,8 @@ const StackItem =

({ wasOpenCallbackCalled, wasClosedByBackdropPress, }: Props

) => { - const { animatedValue, translateY } = useMemo( - () => ({ - animatedValue: new Animated.Value(-1), - translateY: new Animated.Value(0), - }), - [], - ) - - const animatedListenerId = useRef() + const animatedValue = useSharedValue(-1) + const translateY = useSharedValue(0) const { animationIn, @@ -63,41 +63,45 @@ const StackItem =

({ disableFlingGesture, position: verticalPosition, } = useMemo(() => getStackItemOptions(stackItem, stack), [stack, stackItem]) - + const onAnimateListenerRef = useRef<(value: number) => void | null>(null) as MutableRefObject< + (value: number) => void | null + > + useAnimatedReaction( + () => animatedValue.value, + (value) => { + console.log('Change_value') + if (typeof onAnimateListenerRef.current === 'function') runOnJS(onAnimateListenerRef.current)(value) + }, + ) useEffect(() => { - let onAnimateListener: ModalEventCallback = () => undefined let onCloseListener: ModalEventCallback = () => undefined if (transitionOptions && typeof transitionOptions !== 'function') { - throw new Error(`'${stackItem.name}' transitionOptions should be a function. For instance: + throw new Error(`'${stackItem.name}' transitionOptions should be a worklet function. For instance: import ${stackItem.name} from './src/modals/${stackItem.name}'; - + import {interpolate} from 'react-native-reanimated'; ... ${stackItem.name}: { modal: ${stackItem.name}, - transitionOptions: animatedValue => ({ - opacity: animatedValue.interpolate({ - inputRange: [0, 1, 2, 3], - outputRange: [0, 1, 0.5, 0.25], - extrapolate: 'clamp', - }), - }), + transitionOptions: animatedValue =>{ + 'worklet'; + return { + opacity: interpolate(interpolate.value, [0, 1, 2, 3], [0, 1, 0.5, 0.25]), + } + } }, }`) } eventListeners.forEach((item) => { if (item.event === `${stackItem.hash}_onAnimate`) { - onAnimateListener = item.handler + onAnimateListenerRef.current = item.handler } else if (item.event === `${stackItem.hash}_onClose`) { onCloseListener = item.handler } }) - - animatedListenerId.current = animatedValue.addListener(({ value }) => onAnimateListener(value)) - return () => { - animatedValue.removeAllListeners() + // animatedValue.removeAllListeners() onCloseListener() clearListeners(stackItem.hash) } @@ -117,16 +121,20 @@ const StackItem =

({ modalStackItemCallback?.() }) } else { - Animated.timing(animatedValue, { + animatedValue.value = withTiming( toValue, - useNativeDriver: true, - ...(closeModalCallback ? animateOutConfig : animateInConfig), - }).start(({ finished }) => { - if (finished) { - closeModalCallback?.(stackItem) - modalStackItemCallback?.() - } - }) + { ...(closeModalCallback ? animateOutConfig : animateInConfig) }, + (finished) => { + if (finished) { + if (typeof closeModalCallback === 'function') { + runOnJS(closeModalCallback)(stackItem) + } + if (typeof modalStackItemCallback === 'function') { + runOnJS(modalStackItemCallback)() + } + } + }, + ) } }, [stackItem, animationIn, animationOut, animatedValue, animateInConfig, animateOutConfig], @@ -176,19 +184,22 @@ const StackItem =

({ ({ nativeEvent }) => { if (!disableFlingGesture && nativeEvent.oldState === State.ACTIVE) { const toValue = verticalPosition === 'top' ? vh(-100) : vh(100) - - Animated.timing(translateY, { - toValue, - useNativeDriver: true, - ...animateOutConfig, - }).start(({ finished }) => { - if (finished) closeModal(stackItem) + withTiming(toValue, { ...animateOutConfig }, (finished) => { + if (finished) { + runOnJS(closeStackItem)(stackItem.name) + } }) } }, [animateOutConfig, closeModal, disableFlingGesture, stackItem, translateY, verticalPosition], ) + const reanimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }] })) + + const flingReStyle = useAnimatedStyle(() => ({ + ...(transitionOptions && transitionOptions(animatedValue)), + })) + const renderAnimatedComponent = (): ReactNode => { const Component = stackItem.component const addListener = (eventName: ModalEventName, handler: ModalEventCallback) => @@ -196,14 +207,11 @@ const StackItem =

({ const removeAllListeners = () => clearListeners(stackItem.hash) return ( - + - + { - [key: string]: - | { - [key: string]: ModalTransitionValue - }[] - | ModalTransitionValue -} +export type ModalTransitionOptions = (animatedValue: Animated.SharedValue) => ViewStyle export type ModalEventName = 'onAnimate' | 'onClose' @@ -205,7 +199,7 @@ export interface ModalOptions { * @default { easing: Easing.inOut(Easing.exp), duration: 450 } * @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modaloptions#animateinconfig */ - animateInConfig?: Pick + animateInConfig?: WithTimingConfig /** * Animation function that receives the `animatedValue` used by the library to animate the modal opening, * and a `toValue` argument representing the modal position in the stack. @@ -238,10 +232,10 @@ export interface ModalOptions { * @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modaloptions#animationin */ animationIn?: ( - animatedValue: Animated.Value, + animatedValue: Animated.SharedValue, toValue: number, callback?: () => void, - ) => Animated.CompositeAnimation | void + ) => Animated.AnimateStyle /** * Animation configuration used to animate a modal out (underneath other modals or when closing the last one). * Uses Animated.timing(), if you want to use another animation type, use `animationOut`. @@ -251,7 +245,7 @@ export interface ModalOptions { * @default { easing: Easing.inOut(Easing.exp), duration: 450 } * @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modaloptions#animationout */ - animateOutConfig?: Pick + animateOutConfig?: WithTimingConfig /** * Animation function that receives the `animatedValue` used by the library to animate the modal closing, * and a `toValue` argument representing the modal position in the stack. @@ -283,10 +277,10 @@ export interface ModalOptions { * @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modaloptions#animationout */ animationOut?: ( - animatedValue: Animated.Value, + animatedValue: Animated.SharedValue, toValue: number, callback?: () => void, - ) => Animated.CompositeAnimation | void + ) => Animated.AnimateStyle /** * How you want the modal stack to behave when users press the backdrop, but also when the physical back button is pressed on Android. * diff --git a/src/utils/index.ts b/src/utils/index.ts index 0d1c435..44fd34f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,6 @@ -import { Dimensions, Easing } from 'react-native' +import { Dimensions } from 'react-native' + +import { Easing } from 'react-native-reanimated' import type { ModalOptions } from '../types' diff --git a/yarn.lock b/yarn.lock index e3052c5..8abef4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -728,7 +728,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-object-assign@^7.0.0": +"@babel/plugin-transform-object-assign@^7.0.0", "@babel/plugin-transform-object-assign@^7.10.4": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.16.7.tgz#5fe08d63dccfeb6a33aa2638faf98e5c584100f8" integrity sha512-R8mawvm3x0COTJtveuoqZIjNypn2FjfvXZr4pSQ8VhEFBuQGBz4XhHasZtHXjgXU4XptZ4HtGof3NoYc93ZH9Q== @@ -1929,6 +1929,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/invariant@^2.2.35": + version "2.2.35" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" + integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -6275,6 +6280,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -6795,6 +6805,11 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mockdate@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb" + integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -7627,6 +7642,11 @@ react-devtools-core@^4.13.0: shell-quote "^1.6.1" ws "^7" +react-freeze@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.0.tgz#b21c65fe1783743007c8c9a2952b1c8879a77354" + integrity sha512-yQaiOqDmoKqks56LN9MTgY06O0qQHgV4FUrikH357DydArSZHQhl0BJFqGKIZoTqi8JizF9Dxhuk1FIZD6qCaw== + react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7684,6 +7704,27 @@ react-native-gesture-handler@^1.10.3: invariant "^2.2.4" prop-types "^15.7.2" +react-native-reanimated@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.4.1.tgz#4e33876fba525ce60ac13ab3c81fc3a9f8b132fe" + integrity sha512-kvf7ylGlwa5hxMQ+wpPFjQrI2c6eexf53/xRo+dvXBNefGmSYaYR5sFtD0XMMzIPQlkCB9tJ0Pu9+2WCQUY7Cg== + dependencies: + "@babel/plugin-transform-object-assign" "^7.10.4" + "@types/invariant" "^2.2.35" + invariant "^2.2.4" + lodash.isequal "^4.5.0" + mockdate "^3.0.2" + react-native-screens "^3.4.0" + string-hash-64 "^1.0.3" + +react-native-screens@^3.4.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.13.1.tgz#b3b1c5788dca25a71668909f66d87fb35c5c5241" + integrity sha512-xcrnuUs0qUrGpc2gOTDY4VgHHADQwp80mwR1prU/Q0JqbZN5W3koLhuOsT6FkSRKjR5t40l+4LcjhHdpqRB2HA== + dependencies: + react-freeze "^1.0.0" + warn-once "^0.1.0" + react-native@0.66.4: version "0.66.4" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.66.4.tgz#bf89a5fb18bd23046d889fb4de4ea2822a4d7805" @@ -8543,6 +8584,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-hash-64@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" + integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -9210,6 +9256,11 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" +warn-once@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.0.tgz#4f58d89b84f968d0389176aa99e0cf0f14ffd4c8" + integrity sha512-recZTSvuaH/On5ZU5ywq66y99lImWqzP93+AiUo9LUwG8gXHW+LJjhOd6REJHm7qb0niYqrEQJvbHSQfuJtTqA== + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" From 1840623f2d1c25dd9c0f66985f9346a0ecd82574 Mon Sep 17 00:00:00 2001 From: Mason Le <43195241+MasonLe2497@users.noreply.github.com> Date: Fri, 11 Mar 2022 20:25:23 +0700 Subject: [PATCH 2/2] refactor code --- src/lib/ModalStack.tsx | 93 +++++++++++++++---------------------- src/lib/StackItem.tsx | 102 ++++++++++++++++++++++------------------- 2 files changed, 91 insertions(+), 104 deletions(-) diff --git a/src/lib/ModalStack.tsx b/src/lib/ModalStack.tsx index 565af84..4b0fce6 100644 --- a/src/lib/ModalStack.tsx +++ b/src/lib/ModalStack.tsx @@ -11,6 +11,7 @@ import { getStackItemOptions, sh } from '../utils' type Props

= SharedProps

const ModalStack =

(props: Props

) => { + // state const { stack } = props const [hasChangedBackdropColor, setBackdropColorStatus] = useState(false) @@ -20,55 +21,27 @@ const ModalStack =

(props: Props

) => { const [openActionCallbacks, setOpenActionCallbacks] = useState([]) const opacity = useSharedValue(0) + const translateY = useSharedValue(sh(100)) + const fullHeight = useMemo(() => sh(100), []) - // const { opacity, translateY } = useMemo( - // () => ({ - // opacity: new Animated.Value(0), - // translateY: new Animated.Value(sh(100)), - // }), - // [], - // ) const { backBehavior, backdropColor, backdropOpacity } = useMemo( () => getStackItemOptions(Array.from(stack.openedItems).pop(), stack), [stack], ) - useEffect(() => { - if (stack.openedItemsSize && backdropColor && backdropColor !== 'black' && !hasChangedBackdropColor) { - setBackdropColorStatus(true) - } - }, [backdropColor, hasChangedBackdropColor, stack.openedItemsSize]) + // reanimated style + const backdropReStyle = useAnimatedStyle(() => ({ + opacity: interpolate(opacity.value, [0, 1], [0, backdropOpacity ?? 0.6]), + })) - useEffect(() => { - const scrollY = Platform.OS === 'web' ? window.scrollY ?? document.documentElement.scrollTop : 0 - if (stack.openedItemsSize) { - translateY.value = scrollY - opacity.value = withTiming(1, { easing: Easing.in(Easing.ease), duration: 300 }) - // Animated.timing(opacity, { - // toValue: 1, - // easing: Easing.in(Easing.ease), - // duration: 300, - // useNativeDriver: true, - // }).start() - } else { - opacity.value = withTiming(0, { easing: Easing.inOut(Easing.ease), duration: 300 }, (finished) => { - if (finished) { - translateY.value = fullHeight - } - }) - // Animated.timing(opacity, { - // toValue: 0, - // easing: Easing.inOut(Easing.ease), - // duration: 300, - // useNativeDriver: true, - // }).start(({ finished }) => { - // if (finished) translateY.setValue(sh(100)) - // }) - } - }, [opacity, stack.openedItemsSize, translateY]) + const containerReStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ translateY: translateY.value }], + })) + // func const renderStackItem = (stackItem: ModalStackItem

, index: number) => { const position = stack.openedItemsSize - index const pendingClosingAction: ModalPendingClosingAction | undefined = stack.pendingClosingActions @@ -111,34 +84,18 @@ const ModalStack =

(props: Props

) => { translateY.value = fullHeight } }) - // Animated.timing(opacity, { - // toValue: 0, - // easing: Easing.inOut(Easing.ease), - // duration: 300, - // useNativeDriver: true, - // }).start(({ finished }) => { - // if (finished) translateY.setValue(sh(100)) - // }) } setBackdropClosedItems([...backdropClosedItems, currentItem?.hash]) } - const backdropReStyle = useAnimatedStyle(() => ({ - opacity: interpolate(opacity.value, [0, 1], [0, backdropOpacity ?? 0.6]), - })) - const containerReStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [{ translateY: translateY.value }], - })) - const renderBackdrop = () => { - const onPress = () => onBackdropPress() const backgroundColor = stack.openedItemsSize && backdropColor ? backdropColor : hasChangedBackdropColor ? 'transparent' : 'black' + // render return ( - + (props: Props

) => { ) } + // effect + useEffect(() => { + if (stack.openedItemsSize && backdropColor && backdropColor !== 'black' && !hasChangedBackdropColor) { + setBackdropColorStatus(true) + } + }, [backdropColor, hasChangedBackdropColor, stack.openedItemsSize]) + + useEffect(() => { + const scrollY = Platform.OS === 'web' ? window.scrollY ?? document.documentElement.scrollTop : 0 + if (stack.openedItemsSize) { + translateY.value = scrollY + opacity.value = withTiming(1, { easing: Easing.in(Easing.ease), duration: 300 }) + } else { + opacity.value = withTiming(0, { easing: Easing.inOut(Easing.ease), duration: 300 }, (finished) => { + if (finished) { + translateY.value = fullHeight + } + }) + } + }, [opacity, stack.openedItemsSize, translateY]) + + // render return ( ({ wasOpenCallbackCalled, wasClosedByBackdropPress, }: Props

) => { + // state const animatedValue = useSharedValue(-1) + const translateY = useSharedValue(0) const { @@ -63,50 +65,23 @@ const StackItem =

({ disableFlingGesture, position: verticalPosition, } = useMemo(() => getStackItemOptions(stackItem, stack), [stack, stackItem]) + const onAnimateListenerRef = useRef<(value: number) => void | null>(null) as MutableRefObject< (value: number) => void | null > - useAnimatedReaction( - () => animatedValue.value, - (value) => { - console.log('Change_value') - if (typeof onAnimateListenerRef.current === 'function') runOnJS(onAnimateListenerRef.current)(value) - }, - ) - useEffect(() => { - let onCloseListener: ModalEventCallback = () => undefined - if (transitionOptions && typeof transitionOptions !== 'function') { - throw new Error(`'${stackItem.name}' transitionOptions should be a worklet function. For instance: - import ${stackItem.name} from './src/modals/${stackItem.name}'; - import {interpolate} from 'react-native-reanimated'; - ... - ${stackItem.name}: { - modal: ${stackItem.name}, - transitionOptions: animatedValue =>{ - 'worklet'; - return { - opacity: interpolate(interpolate.value, [0, 1, 2, 3], [0, 1, 0.5, 0.25]), - } - } - }, - }`) - } - - eventListeners.forEach((item) => { - if (item.event === `${stackItem.hash}_onAnimate`) { - onAnimateListenerRef.current = item.handler - } else if (item.event === `${stackItem.hash}_onClose`) { - onCloseListener = item.handler - } - }) - return () => { - // animatedValue.removeAllListeners() - onCloseListener() - clearListeners(stackItem.hash) + const justifyContent = useMemo(() => { + switch (verticalPosition) { + case 'top': + return 'flex-start' + case 'bottom': + return 'flex-end' + default: + return 'center' } - }, []) + }, [verticalPosition]) + // func const updateAnimatedValue = useCallback( ( toValue: number, @@ -235,16 +210,48 @@ const StackItem =

({ ) } - const justifyContent = useMemo(() => { - switch (verticalPosition) { - case 'top': - return 'flex-start' - case 'bottom': - return 'flex-end' - default: - return 'center' + // effect + useAnimatedReaction( + () => animatedValue.value, + (value) => { + console.log('Change_value') + if (typeof onAnimateListenerRef.current === 'function') runOnJS(onAnimateListenerRef.current)(value) + }, + ) + + useEffect(() => { + let onCloseListener: ModalEventCallback = () => undefined + + if (transitionOptions && typeof transitionOptions !== 'function') { + throw new Error(`'${stackItem.name}' transitionOptions should be a worklet function. For instance: + import ${stackItem.name} from './src/modals/${stackItem.name}'; + import {interpolate} from 'react-native-reanimated'; + ... + ${stackItem.name}: { + modal: ${stackItem.name}, + transitionOptions: animatedValue =>{ + 'worklet'; + return { + opacity: interpolate(interpolate.value, [0, 1, 2, 3], [0, 1, 0.5, 0.25]), + } + } + }, + }`) } - }, [verticalPosition]) + + eventListeners.forEach((item) => { + if (item.event === `${stackItem.hash}_onAnimate`) { + onAnimateListenerRef.current = item.handler + } else if (item.event === `${stackItem.hash}_onClose`) { + onCloseListener = item.handler + } + }) + return () => { + // animatedValue.removeAllListeners() + onCloseListener() + clearListeners(stackItem.hash) + } + }, []) useEffect(() => { updateAnimatedValue(position, undefined, wasOpenCallbackCalled ? undefined : stackItem.callback) @@ -277,6 +284,7 @@ const StackItem =

({ } }, [closeAllStackItems, closeStackItem, closeStackItems, pendingClosingAction, removeClosingAction]) + // render return ( {renderAnimatedComponent()}