UNPKG

react-native-flip

Version:
853 lines (773 loc) 22.3 kB
/* global _frameTimestamp */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import { useEffect, useRef, useCallback } from 'react'; import WorkletEventHandler from './WorkletEventHandler'; import { startMapper, stopMapper, makeMutable, makeRemote, requestFrame, getTimestamp, } from './core'; import updateProps, { updatePropsJestWrapper } from './UpdateProps'; import { initialUpdaterRun, cancelAnimation } from './animations'; import { getTag } from './NativeMethods'; import NativeReanimated from './NativeReanimated'; import { Platform } from 'react-native'; export function useSharedValue(init) { const ref = useRef(null); if (ref.current === null) { ref.current = makeMutable(init); } useEffect(() => { return () => { cancelAnimation(ref.current); }; }, []); return ref.current; } export function useEvent(handler, eventNames = [], rebuild = false) { const initRef = useRef(null); if (initRef.current === null) { initRef.current = new WorkletEventHandler(handler, eventNames); } else if (rebuild) { initRef.current.updateWorklet(handler); } useEffect(() => { return () => { initRef.current = null; }; }, []); return initRef; } function prepareAnimation(animatedProp, lastAnimation, lastValue) { 'worklet'; function prepareAnimation(animatedProp, lastAnimation, lastValue) { if (Array.isArray(animatedProp)) { animatedProp.forEach((prop, index) => prepareAnimation( prop, lastAnimation && lastAnimation[index], lastValue && lastValue[index] ) ); return animatedProp; } if (typeof animatedProp === 'object' && animatedProp.onFrame) { const animation = animatedProp; let value = animation.current; if (lastValue !== undefined) { if (typeof lastValue === 'object') { if (lastValue.value !== undefined) { // previously it was a shared value value = lastValue.value; } else if (lastValue.onFrame !== undefined) { if (lastAnimation?.current !== undefined) { // it was an animation before, copy its state value = lastAnimation.current; } else if (lastValue?.current !== undefined) { // it was initialized value = lastValue.current; } } } else { // previously it was a plain value, just set it as starting point value = lastValue; } } animation.callStart = (timestamp) => { animation.onStart(animation, value, timestamp, lastAnimation); }; animation.callStart(getTimestamp()); animation.callStart = null; } else if (typeof animatedProp === 'object') { // it is an object Object.keys(animatedProp).forEach((key) => prepareAnimation( animatedProp[key], lastAnimation && lastAnimation[key], lastValue && lastValue[key] ) ); } } return prepareAnimation(animatedProp, lastAnimation, lastValue); } function runAnimations(animation, timestamp, key, result, animationsActive) { 'worklet'; function runAnimations(animation, timestamp, key, result, animationsActive) { if (!animationsActive.value) { return true; } if (Array.isArray(animation)) { result[key] = []; let allFinished = true; animation.forEach((entry, index) => { if ( !runAnimations(entry, timestamp, index, result[key], animationsActive) ) { allFinished = false; } }); return allFinished; } else if (typeof animation === 'object' && animation.onFrame) { let finished = true; if (!animation.finished) { if (animation.callStart) { animation.callStart(timestamp); animation.callStart = null; } finished = animation.onFrame(animation, timestamp); animation.timestamp = timestamp; if (finished) { animation.finished = true; animation.callback && animation.callback(true /* finished */); } } result[key] = animation.current; return finished; } else if (typeof animation === 'object') { result[key] = {}; let allFinished = true; Object.keys(animation).forEach((k) => { if ( !runAnimations( animation[k], timestamp, k, result[key], animationsActive ) ) { allFinished = false; } }); return allFinished; } else { result[key] = animation; return true; } } return runAnimations(animation, timestamp, key, result, animationsActive); } // TODO: recirsive worklets aren't supported yet function isAnimated(prop) { 'worklet'; function isAnimated(prop) { if (Array.isArray(prop)) { return prop.some(isAnimated); } if (typeof prop === 'object') { if (prop.onFrame) { return true; } return Object.keys(prop).some((key) => isAnimated(prop[key])); } return false; } return isAnimated(prop); } function styleDiff(oldStyle, newStyle) { 'worklet'; const diff = {}; Object.keys(oldStyle).forEach((key) => { if (newStyle[key] === undefined) { diff[key] = null; } }); Object.keys(newStyle).forEach((key) => { const value = newStyle[key]; const oldValue = oldStyle[key]; if (isAnimated(value)) { // do nothing return; } if ( oldValue !== value && JSON.stringify(oldValue) !== JSON.stringify(value) ) { // I'd use deep equal here but that'd take additional work and this was easier diff[key] = value; } }); return diff; } const validateAnimatedStyles = (styles) => { 'worklet'; if (typeof styles !== 'object') { throw new Error( `useAnimatedStyle has to return an object, found ${typeof styles} instead` ); } else if (Array.isArray(styles)) { throw new Error( 'useAnimatedStyle has to return an object and cannot return static styles combined with dynamic ones. Please do merging where a component receives props.' ); } }; function styleUpdater( viewDescriptor, updater, state, maybeViewRef, adapters, animationsActive ) { 'worklet'; const animations = state.animations || {}; const newValues = updater() || {}; const oldValues = state.last; // extract animated props let hasAnimations = false; Object.keys(animations).forEach((key) => { const value = newValues[key]; if (!isAnimated(value)) { delete animations[key]; } }); Object.keys(newValues).forEach((key) => { const value = newValues[key]; if (isAnimated(value)) { prepareAnimation(value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } }); function frame(timestamp) { const { animations, last, isAnimationCancelled } = state; if (isAnimationCancelled) { state.isAnimationRunning = false; return; } const updates = {}; let allFinished = true; Object.keys(animations).forEach((propName) => { const finished = runAnimations( animations[propName], timestamp, propName, updates, animationsActive ); if (finished) { last[propName] = updates[propName]; delete animations[propName]; } else { allFinished = false; } }); if (Object.keys(updates).length) { updateProps(viewDescriptor, updates, maybeViewRef, adapters); } if (!allFinished) { requestFrame(frame); } else { state.isAnimationRunning = false; } } if (hasAnimations) { state.animations = animations; if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; if (_frameTimestamp) { frame(_frameTimestamp); } else { requestFrame(frame); } } } else { state.isAnimationCancelled = true; state.animations = {}; } // calculate diff const diff = styleDiff(oldValues, newValues); state.last = Object.assign({}, oldValues, newValues); if (Object.keys(diff).length !== 0) { updateProps(viewDescriptor, diff, maybeViewRef, adapters); } } function jestStyleUpdater( viewDescriptor, updater, state, maybeViewRef, adapters, animationsActive, animatedStyle ) { 'worklet'; const animations = state.animations || {}; const newValues = updater() || {}; const oldValues = state.last; // extract animated props let hasAnimations = false; Object.keys(animations).forEach((key) => { const value = newValues[key]; if (!isAnimated(value)) { delete animations[key]; } }); Object.keys(newValues).forEach((key) => { const value = newValues[key]; if (isAnimated(value)) { prepareAnimation(value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } }); function frame(timestamp) { const { animations, last, isAnimationCancelled } = state; if (isAnimationCancelled) { state.isAnimationRunning = false; return; } const updates = {}; let allFinished = true; Object.keys(animations).forEach((propName) => { const finished = runAnimations( animations[propName], timestamp, propName, updates, animationsActive ); if (finished) { last[propName] = updates[propName]; delete animations[propName]; } else { allFinished = false; } }); if (Object.keys(updates).length) { updatePropsJestWrapper( viewDescriptor, updates, maybeViewRef, adapters, animatedStyle ); } if (!allFinished) { requestFrame(frame); } else { state.isAnimationRunning = false; } } if (hasAnimations) { state.animations = animations; if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; if (_frameTimestamp) { frame(_frameTimestamp); } else { requestFrame(frame); } } } else { state.isAnimationCancelled = true; state.animations = {}; } // calculate diff const diff = styleDiff(oldValues, newValues); state.last = Object.assign({}, oldValues, newValues); if (Object.keys(diff).length !== 0) { updatePropsJestWrapper( viewDescriptor, diff, maybeViewRef, adapters, animatedStyle ); } } export function useAnimatedStyle(updater, dependencies, adapters) { const viewDescriptor = useSharedValue({ tag: -1, name: null }, false); const initRef = useRef(null); const inputs = Object.values(updater._closure); const viewRef = useRef(null); adapters = !adapters || Array.isArray(adapters) ? adapters : [adapters]; const adaptersHash = adapters ? buildWorkletsHash(adapters) : null; const animationsActive = useSharedValue(true); let animatedStyle; if (process.env.JEST_WORKER_ID) { animatedStyle = useRef({}); } // build dependencies if (!dependencies) { dependencies = [...inputs, updater.__workletHash]; } else { dependencies.push(updater.__workletHash); } adaptersHash && dependencies.push(adaptersHash); if (initRef.current === null) { const initial = initialUpdaterRun(updater); validateAnimatedStyles(initial); initRef.current = { initial, remoteState: makeRemote({ last: initial }), }; } const { remoteState, initial } = initRef.current; const maybeViewRef = NativeReanimated.native ? undefined : viewRef; useEffect(() => { let fun; if (process.env.JEST_WORKER_ID) { fun = () => { 'worklet'; jestStyleUpdater( viewDescriptor, updater, remoteState, maybeViewRef, adapters, animationsActive, animatedStyle ); }; } else { fun = () => { 'worklet'; styleUpdater( viewDescriptor, updater, remoteState, maybeViewRef, adapters, animationsActive ); }; } const mapperId = startMapper(fun, inputs, []); return () => { stopMapper(mapperId); }; }, dependencies); useEffect(() => { animationsActive.value = true; return () => { initRef.current = null; viewRef.current = null; animationsActive.value = false; }; }, []); // check for invalid usage of shared values in returned object let wrongKey; const isObjectValid = (element, key) => { const result = typeof element === 'object' && element.value !== undefined; if (result) { wrongKey = key; } return !result; }; const isError = Object.keys(initial).some((key) => { const element = initial[key]; let result = false; // a case for transform that has a format of an array of objects if (Array.isArray(element)) { for (const elementArrayItem of element) { // this means unhandled format and it doesn't match the transform format if (typeof elementArrayItem !== 'object') { break; } const objectValue = Object.values(elementArrayItem)[0]; result = isObjectValid(objectValue, key); if (!result) { break; } } } else { result = isObjectValid(element, key); } return !result; }); if (isError && wrongKey !== undefined) { throw new Error( `invalid value passed to \`${wrongKey}\`, maybe you forgot to use \`.value\`?` ); } if (process.env.JEST_WORKER_ID) { return { viewDescriptor, initial, viewRef, animatedStyle }; } else { return { viewDescriptor, initial, viewRef }; } } // TODO: we should make sure that when useAP is used we are not assigning styles // when you need styles to animated you should always use useAS export const useAnimatedProps = useAnimatedStyle; export function useDerivedValue(processor, dependencies) { const initRef = useRef(null); const inputs = Object.values(processor._closure); // build dependencies if (dependencies === undefined) { dependencies = [...inputs, processor.__workletHash]; } else { dependencies.push(processor.__workletHash); } if (initRef.current === null) { initRef.current = makeMutable(initialUpdaterRun(processor)); } const sharedValue = initRef.current; useEffect(() => { const fun = () => { 'worklet'; sharedValue.value = processor(); }; const mapperId = startMapper(fun, inputs, [sharedValue]); return () => { stopMapper(mapperId); }; }, dependencies); useEffect(() => { return () => { initRef.current = null; }; }, []); return sharedValue; } // builds one big hash from multiple worklets' hashes function buildWorkletsHash(handlers) { return Object.keys(handlers).reduce( (previousValue, key) => previousValue === null ? handlers[key].__workletHash : previousValue.toString() + handlers[key].__workletHash.toString(), null ); } // builds dependencies array for gesture handlers function buildDependencies(dependencies, handlers) { if (!dependencies) { dependencies = Object.keys(handlers).map((handlerKey) => { const handler = handlers[handlerKey]; return { workletHash: handler.__workletHash, closure: handler._closure, }; }); } else { dependencies.push(buildWorkletsHash(handlers)); } return dependencies; } // this is supposed to work as useEffect comparison function areDependenciesEqual(nextDeps, prevDeps) { function is(x, y) { /* eslint-disable no-self-compare */ return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); /* eslint-enable no-self-compare */ } const objectIs = typeof Object.is === 'function' ? Object.is : is; function areHookInputsEqual(nextDeps, prevDeps) { if (!nextDeps || !prevDeps || prevDeps.length !== nextDeps.length) { return false; } for (let i = 0; i < prevDeps.length; ++i) { if (!objectIs(nextDeps[i], prevDeps[i])) { return false; } } return true; } return areHookInputsEqual(nextDeps, prevDeps); } export function useAnimatedGestureHandler(handlers, dependencies) { const initRef = useRef(null); if (initRef.current === null) { initRef.current = { context: makeRemote({}), savedDependencies: [], }; } useEffect(() => { return () => { initRef.current = null; }; }, []); const { context, savedDependencies } = initRef.current; dependencies = buildDependencies(dependencies, handlers); const dependenciesDiffer = !areDependenciesEqual( dependencies, savedDependencies ); initRef.current.savedDependencies = dependencies; const handler = (event) => { 'worklet'; event = Platform.OS === 'web' ? event.nativeEvent : event; const FAILED = 1; const BEGAN = 2; const CANCELLED = 3; const ACTIVE = 4; const END = 5; if (event.state === BEGAN && handlers.onStart) { handlers.onStart(event, context); } if (event.state === ACTIVE && handlers.onActive) { handlers.onActive(event, context); } if (event.oldState === ACTIVE && event.state === END && handlers.onEnd) { handlers.onEnd(event, context); } if (event.oldState === BEGAN && event.state === FAILED && handlers.onFail) { handlers.onFail(event, context); } if ( event.oldState === ACTIVE && event.state === CANCELLED && handlers.onCancel ) { handlers.onCancel(event, context); } if ( (event.oldState === BEGAN || event.oldState === ACTIVE) && event.state !== BEGAN && event.state !== ACTIVE && handlers.onFinish ) { handlers.onFinish( event, context, event.state === CANCELLED || event.state === FAILED ); } }; if (Platform.OS === 'web') { return handler; } return useEvent( handler, ['onGestureHandlerStateChange', 'onGestureHandlerEvent'], dependenciesDiffer ); } export function useAnimatedScrollHandler(handlers, dependencies) { const initRef = useRef(null); if (initRef.current === null) { initRef.current = { context: makeRemote({}), savedDependencies: [], }; } useEffect(() => { return () => { initRef.current = null; }; }, []); const { context, savedDependencies } = initRef.current; dependencies = buildDependencies(dependencies, handlers); const dependenciesDiffer = !areDependenciesEqual( dependencies, savedDependencies ); initRef.current.savedDependencies = dependencies; // build event subscription array const subscribeForEvents = ['onScroll']; if (handlers.onBeginDrag !== undefined) { subscribeForEvents.push('onScrollBeginDrag'); } if (handlers.onEndDrag !== undefined) { subscribeForEvents.push('onScrollEndDrag'); } if (handlers.onMomentumBegin !== undefined) { subscribeForEvents.push('onMomentumScrollBegin'); } if (handlers.onMomentumEnd !== undefined) { subscribeForEvents.push('onMomentumScrollEnd'); } return useEvent( (event) => { 'worklet'; const { onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd, } = handlers; if (event.eventName.endsWith('onScroll')) { if (onScroll) { onScroll(event, context); } else if (typeof handlers === 'function') { handlers(event, context); } } else if (onBeginDrag && event.eventName.endsWith('onScrollBeginDrag')) { onBeginDrag(event, context); } else if (onEndDrag && event.eventName.endsWith('onScrollEndDrag')) { onEndDrag(event, context); } else if ( onMomentumBegin && event.eventName.endsWith('onMomentumScrollBegin') ) { onMomentumBegin(event, context); } else if ( onMomentumEnd && event.eventName.endsWith('onMomentumScrollEnd') ) { onMomentumEnd(event, context); } }, subscribeForEvents, dependenciesDiffer ); } export function useAnimatedRef() { const tag = useSharedValue(-1); const ref = useRef(null); if (!ref.current) { const fun = function (component) { 'worklet'; // enters when ref is set by attaching to a component if (component) { tag.value = getTag(component); fun.current = component; } return tag.value; }; Object.defineProperty(fun, 'current', { value: null, writable: true, enumerable: false, }); ref.current = fun; } return ref.current; } /** * @param prepare - worklet used for data preparation for the second parameter * @param react - worklet which takes data prepared by the one in the first parameter and performs certain actions * the first worklet defines the inputs, in other words on which shared values change will it be called. * the second one can modify any shared values but those which are mentioned in the first worklet. Beware of that, because this may result in endless loop and high cpu usage. */ export function useAnimatedReaction(prepare, react, dependencies) { const previous = useSharedValue(null); if (dependencies === undefined) { dependencies = [ Object.values(prepare._closure), Object.values(react._closure), prepare.__workletHash, react.__workletHash, ]; } else { dependencies.push(prepare.__workletHash, react.__workletHash); } useEffect(() => { const fun = () => { 'worklet'; const input = prepare(); react(input, previous.value); previous.value = input; }; const mapperId = startMapper(fun, Object.values(prepare._closure), []); return () => { stopMapper(mapperId); }; }, dependencies); } export function useWorkletCallback(fun, deps) { return useCallback(fun, deps); } export function createWorklet(fun) { return fun; }