UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

323 lines (301 loc) • 10.5 kB
'use strict'; import { withDelay, withSequence, withTiming } from '../../animation'; import { assertEasingIsWorklet, getReduceMotionFromConfig, } from '../../animation/util'; import type { AnimationFunction, EasingFunction, EntryExitAnimationFunction, IEntryExitAnimationBuilder, KeyframeProps, MaybeInvalidKeyframeProps, StyleProps, StylePropsWithArrayTransform, TransformArrayItem, ValidKeyframeProps, } from '../../commonTypes'; import { ReduceMotion } from '../../commonTypes'; import type { EasingFunctionFactory } from '../../Easing'; import { Easing } from '../../Easing'; import { ReanimatedError } from '../../errors'; interface KeyframePoint { duration: number; value: number | string; easing?: EasingFunction | EasingFunctionFactory; } interface ParsedKeyframesDefinition { initialValues: StyleProps; keyframes: Record<string, KeyframePoint[]>; } class InnerKeyframe implements IEntryExitAnimationBuilder { durationV?: number; delayV?: number; reduceMotionV: ReduceMotion = ReduceMotion.System; callbackV?: (finished: boolean) => void; definitions: MaybeInvalidKeyframeProps; parsedAnimation?: EntryExitAnimationFunction; /* Keyframe definition should be passed in the constructor as the map which keys are between range 0 - 100 (%) and correspond to the point in the animation progress. */ constructor(definitions: ValidKeyframeProps) { this.definitions = definitions as MaybeInvalidKeyframeProps; } private parseDefinitions(): ParsedKeyframesDefinition { /* Each style property contain an array with all their key points: value, duration of transition to that value, and optional easing function (defaults to Linear) */ const parsedKeyframes: Record<string, KeyframePoint[]> = {}; /* Parsing keyframes 'from' and 'to'. */ if (this.definitions.from) { if (this.definitions['0']) { throw new ReanimatedError( "You cannot provide both keyframe 0 and 'from' as they both specified initial values." ); } this.definitions['0'] = this.definitions.from; delete this.definitions.from; } if (this.definitions.to) { if (this.definitions['100']) { throw new ReanimatedError( "You cannot provide both keyframe 100 and 'to' as they both specified values at the end of the animation." ); } this.definitions['100'] = this.definitions.to; delete this.definitions.to; } /* One of the assumptions is that keyframe 0 is required to properly set initial values. Every other keyframe should contain properties from the set provided as initial values. */ if (!this.definitions['0']) { throw new ReanimatedError( "Please provide 0 or 'from' keyframe with initial state of your object." ); } const initialValues: StyleProps = this.definitions['0'] as StyleProps; /* Initialize parsedKeyframes for properties provided in initial keyframe */ Object.keys(initialValues).forEach((styleProp: string) => { if (styleProp === 'transform') { if (!Array.isArray(initialValues.transform)) { return; } initialValues.transform.forEach((transformStyle, index) => { Object.keys(transformStyle).forEach((transformProp: string) => { parsedKeyframes[makeKeyframeKey(index, transformProp)] = []; }); }); } else { parsedKeyframes[styleProp] = []; } }); const duration: number = this.durationV ? this.durationV : 500; const animationKeyPoints: Array<number> = Array.from( Object.keys(this.definitions) ).map(Number); const getAnimationDuration = ( key: string, currentKeyPoint: number ): number => { const maxDuration = (currentKeyPoint / 100) * duration; const currentDuration = parsedKeyframes[key].reduce( (acc: number, value: KeyframePoint) => acc + value.duration, 0 ); return maxDuration - currentDuration; }; /* Other keyframes can't contain properties that were not specified in initial keyframe. */ const addKeyPoint = ({ key, value, currentKeyPoint, easing, }: { key: string; value: string | number; currentKeyPoint: number; easing?: EasingFunction | EasingFunctionFactory; }): void => { if (!(key in parsedKeyframes)) { throw new ReanimatedError( "Keyframe can contain only that set of properties that were provide with initial values (keyframe 0 or 'from')" ); } if (__DEV__ && easing) { assertEasingIsWorklet(easing); } parsedKeyframes[key].push({ duration: getAnimationDuration(key, currentKeyPoint), value, easing, }); }; animationKeyPoints .filter((value: number) => value !== 0) .sort((a: number, b: number) => a - b) .forEach((keyPoint: number) => { if (keyPoint < 0 || keyPoint > 100) { throw new ReanimatedError( 'Keyframe should be in between range 0 - 100.' ); } const keyframe: KeyframeProps = this.definitions[keyPoint]; const easing = keyframe.easing; delete keyframe.easing; const addKeyPointWith = (key: string, value: string | number) => addKeyPoint({ key, value, currentKeyPoint: keyPoint, easing, }); Object.keys(keyframe).forEach((key: string) => { if (key === 'transform') { if (!Array.isArray(keyframe.transform)) { return; } keyframe.transform.forEach((transformStyle, index) => { Object.keys(transformStyle).forEach((transformProp: string) => { addKeyPointWith( makeKeyframeKey(index, transformProp), transformStyle[ transformProp as keyof typeof transformStyle ] as number | string // Here we assume that user has passed props of proper type. // I don't think it's worthwhile to check if he passed i.e. `Animated.Node`. ); }); }); } else { addKeyPointWith(key, keyframe[key]); } }); }); return { initialValues, keyframes: parsedKeyframes }; } duration(durationMs: number): InnerKeyframe { this.durationV = durationMs; return this; } delay(delayMs: number): InnerKeyframe { this.delayV = delayMs; return this; } withCallback(callback: (finsihed: boolean) => void): InnerKeyframe { this.callbackV = callback; return this; } reduceMotion(reduceMotionV: ReduceMotion): this { this.reduceMotionV = reduceMotionV; return this; } private getDelayFunction(): AnimationFunction { const delay = this.delayV; const reduceMotion = this.reduceMotionV; return delay ? // eslint-disable-next-line @typescript-eslint/no-shadow (delay, animation) => { 'worklet'; return withDelay(delay, animation, reduceMotion); } : (_, animation) => { 'worklet'; animation.reduceMotion = getReduceMotionFromConfig(reduceMotion); return animation; }; } build = (): EntryExitAnimationFunction => { const delay = this.delayV; const delayFunction = this.getDelayFunction(); const { keyframes, initialValues } = this.parseDefinitions(); const callback = this.callbackV; if (this.parsedAnimation) { return this.parsedAnimation; } this.parsedAnimation = () => { 'worklet'; const animations: StylePropsWithArrayTransform = {}; /* For each style property, an animations sequence is created that corresponds with its key points. Transform style properties require special handling because of their nested structure. */ const addAnimation = (key: string) => { const keyframePoints = keyframes[key]; // in case if property was only passed as initial value if (keyframePoints.length === 0) { return; } const animation = delayFunction( delay, keyframePoints.length === 1 ? withTiming(keyframePoints[0].value, { duration: keyframePoints[0].duration, easing: keyframePoints[0].easing ? keyframePoints[0].easing : Easing.linear, }) : withSequence( ...keyframePoints.map((keyframePoint: KeyframePoint) => withTiming(keyframePoint.value, { duration: keyframePoint.duration, easing: keyframePoint.easing ? keyframePoint.easing : Easing.linear, }) ) ) ); if (key.includes('transform')) { if (!('transform' in animations)) { animations.transform = []; } animations.transform!.push(<TransformArrayItem>{ [key.split(':')[1]]: animation, }); } else { animations[key] = animation; } }; Object.keys(initialValues).forEach((key: string) => { if (key.includes('transform')) { initialValues[key].forEach( (transformProp: Record<string, number | string>, index: number) => { Object.keys(transformProp).forEach((transformPropKey: string) => { addAnimation(makeKeyframeKey(index, transformPropKey)); }); } ); } else { addAnimation(key); } }); return { animations, initialValues, callback, }; }; return this.parsedAnimation; }; } function makeKeyframeKey(index: number, transformProp: string) { 'worklet'; return `${index}_transform:${transformProp}`; } export declare class ReanimatedKeyframe { constructor(definitions: ValidKeyframeProps); duration(durationMs: number): ReanimatedKeyframe; delay(delayMs: number): ReanimatedKeyframe; reduceMotion(reduceMotionV: ReduceMotion): ReanimatedKeyframe; withCallback(callback: (finished: boolean) => void): ReanimatedKeyframe; } export const Keyframe = InnerKeyframe as typeof ReanimatedKeyframe;