@tamagui/animations-reanimated 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/createAnimations.cjs +361 -124
- package/dist/cjs/createAnimations.native.js +519 -191
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/createAnimations.mjs +361 -124
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +519 -191
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/package.json +12 -6
- package/src/createAnimations.tsx +596 -165
- package/types/createAnimations.d.ts.map +2 -2
package/src/createAnimations.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type UniversalAnimatedNumber,
|
|
14
14
|
} from '@tamagui/core'
|
|
15
15
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
16
|
+
import normalizeColor from '@react-native/normalize-colors'
|
|
16
17
|
import React, { forwardRef, useMemo, useRef } from 'react'
|
|
17
18
|
import type { SharedValue } from 'react-native-reanimated'
|
|
18
19
|
import Animated_, {
|
|
@@ -73,6 +74,25 @@ const silenceAnimatedComponentDevCheck = (style: unknown) => {
|
|
|
73
74
|
|
|
74
75
|
type ReanimatedAnimatedNumber = SharedValue<number>
|
|
75
76
|
|
|
77
|
+
type AnimationSnapshot = {
|
|
78
|
+
animated: Record<string, unknown>
|
|
79
|
+
statics: Record<string, unknown>
|
|
80
|
+
transforms: Array<Record<string, unknown>>
|
|
81
|
+
gatedKeys: Record<string, boolean>
|
|
82
|
+
/**
|
|
83
|
+
* for each animated key (transform sub-keys as `transform:key`), the value the
|
|
84
|
+
* previous committed render painted for it, when one exists and is animatable.
|
|
85
|
+
* the worklet animates a mapper-fresh key FROM its seed instead of snapping —
|
|
86
|
+
* this is how an enterStyle value painted statically during mount becomes the
|
|
87
|
+
* start point once the key turns animated. keys with no seed paint their target
|
|
88
|
+
* directly (a key appearing from nothing has nothing to animate from).
|
|
89
|
+
*/
|
|
90
|
+
seeds: Record<string, unknown>
|
|
91
|
+
removedKeys: Record<string, boolean>
|
|
92
|
+
removeTransform: boolean
|
|
93
|
+
clearValue: '' | null
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
/** Spring animation configuration */
|
|
77
97
|
type SpringConfig = {
|
|
78
98
|
type?: 'spring'
|
|
@@ -130,10 +150,212 @@ const cloneAnimationValue = (value: unknown): unknown => {
|
|
|
130
150
|
return value
|
|
131
151
|
}
|
|
132
152
|
|
|
153
|
+
const cloneStyleRecord = (style: Record<string, unknown>): Record<string, unknown> => {
|
|
154
|
+
const next: Record<string, unknown> = {}
|
|
155
|
+
for (const key in style) {
|
|
156
|
+
next[key] = cloneAnimationValue(style[key])
|
|
157
|
+
}
|
|
158
|
+
return next
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
const cloneTransitionConfig = (config: TransitionConfig): TransitionConfig => {
|
|
134
162
|
return cloneAnimationValue(config) as TransitionConfig
|
|
135
163
|
}
|
|
136
164
|
|
|
165
|
+
const getAnimatedTransforms = (value: unknown): Array<Record<string, unknown>> => {
|
|
166
|
+
if (!Array.isArray(value)) return []
|
|
167
|
+
return value.filter(
|
|
168
|
+
(item): item is Record<string, unknown> =>
|
|
169
|
+
item !== null && typeof item === 'object' && !Array.isArray(item)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const getRemovedAnimatedKeys = (
|
|
174
|
+
current: Set<string>,
|
|
175
|
+
previous: Set<string>
|
|
176
|
+
): Record<string, boolean> => {
|
|
177
|
+
const removedKeys: Record<string, boolean> = {}
|
|
178
|
+
for (const key of previous) {
|
|
179
|
+
if (!current.has(key)) removedKeys[key] = true
|
|
180
|
+
}
|
|
181
|
+
return removedKeys
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// implicit start values for style keys that were never painted (e.g. a key
|
|
185
|
+
// introduced by exitStyle with no base value). most numerics default to 0.
|
|
186
|
+
const getImplicitDefault = (
|
|
187
|
+
key: string,
|
|
188
|
+
targetValue: number | string
|
|
189
|
+
): number | string => {
|
|
190
|
+
'worklet'
|
|
191
|
+
const value =
|
|
192
|
+
key === 'opacity' || key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
|
|
193
|
+
if (typeof targetValue !== 'string') return value
|
|
194
|
+
|
|
195
|
+
// reanimated derives a string animation's suffix from its start value. keep
|
|
196
|
+
// units on implicit starts so fresh `50%` and `90deg` keys interpolate.
|
|
197
|
+
let suffixIndex = 0
|
|
198
|
+
while (suffixIndex < targetValue.length) {
|
|
199
|
+
const character = targetValue[suffixIndex]
|
|
200
|
+
const code = character.charCodeAt(0)
|
|
201
|
+
if (
|
|
202
|
+
(code >= 48 && code <= 57) ||
|
|
203
|
+
character === '.' ||
|
|
204
|
+
character === '-' ||
|
|
205
|
+
character === '+'
|
|
206
|
+
) {
|
|
207
|
+
suffixIndex++
|
|
208
|
+
} else {
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const COLOR_STYLE_KEYS: Record<string, boolean> = {
|
|
216
|
+
backgroundColor: true,
|
|
217
|
+
borderColor: true,
|
|
218
|
+
borderLeftColor: true,
|
|
219
|
+
borderRightColor: true,
|
|
220
|
+
borderTopColor: true,
|
|
221
|
+
borderBottomColor: true,
|
|
222
|
+
color: true,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// reanimated can pass either its rgba output or a processed numeric color back
|
|
226
|
+
// as animation history. snapshot targets are normalized on the JS side below;
|
|
227
|
+
// this worklet conversion exists only to validate that in-flight history.
|
|
228
|
+
const toReanimatedColor = (value: unknown): unknown => {
|
|
229
|
+
'worklet'
|
|
230
|
+
if (typeof value === 'number') {
|
|
231
|
+
const color = value >>> 0
|
|
232
|
+
const alpha = ((color >>> 24) & 255) / 255
|
|
233
|
+
const red = (color >>> 16) & 255
|
|
234
|
+
const green = (color >>> 8) & 255
|
|
235
|
+
const blue = color & 255
|
|
236
|
+
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
|
237
|
+
}
|
|
238
|
+
if (typeof value !== 'string') return value
|
|
239
|
+
const trimmed = value.trim()
|
|
240
|
+
if (trimmed === 'transparent') return 'rgba(0, 0, 0, 0)'
|
|
241
|
+
return trimmed
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const isReanimatedColorHistory = (value: unknown): boolean => {
|
|
245
|
+
'worklet'
|
|
246
|
+
const normalized = toReanimatedColor(value)
|
|
247
|
+
return typeof normalized === 'string' && normalized.startsWith('rgba(')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const normalizeAnimationColor = (value: unknown): string | undefined => {
|
|
251
|
+
if (typeof value === 'number') {
|
|
252
|
+
return toReanimatedColor(value) as string
|
|
253
|
+
}
|
|
254
|
+
if (typeof value !== 'string') return
|
|
255
|
+
|
|
256
|
+
const color = normalizeColor(value)
|
|
257
|
+
if (color == null) return
|
|
258
|
+
const red = Math.round((color & 0xff000000) >>> 24)
|
|
259
|
+
const green = Math.round((color & 0x00ff0000) >>> 16)
|
|
260
|
+
const blue = Math.round((color & 0x0000ff00) >>> 8)
|
|
261
|
+
const alpha = (color & 0x000000ff) / 255
|
|
262
|
+
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const buildSnapshot = (
|
|
266
|
+
animated: Record<string, unknown>,
|
|
267
|
+
statics: Record<string, unknown>,
|
|
268
|
+
transforms: Array<Record<string, unknown>>,
|
|
269
|
+
previousKeys: Set<string>,
|
|
270
|
+
lastPainted: Record<string, unknown>
|
|
271
|
+
) => {
|
|
272
|
+
const snapshotAnimated: Record<string, unknown> = {}
|
|
273
|
+
const snapshotStatics = cloneStyleRecord(statics)
|
|
274
|
+
|
|
275
|
+
for (const key in snapshotStatics) {
|
|
276
|
+
if (!COLOR_STYLE_KEYS[key]) continue
|
|
277
|
+
const normalized = normalizeAnimationColor(snapshotStatics[key])
|
|
278
|
+
if (normalized !== undefined) snapshotStatics[key] = normalized
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const key in animated) {
|
|
282
|
+
if (key === 'transform') continue
|
|
283
|
+
const value = animated[key]
|
|
284
|
+
if (COLOR_STYLE_KEYS[key]) {
|
|
285
|
+
const normalized = normalizeAnimationColor(value)
|
|
286
|
+
if (normalized === undefined) {
|
|
287
|
+
// invalid colors must paint plainly. creating a descriptor makes
|
|
288
|
+
// reanimated's string interpolator emit '<letters>NaN'.
|
|
289
|
+
snapshotStatics[key] = cloneAnimationValue(value)
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
snapshotAnimated[key] = normalized
|
|
293
|
+
} else {
|
|
294
|
+
snapshotAnimated[key] = cloneAnimationValue(value)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const snapshotTransforms = transforms.map(cloneStyleRecord)
|
|
299
|
+
const keys = new Set(Object.keys(snapshotAnimated))
|
|
300
|
+
for (const transform of snapshotTransforms) {
|
|
301
|
+
const key = Object.keys(transform)[0]
|
|
302
|
+
const value = key ? transform[key] : undefined
|
|
303
|
+
if (key && (typeof value === 'number' || typeof value === 'string')) {
|
|
304
|
+
keys.add(`transform:${key}`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const seeds: Record<string, unknown> = {}
|
|
309
|
+
for (const key of keys) {
|
|
310
|
+
let value = lastPainted[key]
|
|
311
|
+
if (typeof value !== 'number' && typeof value !== 'string') continue
|
|
312
|
+
if (value === 'auto' || (typeof value === 'string' && value.startsWith('calc'))) {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value)
|
|
316
|
+
if (value !== undefined) seeds[key] = value
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const gatedKeys: Record<string, boolean> = {}
|
|
320
|
+
for (const key of keys) gatedKeys[key] = true
|
|
321
|
+
|
|
322
|
+
const removedKeys = getRemovedAnimatedKeys(keys, previousKeys)
|
|
323
|
+
const painted: Record<string, unknown> = {
|
|
324
|
+
...snapshotStatics,
|
|
325
|
+
...snapshotAnimated,
|
|
326
|
+
}
|
|
327
|
+
const staticTransforms = getAnimatedTransforms(snapshotStatics.transform)
|
|
328
|
+
delete painted.transform
|
|
329
|
+
for (const transform of staticTransforms) {
|
|
330
|
+
const key = Object.keys(transform)[0]
|
|
331
|
+
if (key) painted[`transform:${key}`] = transform[key]
|
|
332
|
+
}
|
|
333
|
+
for (const transform of snapshotTransforms) {
|
|
334
|
+
const key = Object.keys(transform)[0]
|
|
335
|
+
if (key) painted[`transform:${key}`] = transform[key]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
value: {
|
|
340
|
+
animated: snapshotAnimated,
|
|
341
|
+
statics: snapshotStatics,
|
|
342
|
+
transforms: snapshotTransforms,
|
|
343
|
+
gatedKeys,
|
|
344
|
+
seeds,
|
|
345
|
+
removedKeys,
|
|
346
|
+
removeTransform: Object.keys(removedKeys).some((key) =>
|
|
347
|
+
key.startsWith('transform:')
|
|
348
|
+
),
|
|
349
|
+
clearValue: isWeb ? '' : null,
|
|
350
|
+
},
|
|
351
|
+
painted,
|
|
352
|
+
keys,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const getGatedKeys = (snapshot: AnimationSnapshot): string[] =>
|
|
357
|
+
Object.keys(snapshot.gatedKeys)
|
|
358
|
+
|
|
137
359
|
const createReanimatedConfig = (config: TransitionConfig): Record<string, unknown> => {
|
|
138
360
|
'worklet'
|
|
139
361
|
const next: Record<string, unknown> = {}
|
|
@@ -156,7 +378,9 @@ const createReanimatedConfig = (config: TransitionConfig): Record<string, unknow
|
|
|
156
378
|
const applyAnimation = (
|
|
157
379
|
targetValue: number | string,
|
|
158
380
|
config: TransitionConfig,
|
|
159
|
-
callback?: AnimationCallback
|
|
381
|
+
callback?: AnimationCallback,
|
|
382
|
+
seedValue?: number | string,
|
|
383
|
+
validateStartAsColor = false
|
|
160
384
|
): number | string => {
|
|
161
385
|
'worklet'
|
|
162
386
|
const delay = config.delay
|
|
@@ -177,6 +401,32 @@ const applyAnimation = (
|
|
|
177
401
|
)
|
|
178
402
|
}
|
|
179
403
|
|
|
404
|
+
// reanimated starts a descriptor from its per-view history for the key — the
|
|
405
|
+
// key's last output value, or the descriptor's own toValue when there is
|
|
406
|
+
// none (an instant snap). both are wrong for a key that turns animated with
|
|
407
|
+
// a painted predecessor: history may hold a stale clear-value ('') from when
|
|
408
|
+
// the key was last removed, which poisons the start value (NaN frames). the
|
|
409
|
+
// wrapper substitutes the seed for whatever start value reanimated passes.
|
|
410
|
+
// color history gets the same treatment only when reanimated's own parser
|
|
411
|
+
// rejects it, preserving valid in-flight values during interruption.
|
|
412
|
+
if (seedValue !== undefined || validateStartAsColor) {
|
|
413
|
+
const innerOnStart = animatedValue.onStart
|
|
414
|
+
animatedValue.onStart = (
|
|
415
|
+
animation: unknown,
|
|
416
|
+
value: unknown,
|
|
417
|
+
timestamp: number,
|
|
418
|
+
previousAnimation: unknown
|
|
419
|
+
) => {
|
|
420
|
+
'worklet'
|
|
421
|
+
const startValue = validateStartAsColor
|
|
422
|
+
? isReanimatedColorHistory(value)
|
|
423
|
+
? toReanimatedColor(value)
|
|
424
|
+
: (seedValue ?? targetValue)
|
|
425
|
+
: seedValue
|
|
426
|
+
innerOnStart(animation, startValue, timestamp, previousAnimation)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
180
430
|
if (delay && delay > 0) {
|
|
181
431
|
animatedValue = withDelay(delay, animatedValue)
|
|
182
432
|
}
|
|
@@ -184,6 +434,53 @@ const applyAnimation = (
|
|
|
184
434
|
return animatedValue
|
|
185
435
|
}
|
|
186
436
|
|
|
437
|
+
const animateSnapshotValue = (
|
|
438
|
+
key: string,
|
|
439
|
+
implicitKey: string,
|
|
440
|
+
targetValue: number | string,
|
|
441
|
+
config: TransitionConfig,
|
|
442
|
+
previouslyEmitted: boolean,
|
|
443
|
+
seedValue: unknown,
|
|
444
|
+
gated: boolean,
|
|
445
|
+
currentlyExiting: boolean,
|
|
446
|
+
exitCycleId: number,
|
|
447
|
+
currentlyCompletingAnimation: boolean,
|
|
448
|
+
didAnimateCycleId: number,
|
|
449
|
+
markExitKeyDone: (key: string, cycleId: number, finished: boolean) => void,
|
|
450
|
+
markDidAnimateKeyDone: (key: string, cycleId: number, finished: boolean) => void,
|
|
451
|
+
validateStartAsColor = false
|
|
452
|
+
): number | string => {
|
|
453
|
+
'worklet'
|
|
454
|
+
|
|
455
|
+
const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation)
|
|
456
|
+
if (!previouslyEmitted && seedValue === undefined && !cycleGated) {
|
|
457
|
+
return targetValue
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let callback: AnimationCallback | undefined
|
|
461
|
+
if (gated && currentlyExiting) {
|
|
462
|
+
callback = (finished) => {
|
|
463
|
+
'worklet'
|
|
464
|
+
runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false)
|
|
465
|
+
}
|
|
466
|
+
} else if (gated && currentlyCompletingAnimation) {
|
|
467
|
+
callback = (finished) => {
|
|
468
|
+
'worklet'
|
|
469
|
+
runOnJS(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return applyAnimation(
|
|
474
|
+
targetValue,
|
|
475
|
+
config,
|
|
476
|
+
callback,
|
|
477
|
+
previouslyEmitted
|
|
478
|
+
? undefined
|
|
479
|
+
: ((seedValue ?? getImplicitDefault(implicitKey, targetValue)) as number | string),
|
|
480
|
+
validateStartAsColor
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
187
484
|
// =============================================================================
|
|
188
485
|
// Animatable Properties
|
|
189
486
|
// =============================================================================
|
|
@@ -271,6 +568,29 @@ const canAnimateProperty = (
|
|
|
271
568
|
return true
|
|
272
569
|
}
|
|
273
570
|
|
|
571
|
+
const splitAnimationStyles = (
|
|
572
|
+
style: Record<string, unknown>,
|
|
573
|
+
isDark: boolean,
|
|
574
|
+
disableAnimation: boolean,
|
|
575
|
+
animateOnly?: string[]
|
|
576
|
+
) => {
|
|
577
|
+
const animated: Record<string, unknown> = {}
|
|
578
|
+
const statics: Record<string, unknown> = {}
|
|
579
|
+
|
|
580
|
+
for (const key in style) {
|
|
581
|
+
const value = resolveDynamicValue(style[key], isDark)
|
|
582
|
+
if (value === undefined) continue
|
|
583
|
+
|
|
584
|
+
if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
|
|
585
|
+
animated[key] = cloneAnimationValue(value)
|
|
586
|
+
} else {
|
|
587
|
+
statics[key] = cloneAnimationValue(value)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { animated, statics }
|
|
592
|
+
}
|
|
593
|
+
|
|
274
594
|
// =============================================================================
|
|
275
595
|
// Animated Components (Web)
|
|
276
596
|
// =============================================================================
|
|
@@ -594,6 +914,7 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
594
914
|
themeName,
|
|
595
915
|
stateRef,
|
|
596
916
|
styleState,
|
|
917
|
+
onDidAnimate,
|
|
597
918
|
} = animationProps
|
|
598
919
|
|
|
599
920
|
// State flags
|
|
@@ -629,6 +950,28 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
629
950
|
|
|
630
951
|
// Get sendExitComplete callback from presence
|
|
631
952
|
const sendExitComplete = presence?.[1]
|
|
953
|
+
const onDidAnimateRef = useRef(onDidAnimate)
|
|
954
|
+
useIsomorphicLayoutEffect(() => {
|
|
955
|
+
onDidAnimateRef.current = onDidAnimate
|
|
956
|
+
}, [onDidAnimate])
|
|
957
|
+
|
|
958
|
+
const didAnimateCycleIdRef = useRef(0)
|
|
959
|
+
const pendingDidAnimateKeysRef = useRef<Set<string>>(new Set())
|
|
960
|
+
const didAnimateCompletedRef = useRef(false)
|
|
961
|
+
const isCompletingAnimationRef = useSharedValue(false)
|
|
962
|
+
const didAnimateCycleIdShared = useSharedValue(0)
|
|
963
|
+
const markDidAnimateKeyDone = useEvent(
|
|
964
|
+
(key: string, cycleId: number, finished: boolean) => {
|
|
965
|
+
if (!finished || cycleId !== didAnimateCycleIdRef.current) return
|
|
966
|
+
if (didAnimateCompletedRef.current) return
|
|
967
|
+
pendingDidAnimateKeysRef.current.delete(key)
|
|
968
|
+
if (pendingDidAnimateKeysRef.current.size === 0) {
|
|
969
|
+
didAnimateCompletedRef.current = true
|
|
970
|
+
isCompletingAnimationRef.value = false
|
|
971
|
+
onDidAnimateRef.current?.()
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
)
|
|
632
975
|
|
|
633
976
|
// =========================================================================
|
|
634
977
|
// Exit cycle state for deterministic per-property completion tracking
|
|
@@ -671,6 +1014,9 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
671
1014
|
exitCycleIdRef.current++
|
|
672
1015
|
exitCompletedRef.current = false
|
|
673
1016
|
pendingExitKeysRef.current.clear()
|
|
1017
|
+
didAnimateCycleIdRef.current++
|
|
1018
|
+
pendingDidAnimateKeysRef.current.clear()
|
|
1019
|
+
didAnimateCompletedRef.current = true
|
|
674
1020
|
}
|
|
675
1021
|
// invalidate pending callbacks when exit is canceled/interrupted
|
|
676
1022
|
if (justStoppedExiting) {
|
|
@@ -682,7 +1028,11 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
682
1028
|
useIsomorphicLayoutEffect(() => {
|
|
683
1029
|
isExitingRef.value = isExiting
|
|
684
1030
|
exitCycleIdShared.value = exitCycleIdRef.current
|
|
685
|
-
|
|
1031
|
+
if (justStartedExiting) {
|
|
1032
|
+
isCompletingAnimationRef.value = false
|
|
1033
|
+
didAnimateCycleIdShared.value = didAnimateCycleIdRef.current
|
|
1034
|
+
}
|
|
1035
|
+
}, [isExiting, exitCycleIdRef.current, justStartedExiting])
|
|
686
1036
|
|
|
687
1037
|
// track previous exiting state
|
|
688
1038
|
React.useEffect(() => {
|
|
@@ -692,51 +1042,85 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
692
1042
|
// =========================================================================
|
|
693
1043
|
// avoidRerenders: SharedValues for style updates without re-renders
|
|
694
1044
|
// =========================================================================
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
)
|
|
1045
|
+
const committedRenderKeysRef = useRef(new Set<string>())
|
|
1046
|
+
// full painted style of the last committed source (render or emitter):
|
|
1047
|
+
// statics + animated targets, transform sub-keys flattened as `transform:key`.
|
|
1048
|
+
// seeds read from it.
|
|
1049
|
+
const lastPaintedRef = useRef<Record<string, unknown>>({})
|
|
1050
|
+
// per-key first-animated values React keeps painting under the mapper —
|
|
1051
|
+
// see the styles memo below
|
|
1052
|
+
const carryRef = useRef<Record<string, unknown>>({})
|
|
700
1053
|
|
|
701
1054
|
// Separate styles into animated and static
|
|
702
|
-
const { animatedStyles, staticStyles } = useMemo(() => {
|
|
703
|
-
const animated: Record<string, unknown> = {}
|
|
704
|
-
const staticStyles: Record<string, unknown> = {}
|
|
1055
|
+
const { animatedStyles, staticStyles, nextCarry } = useMemo(() => {
|
|
705
1056
|
const animateOnly = props.animateOnly as string[] | undefined
|
|
1057
|
+
const { animated, statics } = splitAnimationStyles(
|
|
1058
|
+
style as Record<string, unknown>,
|
|
1059
|
+
isDark,
|
|
1060
|
+
disableAnimation,
|
|
1061
|
+
animateOnly
|
|
1062
|
+
)
|
|
706
1063
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
staticStyles[key] = cloneAnimationValue(value)
|
|
722
|
-
}
|
|
1064
|
+
// every animated key keeps its FIRST animated value in React's style for
|
|
1065
|
+
// as long as it stays animated. the value never changes across renders,
|
|
1066
|
+
// so React's per-key style diff never touches the key again and the
|
|
1067
|
+
// mapper's inline writes survive every commit — reanimated's Fabric
|
|
1068
|
+
// commit hook provides this guarantee on native, web has no equivalent.
|
|
1069
|
+
// during enter the carry is the enterStyle (the mapper animates over it);
|
|
1070
|
+
// a key appearing post-mount (a just-measured height) paints its value in
|
|
1071
|
+
// the same commit the mapper first sees it, a frame before the mapper's
|
|
1072
|
+
// first write.
|
|
1073
|
+
// derive from committed state. mutating carryRef during render leaks
|
|
1074
|
+
// abandoned concurrent renders into later commits.
|
|
1075
|
+
const nextCarry = cloneStyleRecord(carryRef.current)
|
|
1076
|
+
for (const key in nextCarry) {
|
|
1077
|
+
if (!(key in animated)) delete nextCarry[key]
|
|
723
1078
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
for (const key in animated) {
|
|
728
|
-
staticStyles[key] = animated[key]
|
|
1079
|
+
for (const key in animated) {
|
|
1080
|
+
if (!(key in nextCarry)) {
|
|
1081
|
+
nextCarry[key] = cloneAnimationValue(animated[key])
|
|
729
1082
|
}
|
|
1083
|
+
statics[key] = nextCarry[key]
|
|
730
1084
|
}
|
|
731
1085
|
|
|
732
|
-
return { animatedStyles: animated, staticStyles }
|
|
733
|
-
}, [disableAnimation, style, isDark,
|
|
1086
|
+
return { animatedStyles: animated, staticStyles: statics, nextCarry }
|
|
1087
|
+
}, [disableAnimation, style, isDark, props.animateOnly])
|
|
1088
|
+
|
|
1089
|
+
const renderSnapshot = useMemo(
|
|
1090
|
+
() =>
|
|
1091
|
+
buildSnapshot(
|
|
1092
|
+
animatedStyles,
|
|
1093
|
+
staticStyles,
|
|
1094
|
+
getAnimatedTransforms(animatedStyles.transform),
|
|
1095
|
+
committedRenderKeysRef.current,
|
|
1096
|
+
lastPaintedRef.current
|
|
1097
|
+
),
|
|
1098
|
+
[animatedStyles, staticStyles]
|
|
1099
|
+
)
|
|
1100
|
+
const renderSnapshotRef = useSharedValue<AnimationSnapshot>({
|
|
1101
|
+
animated: {},
|
|
1102
|
+
statics: {},
|
|
1103
|
+
transforms: [],
|
|
1104
|
+
gatedKeys: {},
|
|
1105
|
+
seeds: {},
|
|
1106
|
+
removedKeys: {},
|
|
1107
|
+
removeTransform: false,
|
|
1108
|
+
clearValue: isWeb ? '' : null,
|
|
1109
|
+
})
|
|
1110
|
+
const emitterSnapshotRef = useSharedValue<AnimationSnapshot | null>(null)
|
|
1111
|
+
const emitterKeysRef = useRef<Set<string> | null>(null)
|
|
1112
|
+
|
|
1113
|
+
useIsomorphicLayoutEffect(() => {
|
|
1114
|
+
carryRef.current = nextCarry
|
|
1115
|
+
committedRenderKeysRef.current = renderSnapshot.keys
|
|
1116
|
+
lastPaintedRef.current = renderSnapshot.painted
|
|
1117
|
+
}, [nextCarry, renderSnapshot])
|
|
734
1118
|
|
|
735
1119
|
// reconcile the emitter latch with real re-renders.
|
|
736
1120
|
//
|
|
737
1121
|
// the avoidReRenders fast path lets a pure pseudo change (hover/press/focus) push
|
|
738
1122
|
// styles straight to the worklet via useStyleEmitter with no React commit — it sets
|
|
739
|
-
//
|
|
1123
|
+
// emitterSnapshotRef. the worklet then treats `emitterSnapshotRef !== null` as a
|
|
740
1124
|
// permanent latch: once the emitter has fired even once, it reads its last-emitted
|
|
741
1125
|
// snapshot and IGNORES `animatedStyles` from every subsequent re-render. so a base
|
|
742
1126
|
// style that changes through React (e.g. a row's `backgroundColor` flipping with an
|
|
@@ -763,14 +1147,39 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
763
1147
|
const pseudoActiveRef = useRef(false)
|
|
764
1148
|
const isExitingJSRef = useRef(false)
|
|
765
1149
|
isExitingJSRef.current = isExiting
|
|
1150
|
+
// the committed render is the worklet's source of truth whenever the emitter is not
|
|
1151
|
+
// latched, so every render must publish its snapshot — not just the renders that drop
|
|
1152
|
+
// a latch. animated keys live only in this snapshot after mount (staticStyles carries
|
|
1153
|
+
// them during mount only), so a render that never publishes leaves the worklet reading
|
|
1154
|
+
// an empty snapshot and the animated properties never reach the screen at all.
|
|
1155
|
+
const publishedSnapshotRef = useRef<object | null>(null)
|
|
766
1156
|
useIsomorphicLayoutEffect(() => {
|
|
767
|
-
|
|
768
|
-
(isExiting || !pseudoActiveRef.current) &&
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1157
|
+
const droppingLatch =
|
|
1158
|
+
(isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null
|
|
1159
|
+
// when the latch drops, keys the emitter owned that this render no longer has must
|
|
1160
|
+
// be cleared too, otherwise reanimated keeps painting the stale emitted value
|
|
1161
|
+
const emitterKeys = droppingLatch ? emitterKeysRef.current : null
|
|
1162
|
+
|
|
1163
|
+
if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
|
|
1164
|
+
const removedKeys = emitterKeys
|
|
1165
|
+
? {
|
|
1166
|
+
...renderSnapshot.value.removedKeys,
|
|
1167
|
+
...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys),
|
|
1168
|
+
}
|
|
1169
|
+
: renderSnapshot.value.removedKeys
|
|
1170
|
+
renderSnapshotRef.value = {
|
|
1171
|
+
...renderSnapshot.value,
|
|
1172
|
+
removedKeys,
|
|
1173
|
+
removeTransform: Object.keys(removedKeys).some((key) =>
|
|
1174
|
+
key.startsWith('transform:')
|
|
1175
|
+
),
|
|
1176
|
+
}
|
|
1177
|
+
publishedSnapshotRef.current = renderSnapshot.value
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (droppingLatch) {
|
|
1181
|
+
emitterSnapshotRef.value = null
|
|
1182
|
+
emitterKeysRef.current = null
|
|
774
1183
|
}
|
|
775
1184
|
})
|
|
776
1185
|
|
|
@@ -828,9 +1237,6 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
828
1237
|
// this emitter snapshot is a transient override to keep latched or a base it can drop.
|
|
829
1238
|
pseudoActiveRef.current = pseudoActive === true
|
|
830
1239
|
const animateOnly = props.animateOnly as string[] | undefined
|
|
831
|
-
const animated: Record<string, unknown> = {}
|
|
832
|
-
const statics: Record<string, unknown> = {}
|
|
833
|
-
const transforms: Array<Record<string, unknown>> = []
|
|
834
1240
|
|
|
835
1241
|
// effectiveTransition is computed in createComponent based on entering/exiting pseudo states
|
|
836
1242
|
// rebuild config whenever transition changes (entering OR exiting pseudo states)
|
|
@@ -851,41 +1257,23 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
851
1257
|
isHydrating: configRef.value.isHydrating,
|
|
852
1258
|
}
|
|
853
1259
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
transforms.push(cloneAnimationValue(t) as Record<string, unknown>)
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
continue
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (canAnimateProperty(key, value, animateOnly)) {
|
|
879
|
-
animated[key] = cloneAnimationValue(value)
|
|
880
|
-
} else {
|
|
881
|
-
statics[key] = cloneAnimationValue(value)
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// update shared values - on web, the mapper watches these if passed in dependencies
|
|
886
|
-
animatedTargetsRef.value = animated
|
|
887
|
-
staticTargetsRef.value = statics
|
|
888
|
-
transformTargetsRef.value = transforms
|
|
1260
|
+
const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current
|
|
1261
|
+
const { animated, statics } = splitAnimationStyles(
|
|
1262
|
+
nextStyle,
|
|
1263
|
+
isDark,
|
|
1264
|
+
configRef.value.disableAnimation,
|
|
1265
|
+
animateOnly
|
|
1266
|
+
)
|
|
1267
|
+
const snapshot = buildSnapshot(
|
|
1268
|
+
animated,
|
|
1269
|
+
statics,
|
|
1270
|
+
getAnimatedTransforms(animated.transform),
|
|
1271
|
+
previousKeys,
|
|
1272
|
+
lastPaintedRef.current
|
|
1273
|
+
)
|
|
1274
|
+
emitterSnapshotRef.value = snapshot.value
|
|
1275
|
+
emitterKeysRef.current = snapshot.keys
|
|
1276
|
+
lastPaintedRef.current = snapshot.painted
|
|
889
1277
|
|
|
890
1278
|
if (
|
|
891
1279
|
process.env.NODE_ENV === 'development' &&
|
|
@@ -895,7 +1283,7 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
895
1283
|
console.info('[animations-reanimated] useStyleEmitter update', {
|
|
896
1284
|
animated,
|
|
897
1285
|
statics,
|
|
898
|
-
transforms,
|
|
1286
|
+
transforms: snapshot.value.transforms,
|
|
899
1287
|
})
|
|
900
1288
|
}
|
|
901
1289
|
}
|
|
@@ -905,37 +1293,54 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
905
1293
|
// This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set
|
|
906
1294
|
const exitKeysRegistered = useRef(false)
|
|
907
1295
|
if (justStartedExiting && sendExitComplete) {
|
|
908
|
-
const exitKeys
|
|
909
|
-
|
|
1296
|
+
const exitKeys = getGatedKeys(renderSnapshot.value)
|
|
1297
|
+
pendingExitKeysRef.current = new Set(exitKeys)
|
|
1298
|
+
exitKeysRegistered.current = exitKeys.length > 0
|
|
1299
|
+
}
|
|
910
1300
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1301
|
+
// onDidAnimate is an internal completion hook used by components that
|
|
1302
|
+
// need to retain content through an in-place style transition. register
|
|
1303
|
+
// the exact keys that can animate before publishing the cycle to the
|
|
1304
|
+
// worklet; shared values are only written from the layout effect, never
|
|
1305
|
+
// from the mapper that reads them.
|
|
1306
|
+
const previousDidAnimateStyleRef = useRef<string | null>(null)
|
|
1307
|
+
const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null
|
|
1308
|
+
const previousDidAnimateStyle = previousDidAnimateStyleRef.current
|
|
1309
|
+
const shouldRegisterDidAnimate =
|
|
1310
|
+
didAnimateStyle !== null &&
|
|
1311
|
+
previousDidAnimateStyle !== null &&
|
|
1312
|
+
previousDidAnimateStyle !== didAnimateStyle &&
|
|
1313
|
+
!isExiting &&
|
|
1314
|
+
!isEntering &&
|
|
1315
|
+
!justFinishedEntering
|
|
1316
|
+
const didAnimateKeys = shouldRegisterDidAnimate
|
|
1317
|
+
? getGatedKeys(renderSnapshot.value)
|
|
1318
|
+
: []
|
|
918
1319
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const tKey = Object.keys(t)[0]
|
|
925
|
-
if (tKey) {
|
|
926
|
-
// check animateOnly filter for transform sub-keys
|
|
927
|
-
if (animateOnly && !animateOnly.includes(tKey)) {
|
|
928
|
-
continue
|
|
929
|
-
}
|
|
930
|
-
exitKeys.push(`transform:${tKey}`)
|
|
931
|
-
}
|
|
932
|
-
}
|
|
1320
|
+
useIsomorphicLayoutEffect(() => {
|
|
1321
|
+
if (didAnimateStyle === null) {
|
|
1322
|
+
previousDidAnimateStyleRef.current = null
|
|
1323
|
+
isCompletingAnimationRef.value = false
|
|
1324
|
+
return
|
|
933
1325
|
}
|
|
934
1326
|
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1327
|
+
// only a committed render may advance the authoritative cycle. the
|
|
1328
|
+
// captured previous value prevents StrictMode's repeated effect from
|
|
1329
|
+
// registering the same transition twice.
|
|
1330
|
+
const previousStyleStillCurrent =
|
|
1331
|
+
previousDidAnimateStyleRef.current === previousDidAnimateStyle
|
|
1332
|
+
previousDidAnimateStyleRef.current = didAnimateStyle
|
|
1333
|
+
if (!shouldRegisterDidAnimate || !previousStyleStillCurrent) return
|
|
1334
|
+
|
|
1335
|
+
const cycleId = ++didAnimateCycleIdRef.current
|
|
1336
|
+
pendingDidAnimateKeysRef.current = new Set(didAnimateKeys)
|
|
1337
|
+
didAnimateCompletedRef.current = didAnimateKeys.length === 0
|
|
1338
|
+
isCompletingAnimationRef.value = didAnimateKeys.length > 0
|
|
1339
|
+
didAnimateCycleIdShared.value = cycleId
|
|
1340
|
+
if (didAnimateKeys.length === 0) {
|
|
1341
|
+
onDidAnimateRef.current?.()
|
|
1342
|
+
}
|
|
1343
|
+
}, [didAnimateStyle, shouldRegisterDidAnimate])
|
|
939
1344
|
|
|
940
1345
|
// handle zero-animation case in effect (after render commit)
|
|
941
1346
|
React.useEffect(() => {
|
|
@@ -950,130 +1355,153 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
950
1355
|
}
|
|
951
1356
|
}, [justStartedExiting, sendExitComplete])
|
|
952
1357
|
|
|
1358
|
+
// the worklet's own record of which animated keys it has emitted (mirrors
|
|
1359
|
+
// reanimated's per-view style history, which resets whenever a key leaves
|
|
1360
|
+
// the output). a key not in here is fresh to the mapper no matter what
|
|
1361
|
+
// React has committed — several renders can coalesce into one mapper run.
|
|
1362
|
+
// mutated in place from the worklet; never watched, so writes don't
|
|
1363
|
+
// retrigger the mapper.
|
|
1364
|
+
const mapperStateRef = useSharedValue<{ emitted: Record<string, boolean> }>({
|
|
1365
|
+
emitted: {},
|
|
1366
|
+
})
|
|
953
1367
|
// Create animated style
|
|
954
1368
|
const animatedStyle = useAnimatedStyle(
|
|
955
1369
|
() => {
|
|
956
1370
|
'worklet'
|
|
957
1371
|
|
|
958
|
-
|
|
1372
|
+
const mapperState = mapperStateRef.value
|
|
1373
|
+
const config = configRef.value
|
|
1374
|
+
if (config.disableAnimation || config.isHydrating) {
|
|
1375
|
+
// the empty return wipes reanimated's per-key history, so ours
|
|
1376
|
+
// resets with it
|
|
1377
|
+
mapperState.emitted = {}
|
|
959
1378
|
return {}
|
|
960
1379
|
}
|
|
961
1380
|
|
|
1381
|
+
const previouslyEmitted = mapperState.emitted
|
|
1382
|
+
const emitted: Record<string, boolean> = {}
|
|
1383
|
+
|
|
962
1384
|
const result: Record<string, any> = {}
|
|
963
|
-
const config = configRef.value
|
|
964
1385
|
|
|
965
1386
|
// Check if we have avoidRerenders updates from useStyleEmitter
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const emitterTransforms = transformTargetsRef.value
|
|
969
|
-
const hasEmitterUpdates = emitterAnimated !== null
|
|
1387
|
+
const emitterSnapshot = emitterSnapshotRef.value
|
|
1388
|
+
const snapshot = emitterSnapshot ?? renderSnapshotRef.value
|
|
970
1389
|
|
|
971
|
-
// Use emitter values if available, otherwise use
|
|
1390
|
+
// Use emitter values if available, otherwise use the committed render snapshot.
|
|
972
1391
|
// statics must fall back to the render's staticStyles (not {}): once an
|
|
973
1392
|
// emitter snapshot has applied static keys, reanimated never unsets
|
|
974
1393
|
// them, so after the latch drops a stale emitted value (e.g. a border
|
|
975
1394
|
// color that missed animateOnly) would keep painting over the fresh
|
|
976
1395
|
// style prop forever
|
|
977
|
-
const animatedValues =
|
|
978
|
-
const staticValues =
|
|
1396
|
+
const animatedValues = snapshot.animated
|
|
1397
|
+
const staticValues = snapshot.statics
|
|
979
1398
|
|
|
980
1399
|
// read exit state from shared values
|
|
981
1400
|
const currentlyExiting = isExitingRef.value
|
|
982
1401
|
const currentCycleId = exitCycleIdShared.value
|
|
1402
|
+
const currentlyCompletingAnimation = isCompletingAnimationRef.value
|
|
1403
|
+
const currentDidAnimateCycleId = didAnimateCycleIdShared.value
|
|
983
1404
|
|
|
984
1405
|
// Include static values from emitter (for hover/press style changes)
|
|
985
1406
|
for (const key in staticValues) {
|
|
986
1407
|
result[key] = staticValues[key]
|
|
987
1408
|
}
|
|
1409
|
+
for (const key in snapshot.removedKeys) {
|
|
1410
|
+
if (!key.startsWith('transform:') && !(key in staticValues)) {
|
|
1411
|
+
result[key] = snapshot.clearValue
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
988
1414
|
|
|
989
1415
|
// Animate regular properties
|
|
990
1416
|
for (const key in animatedValues) {
|
|
991
1417
|
if (key === 'transform') continue
|
|
992
1418
|
|
|
993
1419
|
const targetValue = animatedValues[key]
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (currentlyExiting) {
|
|
999
|
-
const capturedKey = key
|
|
1000
|
-
const capturedCycleId = currentCycleId
|
|
1001
|
-
callback = (finished) => {
|
|
1002
|
-
'worklet'
|
|
1003
|
-
runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false)
|
|
1004
|
-
}
|
|
1420
|
+
emitted[key] = true
|
|
1421
|
+
if (typeof targetValue !== 'number' && typeof targetValue !== 'string') {
|
|
1422
|
+
result[key] = targetValue
|
|
1423
|
+
continue
|
|
1005
1424
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1425
|
+
result[key] = animateSnapshotValue(
|
|
1426
|
+
key,
|
|
1427
|
+
key,
|
|
1428
|
+
targetValue,
|
|
1429
|
+
config.propertyConfigs[key] ?? config.baseConfig,
|
|
1430
|
+
!!previouslyEmitted[key],
|
|
1431
|
+
snapshot.seeds[key],
|
|
1432
|
+
!!snapshot.gatedKeys[key],
|
|
1433
|
+
currentlyExiting,
|
|
1434
|
+
currentCycleId,
|
|
1435
|
+
currentlyCompletingAnimation,
|
|
1436
|
+
currentDidAnimateCycleId,
|
|
1437
|
+
markExitKeyDone,
|
|
1438
|
+
markDidAnimateKeyDone,
|
|
1439
|
+
!!COLOR_STYLE_KEYS[key]
|
|
1440
|
+
)
|
|
1008
1441
|
}
|
|
1009
1442
|
|
|
1010
1443
|
// Handle transforms
|
|
1011
|
-
const transforms =
|
|
1012
|
-
? emitterTransforms
|
|
1013
|
-
: animatedStyles.transform
|
|
1444
|
+
const transforms = snapshot.transforms
|
|
1014
1445
|
|
|
1015
1446
|
// Animate transform properties with validation
|
|
1016
|
-
if (transforms
|
|
1447
|
+
if (transforms.length > 0 || snapshot.removeTransform) {
|
|
1017
1448
|
const validTransforms: Record<string, unknown>[] = []
|
|
1018
1449
|
|
|
1019
1450
|
for (const t of transforms) {
|
|
1020
1451
|
if (!t) continue
|
|
1021
1452
|
const keys = Object.keys(t)
|
|
1022
1453
|
if (keys.length === 0) continue
|
|
1023
|
-
const
|
|
1454
|
+
const transformKey = keys[0]
|
|
1455
|
+
const value = t[transformKey]
|
|
1024
1456
|
if (typeof value === 'number' || typeof value === 'string') {
|
|
1025
|
-
const transformKey = Object.keys(t)[0]
|
|
1026
|
-
const targetValue = t[transformKey]
|
|
1027
1457
|
const propConfig =
|
|
1028
1458
|
config.propertyConfigs[transformKey] ?? config.baseConfig
|
|
1029
1459
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (currentlyExiting) {
|
|
1033
|
-
const capturedKey = `transform:${transformKey}`
|
|
1034
|
-
const capturedCycleId = currentCycleId
|
|
1035
|
-
callback = (finished) => {
|
|
1036
|
-
'worklet'
|
|
1037
|
-
runOnJS(markExitKeyDone)(
|
|
1038
|
-
capturedKey,
|
|
1039
|
-
capturedCycleId,
|
|
1040
|
-
finished ?? false
|
|
1041
|
-
)
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1460
|
+
const subKey = `transform:${transformKey}`
|
|
1461
|
+
emitted[subKey] = true
|
|
1045
1462
|
validTransforms.push({
|
|
1046
|
-
[transformKey]:
|
|
1047
|
-
|
|
1463
|
+
[transformKey]: animateSnapshotValue(
|
|
1464
|
+
subKey,
|
|
1465
|
+
transformKey,
|
|
1466
|
+
value,
|
|
1048
1467
|
propConfig,
|
|
1049
|
-
|
|
1468
|
+
!!previouslyEmitted[subKey],
|
|
1469
|
+
snapshot.seeds[subKey],
|
|
1470
|
+
!!snapshot.gatedKeys[subKey],
|
|
1471
|
+
currentlyExiting,
|
|
1472
|
+
currentCycleId,
|
|
1473
|
+
currentlyCompletingAnimation,
|
|
1474
|
+
currentDidAnimateCycleId,
|
|
1475
|
+
markExitKeyDone,
|
|
1476
|
+
markDidAnimateKeyDone
|
|
1050
1477
|
),
|
|
1051
1478
|
})
|
|
1479
|
+
} else {
|
|
1480
|
+
validTransforms.push(t)
|
|
1052
1481
|
}
|
|
1053
1482
|
}
|
|
1054
1483
|
|
|
1055
|
-
if (validTransforms.length > 0) {
|
|
1484
|
+
if (validTransforms.length > 0 || !('transform' in staticValues)) {
|
|
1056
1485
|
result.transform = validTransforms
|
|
1057
1486
|
}
|
|
1058
1487
|
}
|
|
1059
1488
|
|
|
1489
|
+
mapperState.emitted = emitted
|
|
1490
|
+
|
|
1060
1491
|
return result
|
|
1061
1492
|
},
|
|
1062
1493
|
isWeb
|
|
1063
1494
|
? [
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
disableAnimation,
|
|
1069
|
-
isHydrating,
|
|
1070
|
-
// pass SharedValues so the mapper watches them on web (no babel plugin)
|
|
1071
|
-
animatedTargetsRef,
|
|
1072
|
-
staticTargetsRef,
|
|
1073
|
-
transformTargetsRef,
|
|
1495
|
+
// pass SharedValues so the stable mapper watches them on web (no babel plugin)
|
|
1496
|
+
renderSnapshotRef,
|
|
1497
|
+
emitterSnapshotRef,
|
|
1498
|
+
configRef,
|
|
1074
1499
|
isExitingRef,
|
|
1075
1500
|
exitCycleIdShared,
|
|
1076
1501
|
markExitKeyDone,
|
|
1502
|
+
isCompletingAnimationRef,
|
|
1503
|
+
didAnimateCycleIdShared,
|
|
1504
|
+
markDidAnimateKeyDone,
|
|
1077
1505
|
]
|
|
1078
1506
|
: undefined
|
|
1079
1507
|
)
|
|
@@ -1097,6 +1525,9 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1097
1525
|
})
|
|
1098
1526
|
}
|
|
1099
1527
|
|
|
1528
|
+
// staticStyles carries animated keys only while mounting or on a key's
|
|
1529
|
+
// first post-mount appearance; after that the mapper owns them and the
|
|
1530
|
+
// commit bridge above keeps React commits from wiping its inline writes.
|
|
1100
1531
|
return {
|
|
1101
1532
|
style: [staticStyles, animatedStyle],
|
|
1102
1533
|
}
|