UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

352 lines (325 loc) • 10.3 kB
'use strict'; import type { RefObject } from 'react'; import { useEffect, useRef } from 'react'; import type { WorkletFunction } from 'react-native-worklets'; import { isWorkletFunction, makeShareable } from 'react-native-worklets'; import { initialUpdaterRun } from '../animation'; import { IS_JEST } from '../common'; import type { AnimatedPropsAdapterFunction, AnimatedPropsAdapterWorklet, AnimatedStyle, SharedValue, Timestamp, ViewDescriptorsWrapper, } from '../commonTypes'; import { startMapper, stopMapper } from '../core'; import type { AnimatedProps } from '../createAnimatedComponent/commonTypes'; import { updateProps } from '../updateProps'; import { makeViewDescriptorsSet } from '../ViewDescriptorsSet'; import type { AnimatedStyleHandle, DefaultStyle, DependencyList, Descriptor, JestAnimatedStyleHandle, } from './commonTypes'; import type { AnimatedState, AnimatedUpdaterData, } from './useAnimatedStyleCommon'; import { buildWorkletsHash, checkSharedValueUsage, prepareAnimation, runAnimations, styleUpdater, } from './useAnimatedStyleCommon'; import { useSharedValue } from './useSharedValue'; import { isAnimated, shallowEqual, validateAnimatedStyles } from './utils'; const updatePropsJestWrapper = IS_JEST ? ( viewDescriptors: ViewDescriptorsWrapper, // eslint-disable-next-line @typescript-eslint/no-explicit-any updates: AnimatedStyle<any>, // eslint-disable-next-line @typescript-eslint/no-explicit-any animatedValues: RefObject<AnimatedStyle<any>>, // eslint-disable-next-line @typescript-eslint/no-explicit-any adapters: ((updates: AnimatedStyle<any>) => void)[] ): void => { adapters.forEach((adapter) => { adapter(updates); }); animatedValues.current.value = { ...animatedValues.current.value, ...updates, }; updateProps(viewDescriptors, updates); } : undefined; function jestStyleUpdater( viewDescriptors: SharedValue<Descriptor[]>, // eslint-disable-next-line @typescript-eslint/no-explicit-any updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>), state: AnimatedState, animationsActive: SharedValue<boolean>, // eslint-disable-next-line @typescript-eslint/no-explicit-any animatedValues: RefObject<AnimatedStyle<any>>, adapters: AnimatedPropsAdapterFunction[], forceUpdate?: boolean ): void { 'worklet'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const animations: AnimatedStyle<any> = state.animations ?? {}; const newValues = updater() ?? {}; const oldValues = state.last; // extract animated props let hasAnimations = false; let frameTimestamp: number | undefined; 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)) { frameTimestamp = global.__frameTimestamp || global._getAnimationTimestamp(); prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } }); function frame(timestamp: Timestamp) { // eslint-disable-next-line @typescript-eslint/no-shadow const { animations, last, isAnimationCancelled } = state; if (isAnimationCancelled) { state.isAnimationRunning = false; return; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const updates: AnimatedStyle<any> = {}; 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?.( viewDescriptors, updates, animatedValues, adapters ); } if (!allFinished) { requestAnimationFrame(frame); } else { state.isAnimationRunning = false; } } if (hasAnimations) { state.animations = animations; if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; frame(frameTimestamp!); } } else { state.isAnimationCancelled = true; state.animations = []; } // calculate diff state.last = newValues; if (!shallowEqual(oldValues, newValues) || forceUpdate) { updatePropsJestWrapper?.( viewDescriptors, newValues, animatedValues, adapters ); } } function animatedStyleHandleToJSON(): string { return '{}'; } /** * Lets you create a styles object, similar to StyleSheet styles, which can be * animated using shared values. * * @param updater - A function returning an object with style properties you * want to animate. * @param dependencies - An optional array of dependencies. Only relevant when * using Reanimated without the Babel plugin on the Web. * @returns An animated style object which has to be passed to the `style` * property of an Animated component you want to animate. * @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedStyle */ // @ts-expect-error Public type definition which strips internals. export function useAnimatedStyle<Style extends DefaultStyle>( updater: () => Style, dependencies?: DependencyList | null ): AnimatedStyleHandle<Style>; export function useAnimatedStyle<Style extends DefaultStyle | AnimatedProps>( updater: | WorkletFunction<[], Style> | ((() => Style) & Record<string, unknown>), dependencies?: DependencyList | null, adapters?: AnimatedPropsAdapterWorklet | AnimatedPropsAdapterWorklet[] | null, isAnimatedProps = false ): | AnimatedStyleHandle<Style | AnimatedProps> | JestAnimatedStyleHandle<Style | AnimatedProps> { const animatedUpdaterData = useRef<AnimatedUpdaterData | null>(null); let inputs = Object.values(updater.__closure ?? {}); if (!inputs.length && dependencies?.length) { // let web work without a Babel plugin inputs = dependencies; } if ( __DEV__ && !inputs.length && !dependencies && !isWorkletFunction(updater) ) { throw new Error( `[Reanimated] \`useAnimatedStyle\` was used without a dependency array or Babel plugin. Please explicitly pass a dependency array, or enable the Babel plugin. For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/docs/guides/web-support#web-without-the-babel-plugin\`.` ); } const adaptersArray = adapters ? Array.isArray(adapters) ? adapters : [adapters] : []; const adaptersHash = adapters ? buildWorkletsHash(adaptersArray) : null; const areAnimationsActive = useSharedValue<boolean>(true); const jestAnimatedValues = useRef<AnimatedStyle<Style | AnimatedProps>>( {} as AnimatedStyle<Style | AnimatedProps> ); // build dependencies if (!dependencies) { dependencies = [...inputs, updater.__workletHash]; } else { dependencies = [...dependencies, updater.__workletHash]; } if (adaptersHash) { dependencies.push(adaptersHash); } if (!animatedUpdaterData.current) { const initialStyle = initialUpdaterRun(updater); if (__DEV__) { validateAnimatedStyles(initialStyle); } animatedUpdaterData.current = { initial: { value: initialStyle, updater, }, remoteState: makeShareable({ last: initialStyle, animations: {}, isAnimationCancelled: false, isAnimationRunning: false, }), viewDescriptors: makeViewDescriptorsSet(), styleUpdaterContainer: { current: undefined }, }; } const { initial, remoteState, viewDescriptors } = animatedUpdaterData.current; const shareableViewDescriptors = viewDescriptors.shareableViewDescriptors; dependencies.push(shareableViewDescriptors); useEffect(() => { let fun; let updaterFn = updater; if (adapters) { updaterFn = (() => { 'worklet'; const newValues = updater(); adaptersArray.forEach((adapter) => { adapter(newValues as Record<string, unknown>); }); return newValues; }) as WorkletFunction<[], Style>; } if (IS_JEST) { fun = (forceUpdate?: boolean) => { 'worklet'; jestStyleUpdater( shareableViewDescriptors, updater, remoteState, areAnimationsActive, jestAnimatedValues, adaptersArray, forceUpdate ); }; } else { fun = (forceUpdate?: boolean) => { 'worklet'; styleUpdater( shareableViewDescriptors, updaterFn, remoteState, areAnimationsActive, isAnimatedProps, forceUpdate ); }; } if (animatedUpdaterData.current) { animatedUpdaterData.current.styleUpdaterContainer.current = fun; } const mapperId = startMapper(fun, inputs); return () => { stopMapper(mapperId); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, dependencies); useEffect(() => { areAnimationsActive.value = true; return () => { areAnimationsActive.value = false; }; }, [areAnimationsActive]); if (__DEV__) { checkSharedValueUsage(initial.value); } const animatedStyleHandle = useRef< | AnimatedStyleHandle<Style | AnimatedProps> | JestAnimatedStyleHandle<Style | AnimatedProps> | null >(null); if (!animatedStyleHandle.current) { const styleUpdaterContainer = animatedUpdaterData.current.styleUpdaterContainer; animatedStyleHandle.current = IS_JEST ? { viewDescriptors, initial, jestAnimatedValues, toJSON: animatedStyleHandleToJSON, styleUpdaterContainer, } : { viewDescriptors, initial, styleUpdaterContainer, }; } return animatedStyleHandle.current; }