UNPKG

create-expo-cljs-app

Version:

Create a react native application with Expo and Shadow-CLJS!

539 lines (504 loc) 14.4 kB
/* global _frameTimestamp */ import { MutableRefObject, useEffect, useRef } from 'react'; import { startMapper, stopMapper, makeRemote, requestFrame, getTimestamp, makeMutable, } from '../core'; import updateProps, { updatePropsJestWrapper } from '../UpdateProps'; import { initialUpdaterRun, Timestamp } from '../animation'; import NativeReanimatedModule from '../NativeReanimated'; import { useSharedValue } from './useSharedValue'; import { buildWorkletsHash, canApplyOptimalisation, getStyleWithoutAnimations, hasColorProps, isAnimated, parseColors, styleDiff, validateAnimatedStyles, } from './utils'; import { DependencyList, Descriptor } from './commonTypes'; import { makeViewDescriptorsSet, makeViewsRefSet, ViewDescriptorsSet, ViewRefSet, } from '../ViewDescriptorsSet'; import { isJest, shouldBeUseWeb } from '../PlatformChecker'; import { AnimationObject } from '../animation/commonTypes'; import { AdapterWorkletFunction, AnimatedStyle, BasicWorkletFunction, NestedObjectValues, SharedValue, } from '../commonTypes'; export interface AnimatedStyleResult { viewDescriptors: ViewDescriptorsSet; initial: AnimatedStyle; viewsRef: ViewRefSet<any>; animatedStyle?: MutableRefObject<AnimatedStyle>; } interface AnimatedState { last: AnimatedStyle; animations: AnimatedStyle; isAnimationRunning: boolean; isAnimationCancelled: boolean; } interface AnimationRef { initial: { value: AnimatedStyle; updater: () => AnimatedStyle; }; remoteState: AnimatedState; sharableViewDescriptors: SharedValue<Descriptor[]>; } function prepareAnimation( animatedProp: AnimatedStyle, lastAnimation: AnimatedStyle, lastValue: AnimatedStyle ): void { 'worklet'; 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: 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] ) ); } } function runAnimations( animation: AnimatedStyle, timestamp: Timestamp, key: number | string, result: AnimatedStyle, animationsActive: SharedValue<boolean> ): boolean { 'worklet'; 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; } } function styleUpdater( viewDescriptors: SharedValue<Descriptor[]>, updater: BasicWorkletFunction<AnimatedStyle>, state: AnimatedState, maybeViewRef: ViewRefSet<any> | undefined, animationsActive: SharedValue<boolean> ): void { 'worklet'; const animations = state.animations ?? {}; const newValues = updater() ?? {}; const oldValues = state.last; let hasAnimations = false; for (const key in newValues) { const value = newValues[key]; if (isAnimated(value)) { prepareAnimation(value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } else { delete animations[key]; } } if (hasAnimations) { const frame = (timestamp: Timestamp) => { const { animations, last, isAnimationCancelled } = state; if (isAnimationCancelled) { state.isAnimationRunning = false; return; } const updates: AnimatedStyle = {}; let allFinished = true; for (const propName in animations) { const finished = runAnimations( animations[propName], timestamp, propName, updates, animationsActive ); if (finished) { last[propName] = updates[propName]; delete animations[propName]; } else { allFinished = false; } } if (updates) { updateProps(viewDescriptors, updates, maybeViewRef); } if (!allFinished) { requestFrame(frame); } else { state.isAnimationRunning = false; } }; state.animations = animations; if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; if (_frameTimestamp) { frame(_frameTimestamp); } else { requestFrame(frame); } } state.last = Object.assign({}, oldValues, newValues); const style = getStyleWithoutAnimations(state.last); if (style) { updateProps(viewDescriptors, style, maybeViewRef); } } else { state.isAnimationCancelled = true; state.animations = []; updateProps(viewDescriptors, newValues, maybeViewRef); } } function jestStyleUpdater( viewDescriptors: SharedValue<Descriptor[]>, updater: BasicWorkletFunction<AnimatedStyle>, state: AnimatedState, maybeViewRef: ViewRefSet<any> | undefined, animationsActive: SharedValue<boolean>, animatedStyle: MutableRefObject<AnimatedStyle>, adapters: AdapterWorkletFunction[] = [] ): void { 'worklet'; const animations: AnimatedStyle = 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: Timestamp) { const { animations, last, isAnimationCancelled } = state; if (isAnimationCancelled) { state.isAnimationRunning = false; return; } const updates: AnimatedStyle = {}; 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, maybeViewRef, animatedStyle, 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) { updatePropsJestWrapper( viewDescriptors, diff, maybeViewRef, animatedStyle, adapters ); } } // check for invalid usage of shared values in returned object function checkSharedValueUsage( prop: NestedObjectValues<AnimationObject>, currentKey?: string ): void { if (Array.isArray(prop)) { // if it's an array (i.ex. transform) validate all its elements for (const element of prop) { checkSharedValueUsage(element, currentKey); } } else if (typeof prop === 'object' && prop.value === undefined) { // if it's a nested object, run validation for all its props for (const key of Object.keys(prop)) { checkSharedValueUsage(prop[key], key); } } else if ( currentKey !== undefined && typeof prop === 'object' && prop.value !== undefined ) { // if shared value is passed insted of its value, throw an error throw new Error( `invalid value passed to \`${currentKey}\`, maybe you forgot to use \`.value\`?` ); } } export function useAnimatedStyle<T extends AnimatedStyle>( updater: BasicWorkletFunction<T>, dependencies?: DependencyList, adapters?: AdapterWorkletFunction | AdapterWorkletFunction[] ): AnimatedStyleResult { const viewsRef: ViewRefSet<any> = makeViewsRefSet(); const viewDescriptors: ViewDescriptorsSet = makeViewDescriptorsSet(); const initRef = useRef<AnimationRef>(); const inputs = Object.values(updater._closure ?? {}); const adaptersArray: AdapterWorkletFunction[] = adapters ? Array.isArray(adapters) ? adapters : [adapters] : []; const adaptersHash = adapters ? buildWorkletsHash(adaptersArray) : null; const animationsActive = useSharedValue<boolean>(true); const animatedStyle: MutableRefObject<AnimatedStyle> = useRef<AnimatedStyle>( {} ); // build dependencies if (!dependencies) { dependencies = [...inputs, updater.__workletHash]; } else { dependencies.push(updater.__workletHash); } adaptersHash && dependencies.push(adaptersHash); if (!initRef.current) { const initialStyle: AnimatedStyle = initialUpdaterRun(updater); validateAnimatedStyles(initialStyle); initRef.current = { initial: { value: initialStyle, updater: updater, }, remoteState: makeRemote({ last: initialStyle }), sharableViewDescriptors: makeMutable([]), }; viewDescriptors.rebuildsharableViewDescriptors( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion initRef.current!.sharableViewDescriptors ); } dependencies.push(initRef.current?.sharableViewDescriptors.value); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { initial, remoteState, sharableViewDescriptors } = initRef.current!; const maybeViewRef = NativeReanimatedModule.native ? undefined : viewsRef; useEffect(() => { let fun; let upadterFn = updater; let optimalization = updater.__optimalization; if (adapters) { upadterFn = () => { 'worklet'; const newValues = updater(); adaptersArray.forEach((adapter) => { adapter(newValues); }); return newValues; }; } if (canApplyOptimalisation(upadterFn) && !shouldBeUseWeb()) { if (hasColorProps(upadterFn())) { upadterFn = () => { 'worklet'; const style = upadterFn(); parseColors(style); return style; }; } } else if (!shouldBeUseWeb()) { optimalization = 0; upadterFn = () => { 'worklet'; const style = upadterFn(); parseColors(style); return style; }; } if (typeof updater.__optimalization !== undefined) { upadterFn.__optimalization = optimalization; } if (isJest()) { fun = () => { 'worklet'; jestStyleUpdater( sharableViewDescriptors, updater, remoteState, maybeViewRef, animationsActive, animatedStyle, adaptersArray ); }; } else { fun = () => { 'worklet'; styleUpdater( sharableViewDescriptors, upadterFn, remoteState, maybeViewRef, animationsActive ); }; } const mapperId = startMapper( fun, inputs, [], upadterFn, // TODO fix this sharableViewDescriptors ); return () => { stopMapper(mapperId); }; }, dependencies); useEffect(() => { animationsActive.value = true; return () => { // initRef.current = null; // viewsRef = null; animationsActive.value = false; }; }, []); checkSharedValueUsage(initial.value); if (process.env.JEST_WORKER_ID) { return { viewDescriptors, initial: initial, viewsRef, animatedStyle }; } else { return { viewDescriptors, initial: initial, viewsRef }; } }