UNPKG

15.9 kBPlain TextView Raw
1'use strict';
2import type { MutableRefObject } from 'react';
3import { useEffect, useRef } from 'react';
4
5import { makeShareable, startMapper, stopMapper } from '../core';
6import updateProps, { updatePropsJestWrapper } from '../UpdateProps';
7import { initialUpdaterRun } from '../animation';
8import { useSharedValue } from './useSharedValue';
9import {
10 buildWorkletsHash,
11 isAnimated,
12 shallowEqual,
13 validateAnimatedStyles,
14} from './utils';
15import type {
16 AnimatedStyleHandle,
17 DefaultStyle,
18 DependencyList,
19 Descriptor,
20 JestAnimatedStyleHandle,
21} from './commonTypes';
22import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
23import { makeViewDescriptorsSet, useViewRefSet } from '../ViewDescriptorsSet';
24import { isJest, shouldBeUseWeb } from '../PlatformChecker';
25import type {
26 AnimationObject,
27 Timestamp,
28 NestedObjectValues,
29 SharedValue,
30 StyleProps,
31 WorkletFunction,
32 AnimatedPropsAdapterFunction,
33 AnimatedPropsAdapterWorklet,
34} from '../commonTypes';
35import type { AnimatedStyle } from '../helperTypes';
36import { isWorkletFunction } from '../commonTypes';
37
38const SHOULD_BE_USE_WEB = shouldBeUseWeb();
39
40interface AnimatedState {
41 last: AnimatedStyle<any>;
42 animations: AnimatedStyle<any>;
43 isAnimationRunning: boolean;
44 isAnimationCancelled: boolean;
45}
46
47interface AnimatedUpdaterData {
48 initial: {
49 value: AnimatedStyle<any>;
50 updater: () => AnimatedStyle<any>;
51 };
52 remoteState: AnimatedState;
53 viewDescriptors: ViewDescriptorsSet;
54}
55
56function prepareAnimation(
57 frameTimestamp: number,
58 animatedProp: AnimatedStyle<any>,
59 lastAnimation: AnimatedStyle<any>,
60 lastValue: AnimatedStyle<any>
61): void {
62 'worklet';
63 if (Array.isArray(animatedProp)) {
64 animatedProp.forEach((prop, index) => {
65 prepareAnimation(
66 frameTimestamp,
67 prop,
68 lastAnimation && lastAnimation[index],
69 lastValue && lastValue[index]
70 );
71 });
72 // return animatedProp;
73 }
74 if (typeof animatedProp === 'object' && animatedProp.onFrame) {
75 const animation = animatedProp;
76
77 let value = animation.current;
78 if (lastValue !== undefined && lastValue !== null) {
79 if (typeof lastValue === 'object') {
80 if (lastValue.value !== undefined) {
81 // previously it was a shared value
82 value = lastValue.value;
83 } else if (lastValue.onFrame !== undefined) {
84 if (lastAnimation?.current !== undefined) {
85 // it was an animation before, copy its state
86 value = lastAnimation.current;
87 } else if (lastValue?.current !== undefined) {
88 // it was initialized
89 value = lastValue.current;
90 }
91 }
92 } else {
93 // previously it was a plain value, just set it as starting point
94 value = lastValue;
95 }
96 }
97
98 animation.callStart = (timestamp: Timestamp) => {
99 animation.onStart(animation, value, timestamp, lastAnimation);
100 };
101 animation.callStart(frameTimestamp);
102 animation.callStart = null;
103 } else if (typeof animatedProp === 'object') {
104 // it is an object
105 Object.keys(animatedProp).forEach((key) =>
106 prepareAnimation(
107 frameTimestamp,
108 animatedProp[key],
109 lastAnimation && lastAnimation[key],
110 lastValue && lastValue[key]
111 )
112 );
113 }
114}
115
116function runAnimations(
117 animation: AnimatedStyle<any>,
118 timestamp: Timestamp,
119 key: number | string,
120 result: AnimatedStyle<any>,
121 animationsActive: SharedValue<boolean>
122): boolean {
123 'worklet';
124 if (!animationsActive.value) {
125 return true;
126 }
127 if (Array.isArray(animation)) {
128 result[key] = [];
129 let allFinished = true;
130 animation.forEach((entry, index) => {
131 if (
132 !runAnimations(entry, timestamp, index, result[key], animationsActive)
133 ) {
134 allFinished = false;
135 }
136 });
137 return allFinished;
138 } else if (typeof animation === 'object' && animation.onFrame) {
139 let finished = true;
140 if (!animation.finished) {
141 if (animation.callStart) {
142 animation.callStart(timestamp);
143 animation.callStart = null;
144 }
145 finished = animation.onFrame(animation, timestamp);
146 animation.timestamp = timestamp;
147 if (finished) {
148 animation.finished = true;
149 animation.callback && animation.callback(true /* finished */);
150 }
151 }
152 result[key] = animation.current;
153 return finished;
154 } else if (typeof animation === 'object') {
155 result[key] = {};
156 let allFinished = true;
157 Object.keys(animation).forEach((k) => {
158 if (
159 !runAnimations(
160 animation[k],
161 timestamp,
162 k,
163 result[key],
164 animationsActive
165 )
166 ) {
167 allFinished = false;
168 }
169 });
170 return allFinished;
171 } else {
172 result[key] = animation;
173 return true;
174 }
175}
176
177function styleUpdater(
178 viewDescriptors: SharedValue<Descriptor[]>,
179 updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
180 state: AnimatedState,
181 maybeViewRef: ViewRefSet<any> | undefined,
182 animationsActive: SharedValue<boolean>,
183 isAnimatedProps = false
184): void {
185 'worklet';
186 const animations = state.animations ?? {};
187 const newValues = updater() ?? {};
188 const oldValues = state.last;
189 const nonAnimatedNewValues: StyleProps = {};
190
191 let hasAnimations = false;
192 let frameTimestamp: number | undefined;
193 let hasNonAnimatedValues = false;
194 for (const key in newValues) {
195 const value = newValues[key];
196 if (isAnimated(value)) {
197 frameTimestamp =
198 global.__frameTimestamp || global._getAnimationTimestamp();
199 prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]);
200 animations[key] = value;
201 hasAnimations = true;
202 } else {
203 hasNonAnimatedValues = true;
204 nonAnimatedNewValues[key] = value;
205 delete animations[key];
206 }
207 }
208
209 if (hasAnimations) {
210 const frame = (timestamp: Timestamp) => {
211 // eslint-disable-next-line @typescript-eslint/no-shadow
212 const { animations, last, isAnimationCancelled } = state;
213 if (isAnimationCancelled) {
214 state.isAnimationRunning = false;
215 return;
216 }
217
218 const updates: AnimatedStyle<any> = {};
219 let allFinished = true;
220 for (const propName in animations) {
221 const finished = runAnimations(
222 animations[propName],
223 timestamp,
224 propName,
225 updates,
226 animationsActive
227 );
228 if (finished) {
229 last[propName] = updates[propName];
230 delete animations[propName];
231 } else {
232 allFinished = false;
233 }
234 }
235
236 if (updates) {
237 updateProps(viewDescriptors, updates, maybeViewRef);
238 }
239
240 if (!allFinished) {
241 requestAnimationFrame(frame);
242 } else {
243 state.isAnimationRunning = false;
244 }
245 };
246
247 state.animations = animations;
248 if (!state.isAnimationRunning) {
249 state.isAnimationCancelled = false;
250 state.isAnimationRunning = true;
251 frame(frameTimestamp!);
252 }
253
254 if (hasNonAnimatedValues) {
255 updateProps(viewDescriptors, nonAnimatedNewValues, maybeViewRef);
256 }
257 } else {
258 state.isAnimationCancelled = true;
259 state.animations = [];
260
261 if (!shallowEqual(oldValues, newValues)) {
262 updateProps(viewDescriptors, newValues, maybeViewRef, isAnimatedProps);
263 }
264 }
265 state.last = newValues;
266}
267
268function jestStyleUpdater(
269 viewDescriptors: SharedValue<Descriptor[]>,
270 updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
271 state: AnimatedState,
272 maybeViewRef: ViewRefSet<any> | undefined,
273 animationsActive: SharedValue<boolean>,
274 animatedStyle: MutableRefObject<AnimatedStyle<any>>,
275 adapters: AnimatedPropsAdapterFunction[]
276): void {
277 'worklet';
278 const animations: AnimatedStyle<any> = state.animations ?? {};
279 const newValues = updater() ?? {};
280 const oldValues = state.last;
281
282 // extract animated props
283 let hasAnimations = false;
284 let frameTimestamp: number | undefined;
285 Object.keys(animations).forEach((key) => {
286 const value = newValues[key];
287 if (!isAnimated(value)) {
288 delete animations[key];
289 }
290 });
291 Object.keys(newValues).forEach((key) => {
292 const value = newValues[key];
293 if (isAnimated(value)) {
294 frameTimestamp =
295 global.__frameTimestamp || global._getAnimationTimestamp();
296 prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]);
297 animations[key] = value;
298 hasAnimations = true;
299 }
300 });
301
302 function frame(timestamp: Timestamp) {
303 // eslint-disable-next-line @typescript-eslint/no-shadow
304 const { animations, last, isAnimationCancelled } = state;
305 if (isAnimationCancelled) {
306 state.isAnimationRunning = false;
307 return;
308 }
309
310 const updates: AnimatedStyle<any> = {};
311 let allFinished = true;
312 Object.keys(animations).forEach((propName) => {
313 const finished = runAnimations(
314 animations[propName],
315 timestamp,
316 propName,
317 updates,
318 animationsActive
319 );
320 if (finished) {
321 last[propName] = updates[propName];
322 delete animations[propName];
323 } else {
324 allFinished = false;
325 }
326 });
327
328 if (Object.keys(updates).length) {
329 updatePropsJestWrapper(
330 viewDescriptors,
331 updates,
332 maybeViewRef,
333 animatedStyle,
334 adapters
335 );
336 }
337
338 if (!allFinished) {
339 requestAnimationFrame(frame);
340 } else {
341 state.isAnimationRunning = false;
342 }
343 }
344
345 if (hasAnimations) {
346 state.animations = animations;
347 if (!state.isAnimationRunning) {
348 state.isAnimationCancelled = false;
349 state.isAnimationRunning = true;
350 frame(frameTimestamp!);
351 }
352 } else {
353 state.isAnimationCancelled = true;
354 state.animations = [];
355 }
356
357 // calculate diff
358 state.last = newValues;
359
360 if (!shallowEqual(oldValues, newValues)) {
361 updatePropsJestWrapper(
362 viewDescriptors,
363 newValues,
364 maybeViewRef,
365 animatedStyle,
366 adapters
367 );
368 }
369}
370
371// check for invalid usage of shared values in returned object
372function checkSharedValueUsage(
373 prop: NestedObjectValues<AnimationObject>,
374 currentKey?: string
375): void {
376 if (Array.isArray(prop)) {
377 // if it's an array (i.ex. transform) validate all its elements
378 for (const element of prop) {
379 checkSharedValueUsage(element, currentKey);
380 }
381 } else if (
382 typeof prop === 'object' &&
383 prop !== null &&
384 prop.value === undefined
385 ) {
386 // if it's a nested object, run validation for all its props
387 for (const key of Object.keys(prop)) {
388 checkSharedValueUsage(prop[key], key);
389 }
390 } else if (
391 currentKey !== undefined &&
392 typeof prop === 'object' &&
393 prop !== null &&
394 prop.value !== undefined
395 ) {
396 // if shared value is passed insted of its value, throw an error
397 throw new Error(
398 `[Reanimated] Invalid value passed to \`${currentKey}\`, maybe you forgot to use \`.value\`?`
399 );
400 }
401}
402
403/**
404 * Lets you create a styles object, similar to StyleSheet styles, which can be animated using shared values.
405 *
406 * @param updater - A function returning an object with style properties you want to animate.
407 * @param dependencies - An optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
408 * @returns An animated style object which has to be passed to the `style` property of an Animated component you want to animate.
409 * @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedStyle
410 */
411// You cannot pass Shared Values to `useAnimatedStyle` directly.
412// @ts-expect-error This overload is required by our API.
413export function useAnimatedStyle<Style extends DefaultStyle>(
414 updater: () => Style,
415 dependencies?: DependencyList | null
416): Style;
417
418export function useAnimatedStyle<Style extends DefaultStyle>(
419 updater:
420 | WorkletFunction<[], Style>
421 | ((() => Style) & Record<string, unknown>),
422 dependencies?: DependencyList | null,
423 adapters?: AnimatedPropsAdapterWorklet | AnimatedPropsAdapterWorklet[] | null,
424 isAnimatedProps = false
425): AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> {
426 const viewsRef: ViewRefSet<unknown> | undefined = useViewRefSet();
427 const animatedUpdaterData = useRef<AnimatedUpdaterData>();
428 let inputs = Object.values(updater.__closure ?? {});
429 if (SHOULD_BE_USE_WEB) {
430 if (!inputs.length && dependencies?.length) {
431 // let web work without a Babel plugin
432 inputs = dependencies;
433 }
434 if (
435 __DEV__ &&
436 !inputs.length &&
437 !dependencies &&
438 !isWorkletFunction(updater)
439 ) {
440 throw new Error(
441 `[Reanimated] \`useAnimatedStyle\` was used without a dependency array or Babel plugin. Please explicitly pass a dependency array, or enable the Babel plugin.
442For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/docs/guides/web-support#web-without-the-babel-plugin\`.`
443 );
444 }
445 }
446 const adaptersArray = adapters
447 ? Array.isArray(adapters)
448 ? adapters
449 : [adapters]
450 : [];
451 const adaptersHash = adapters ? buildWorkletsHash(adaptersArray) : null;
452 const areAnimationsActive = useSharedValue<boolean>(true);
453 const jestAnimatedStyle = useRef<Style>({} as Style);
454
455 // build dependencies
456 if (!dependencies) {
457 dependencies = [...inputs, updater.__workletHash];
458 } else {
459 dependencies.push(updater.__workletHash);
460 }
461 adaptersHash && dependencies.push(adaptersHash);
462
463 if (!animatedUpdaterData.current) {
464 const initialStyle = initialUpdaterRun(updater);
465 if (__DEV__) {
466 validateAnimatedStyles(initialStyle);
467 }
468 animatedUpdaterData.current = {
469 initial: {
470 value: initialStyle,
471 updater,
472 },
473 remoteState: makeShareable({
474 last: initialStyle,
475 animations: {},
476 isAnimationCancelled: false,
477 isAnimationRunning: false,
478 }),
479 viewDescriptors: makeViewDescriptorsSet(),
480 };
481 }
482
483 const { initial, remoteState, viewDescriptors } = animatedUpdaterData.current;
484 const shareableViewDescriptors = viewDescriptors.shareableViewDescriptors;
485
486 dependencies.push(shareableViewDescriptors);
487
488 useEffect(() => {
489 let fun;
490 let updaterFn = updater;
491 if (adapters) {
492 updaterFn = (() => {
493 'worklet';
494 const newValues = updater();
495 adaptersArray.forEach((adapter) => {
496 adapter(newValues as Record<string, unknown>);
497 });
498 return newValues;
499 }) as WorkletFunction<[], Style>;
500 }
501
502 if (isJest()) {
503 fun = () => {
504 'worklet';
505 jestStyleUpdater(
506 shareableViewDescriptors,
507 updater,
508 remoteState,
509 viewsRef,
510 areAnimationsActive,
511 jestAnimatedStyle,
512 adaptersArray
513 );
514 };
515 } else {
516 fun = () => {
517 'worklet';
518 styleUpdater(
519 shareableViewDescriptors,
520 updaterFn,
521 remoteState,
522 viewsRef,
523 areAnimationsActive,
524 isAnimatedProps
525 );
526 };
527 }
528 const mapperId = startMapper(fun, inputs);
529 return () => {
530 stopMapper(mapperId);
531 };
532 // eslint-disable-next-line react-hooks/exhaustive-deps
533 }, dependencies);
534
535 useEffect(() => {
536 areAnimationsActive.value = true;
537 return () => {
538 areAnimationsActive.value = false;
539 };
540 }, [areAnimationsActive]);
541
542 checkSharedValueUsage(initial.value);
543
544 const animatedStyleHandle = useRef<
545 AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> | null
546 >(null);
547
548 if (!animatedStyleHandle.current) {
549 animatedStyleHandle.current = isJest()
550 ? { viewDescriptors, initial, viewsRef, jestAnimatedStyle }
551 : { initial, viewsRef, viewDescriptors };
552 }
553
554 return animatedStyleHandle.current;
555}