1 | 'use strict';
|
2 | import type { MutableRefObject } from 'react';
|
3 | import { useEffect, useRef } from 'react';
|
4 |
|
5 | import { makeShareable, startMapper, stopMapper } from '../core';
|
6 | import updateProps, { updatePropsJestWrapper } from '../UpdateProps';
|
7 | import { initialUpdaterRun } from '../animation';
|
8 | import { useSharedValue } from './useSharedValue';
|
9 | import {
|
10 | buildWorkletsHash,
|
11 | isAnimated,
|
12 | shallowEqual,
|
13 | validateAnimatedStyles,
|
14 | } from './utils';
|
15 | import type {
|
16 | AnimatedStyleHandle,
|
17 | DefaultStyle,
|
18 | DependencyList,
|
19 | Descriptor,
|
20 | JestAnimatedStyleHandle,
|
21 | } from './commonTypes';
|
22 | import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
|
23 | import { makeViewDescriptorsSet, useViewRefSet } from '../ViewDescriptorsSet';
|
24 | import { isJest, shouldBeUseWeb } from '../PlatformChecker';
|
25 | import type {
|
26 | AnimationObject,
|
27 | Timestamp,
|
28 | NestedObjectValues,
|
29 | SharedValue,
|
30 | StyleProps,
|
31 | WorkletFunction,
|
32 | AnimatedPropsAdapterFunction,
|
33 | AnimatedPropsAdapterWorklet,
|
34 | } from '../commonTypes';
|
35 | import type { AnimatedStyle } from '../helperTypes';
|
36 | import { isWorkletFunction } from '../commonTypes';
|
37 |
|
38 | const SHOULD_BE_USE_WEB = shouldBeUseWeb();
|
39 |
|
40 | interface AnimatedState {
|
41 | last: AnimatedStyle<any>;
|
42 | animations: AnimatedStyle<any>;
|
43 | isAnimationRunning: boolean;
|
44 | isAnimationCancelled: boolean;
|
45 | }
|
46 |
|
47 | interface AnimatedUpdaterData {
|
48 | initial: {
|
49 | value: AnimatedStyle<any>;
|
50 | updater: () => AnimatedStyle<any>;
|
51 | };
|
52 | remoteState: AnimatedState;
|
53 | viewDescriptors: ViewDescriptorsSet;
|
54 | }
|
55 |
|
56 | function 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 |
|
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 |
|
82 | value = lastValue.value;
|
83 | } else if (lastValue.onFrame !== undefined) {
|
84 | if (lastAnimation?.current !== undefined) {
|
85 |
|
86 | value = lastAnimation.current;
|
87 | } else if (lastValue?.current !== undefined) {
|
88 |
|
89 | value = lastValue.current;
|
90 | }
|
91 | }
|
92 | } else {
|
93 |
|
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 |
|
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 |
|
116 | function 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 );
|
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 |
|
177 | function 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 |
|
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 |
|
268 | function 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 |
|
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 |
|
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 |
|
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 |
|
372 | function checkSharedValueUsage(
|
373 | prop: NestedObjectValues<AnimationObject>,
|
374 | currentKey?: string
|
375 | ): void {
|
376 | if (Array.isArray(prop)) {
|
377 |
|
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 |
|
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 |
|
397 | throw new Error(
|
398 | `[Reanimated] Invalid value passed to \`${currentKey}\`, maybe you forgot to use \`.value\`?`
|
399 | );
|
400 | }
|
401 | }
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 | export function useAnimatedStyle<Style extends DefaultStyle>(
|
414 | updater: () => Style,
|
415 | dependencies?: DependencyList | null
|
416 | ): Style;
|
417 |
|
418 | export 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 |
|
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.
|
442 | For 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 |
|
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 |
|
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 | }
|