@tamagui/animations-reanimated 2.7.7 → 3.0.0-beta.643.1
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 +940 -897
- package/dist/cjs/createAnimations.native.cjs +1196 -0
- package/dist/cjs/createAnimations.native.js +1138 -1115
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +21 -0
- package/dist/cjs/index.native.js +10 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/createAnimations.mjs +926 -873
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +1125 -1093
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/index.native.js +1 -2
- package/package.json +8 -8
- package/src/createAnimations.tsx +264 -89
- package/types/createAnimations.d.ts.map +2 -2
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
package/src/createAnimations.tsx
CHANGED
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
View,
|
|
12
12
|
type AnimationDriver,
|
|
13
13
|
type UniversalAnimatedNumber,
|
|
14
|
+
createRefComponent,
|
|
14
15
|
} from '@tamagui/core'
|
|
15
16
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
16
17
|
import normalizeColor from '@react-native/normalize-colors'
|
|
17
|
-
import React, {
|
|
18
|
+
import React, { useMemo, useRef } from 'react'
|
|
18
19
|
import type { SharedValue } from 'react-native-reanimated'
|
|
19
20
|
import Animated_, {
|
|
20
21
|
cancelAnimation,
|
|
@@ -83,7 +84,7 @@ type AnimationSnapshot = {
|
|
|
83
84
|
* for each animated key (transform sub-keys as `transform:key`), the value the
|
|
84
85
|
* previous committed render painted for it, when one exists and is animatable.
|
|
85
86
|
* the worklet animates a mapper-fresh key FROM its seed instead of snapping —
|
|
86
|
-
* this is how an
|
|
87
|
+
* this is how an enter-clause value painted statically during mount becomes the
|
|
87
88
|
* start point once the key turns animated. keys with no seed paint their target
|
|
88
89
|
* directly (a key appearing from nothing has nothing to animate from).
|
|
89
90
|
*/
|
|
@@ -199,7 +200,7 @@ const getRemovedAnimatedKeys = (
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
// implicit start values for style keys that were never painted (e.g. a key
|
|
202
|
-
// introduced by
|
|
203
|
+
// introduced by an exit clause with no base value). most numerics default to 0.
|
|
203
204
|
const getImplicitDefault = (
|
|
204
205
|
key: string,
|
|
205
206
|
targetValue: number | string
|
|
@@ -285,7 +286,11 @@ const buildSnapshot = (
|
|
|
285
286
|
transforms: Array<Record<string, unknown>>,
|
|
286
287
|
previousKeys: Set<string>,
|
|
287
288
|
lastPainted: Record<string, unknown>
|
|
288
|
-
)
|
|
289
|
+
): {
|
|
290
|
+
value: AnimationSnapshot
|
|
291
|
+
painted: Record<string, unknown>
|
|
292
|
+
keys: Set<string>
|
|
293
|
+
} => {
|
|
289
294
|
const snapshotAnimated: Record<string, unknown> = {}
|
|
290
295
|
const snapshotStatics = cloneStyleRecord(statics)
|
|
291
296
|
|
|
@@ -392,13 +397,13 @@ const createReanimatedConfig = (config: TransitionConfig): Record<string, unknow
|
|
|
392
397
|
/**
|
|
393
398
|
* Apply animation to a value based on config, with optional completion callback
|
|
394
399
|
*/
|
|
395
|
-
const applyAnimation = (
|
|
396
|
-
targetValue:
|
|
400
|
+
const applyAnimation = <T extends number | string>(
|
|
401
|
+
targetValue: T,
|
|
397
402
|
config: TransitionConfig,
|
|
398
403
|
callback?: AnimationCallback,
|
|
399
404
|
seedValue?: number | string,
|
|
400
405
|
validateStartAsColor = false
|
|
401
|
-
):
|
|
406
|
+
): T => {
|
|
402
407
|
'worklet'
|
|
403
408
|
const delay = config.delay
|
|
404
409
|
const reanimatedConfig = createReanimatedConfig(config)
|
|
@@ -416,6 +421,23 @@ const applyAnimation = (
|
|
|
416
421
|
reanimatedConfig as WithSpringConfig,
|
|
417
422
|
callback
|
|
418
423
|
)
|
|
424
|
+
|
|
425
|
+
// reanimated stamps lastTimestamp from performance.now() when an animation
|
|
426
|
+
// starts outside a frame (an event handler), then steps it on the frame
|
|
427
|
+
// timestamp, which is the frame's start and lags performance.now() badly
|
|
428
|
+
// when frames are starved. the delta goes negative, and the closed-form
|
|
429
|
+
// spring exponentiates instead of decaying: a tooltip crossing triggers on
|
|
430
|
+
// a loaded machine lands at translate -33,554,430px for a frame before it
|
|
431
|
+
// relaxes back. reanimated clamps the delta's upper bound only, so hold the
|
|
432
|
+
// clock monotonic before it reaches the spring math.
|
|
433
|
+
const innerOnFrame = animatedValue.onFrame
|
|
434
|
+
animatedValue.onFrame = (animation: any, now: number) => {
|
|
435
|
+
'worklet'
|
|
436
|
+
return innerOnFrame(
|
|
437
|
+
animation,
|
|
438
|
+
now < animation.lastTimestamp ? animation.lastTimestamp : now
|
|
439
|
+
)
|
|
440
|
+
}
|
|
419
441
|
}
|
|
420
442
|
|
|
421
443
|
// reanimated starts a descriptor from its per-view history for the key — the
|
|
@@ -448,7 +470,7 @@ const applyAnimation = (
|
|
|
448
470
|
animatedValue = withDelay(delay, animatedValue)
|
|
449
471
|
}
|
|
450
472
|
|
|
451
|
-
return animatedValue
|
|
473
|
+
return animatedValue as T
|
|
452
474
|
}
|
|
453
475
|
|
|
454
476
|
const animateSnapshotValue = (
|
|
@@ -461,15 +483,19 @@ const animateSnapshotValue = (
|
|
|
461
483
|
gated: boolean,
|
|
462
484
|
currentlyExiting: boolean,
|
|
463
485
|
exitCycleId: number,
|
|
464
|
-
|
|
465
|
-
|
|
486
|
+
currentlyCompletingEnter: boolean,
|
|
487
|
+
enterCycleId: number,
|
|
488
|
+
currentlyCompletingUpdate: boolean,
|
|
489
|
+
updateCycleId: number,
|
|
466
490
|
markExitKeyDone: (key: string, cycleId: number, finished: boolean) => void,
|
|
467
|
-
|
|
491
|
+
markEnterKeyDone: (key: string, cycleId: number) => void,
|
|
492
|
+
markUpdateKeyDone: (key: string, cycleId: number, finished: boolean) => void,
|
|
468
493
|
validateStartAsColor = false
|
|
469
494
|
): number | string => {
|
|
470
495
|
'worklet'
|
|
471
496
|
|
|
472
|
-
const cycleGated =
|
|
497
|
+
const cycleGated =
|
|
498
|
+
gated && (currentlyExiting || currentlyCompletingEnter || currentlyCompletingUpdate)
|
|
473
499
|
if (!previouslyEmitted && seedValue === undefined && !cycleGated) {
|
|
474
500
|
return targetValue
|
|
475
501
|
}
|
|
@@ -480,10 +506,15 @@ const animateSnapshotValue = (
|
|
|
480
506
|
'worklet'
|
|
481
507
|
runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false)
|
|
482
508
|
}
|
|
483
|
-
} else if (gated &&
|
|
509
|
+
} else if (gated && currentlyCompletingEnter) {
|
|
510
|
+
callback = () => {
|
|
511
|
+
'worklet'
|
|
512
|
+
runOnJS(markEnterKeyDone)(key, enterCycleId)
|
|
513
|
+
}
|
|
514
|
+
} else if (gated && currentlyCompletingUpdate) {
|
|
484
515
|
callback = (finished) => {
|
|
485
516
|
'worklet'
|
|
486
|
-
runOnJS(
|
|
517
|
+
runOnJS(markUpdateKeyDone)(key, updateCycleId, finished ?? false)
|
|
487
518
|
}
|
|
488
519
|
}
|
|
489
520
|
|
|
@@ -620,7 +651,7 @@ function createWebAnimatedComponent(defaultTag: 'div' | 'span') {
|
|
|
620
651
|
const isText = defaultTag === 'span'
|
|
621
652
|
|
|
622
653
|
const Component = Animated.createAnimatedComponent(
|
|
623
|
-
|
|
654
|
+
createRefComponent((propsIn: any, ref) => {
|
|
624
655
|
const { forwardedRef, render = defaultTag, ...rest } = propsIn
|
|
625
656
|
const hostRef = useRef<HTMLElement>(null)
|
|
626
657
|
const composedRefs = useComposedRefs(forwardedRef, ref, hostRef)
|
|
@@ -650,8 +681,9 @@ function createWebAnimatedComponent(defaultTag: 'div' | 'span') {
|
|
|
650
681
|
stateRef as any,
|
|
651
682
|
false
|
|
652
683
|
)
|
|
684
|
+
const { nativeID, ...webProps } = transformedProps ?? viewProps
|
|
653
685
|
|
|
654
|
-
return <Element {...
|
|
686
|
+
return <Element {...webProps} ref={composedRefs} />
|
|
655
687
|
})
|
|
656
688
|
)
|
|
657
689
|
;(Component as any).acceptRenderProp = true
|
|
@@ -695,7 +727,7 @@ function buildTransitionConfig<A extends Record<string, TransitionConfig>>(
|
|
|
695
727
|
}
|
|
696
728
|
|
|
697
729
|
if (normalized.config) {
|
|
698
|
-
base = cloneTransitionConfig({ ...base, ...normalized.config })
|
|
730
|
+
base = cloneTransitionConfig({ ...base, ...normalized.config } as TransitionConfig)
|
|
699
731
|
// infer type: 'timing' if duration is provided without spring params
|
|
700
732
|
if (
|
|
701
733
|
base.type !== 'timing' &&
|
|
@@ -931,7 +963,7 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
931
963
|
themeName,
|
|
932
964
|
stateRef,
|
|
933
965
|
styleState,
|
|
934
|
-
|
|
966
|
+
onTransition,
|
|
935
967
|
} = animationProps
|
|
936
968
|
|
|
937
969
|
// State flags
|
|
@@ -967,25 +999,46 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
967
999
|
|
|
968
1000
|
// Get sendExitComplete callback from presence
|
|
969
1001
|
const sendExitComplete = presence?.[1]
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1002
|
+
const onTransitionRef = useRef(onTransition)
|
|
1003
|
+
onTransitionRef.current = onTransition
|
|
1004
|
+
const emit = (
|
|
1005
|
+
phase: 'start' | 'end',
|
|
1006
|
+
cause: 'enter' | 'exit' | 'update',
|
|
1007
|
+
finished?: boolean
|
|
1008
|
+
) => {
|
|
1009
|
+
onTransitionRef.current?.(
|
|
1010
|
+
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
const enterStartedRef = useRef(false)
|
|
1014
|
+
const exitStartedRef = useRef(false)
|
|
1015
|
+
const updateStartedRef = useRef(false)
|
|
1016
|
+
const isExitingJSRef = useRef(false)
|
|
1017
|
+
isExitingJSRef.current = isExiting
|
|
1018
|
+
|
|
1019
|
+
// =========================================================================
|
|
1020
|
+
// Update cycle state: an in-place style transition while mounted (not
|
|
1021
|
+
// enter/exit). drives the onTransition 'update' lifecycle events and the
|
|
1022
|
+
// components (e.g. accordion HeightAnimator) that retain content through
|
|
1023
|
+
// an in-place transition.
|
|
1024
|
+
// =========================================================================
|
|
1025
|
+
const updateCycleIdRef = useRef(0)
|
|
1026
|
+
const pendingUpdateKeysRef = useRef<Set<string>>(new Set())
|
|
1027
|
+
const updateCompletedRef = useRef(false)
|
|
1028
|
+
const isCompletingUpdateRef = useSharedValue(false)
|
|
1029
|
+
const updateCycleIdShared = useSharedValue(0)
|
|
1030
|
+
const markUpdateKeyDone = useEvent(
|
|
981
1031
|
(key: string, cycleId: number, finished: boolean) => {
|
|
982
|
-
if (!finished || cycleId !==
|
|
983
|
-
if (
|
|
984
|
-
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1032
|
+
if (!finished || cycleId !== updateCycleIdRef.current) return
|
|
1033
|
+
if (updateCompletedRef.current) return
|
|
1034
|
+
pendingUpdateKeysRef.current.delete(key)
|
|
1035
|
+
if (pendingUpdateKeysRef.current.size === 0) {
|
|
1036
|
+
updateCompletedRef.current = true
|
|
1037
|
+
isCompletingUpdateRef.value = false
|
|
1038
|
+
if (updateStartedRef.current) {
|
|
1039
|
+
updateStartedRef.current = false
|
|
1040
|
+
emit('end', 'update', true)
|
|
1041
|
+
}
|
|
989
1042
|
}
|
|
990
1043
|
}
|
|
991
1044
|
)
|
|
@@ -1017,23 +1070,55 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1017
1070
|
// check if all exit animations are done
|
|
1018
1071
|
if (pendingExitKeysRef.current.size === 0) {
|
|
1019
1072
|
exitCompletedRef.current = true
|
|
1073
|
+
if (exitStartedRef.current) {
|
|
1074
|
+
exitStartedRef.current = false
|
|
1075
|
+
// exit 'end' fires immediately before presence safeToRemove
|
|
1076
|
+
emit('end', 'exit', true)
|
|
1077
|
+
}
|
|
1020
1078
|
sendExitComplete?.()
|
|
1021
1079
|
}
|
|
1022
1080
|
}
|
|
1023
1081
|
)
|
|
1024
1082
|
|
|
1083
|
+
// =========================================================================
|
|
1084
|
+
// Enter cycle state for the onTransition enter 'end' event
|
|
1085
|
+
// =========================================================================
|
|
1086
|
+
const enterCycleIdRef = useRef(0)
|
|
1087
|
+
const pendingEnterKeysRef = useRef<Set<string>>(new Set())
|
|
1088
|
+
const enterCompletedRef = useRef(false)
|
|
1089
|
+
|
|
1090
|
+
const markEnterKeyDone = useEvent((key: string, cycleId: number) => {
|
|
1091
|
+
if (cycleId !== enterCycleIdRef.current) return
|
|
1092
|
+
if (enterCompletedRef.current) return
|
|
1093
|
+
if (isExitingJSRef.current) return
|
|
1094
|
+
|
|
1095
|
+
pendingEnterKeysRef.current.delete(key)
|
|
1096
|
+
|
|
1097
|
+
if (pendingEnterKeysRef.current.size === 0) {
|
|
1098
|
+
enterCompletedRef.current = true
|
|
1099
|
+
if (enterStartedRef.current) {
|
|
1100
|
+
enterStartedRef.current = false
|
|
1101
|
+
emit('end', 'enter', true)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1025
1106
|
// SharedValue to pass exit state into worklet
|
|
1026
1107
|
const isExitingRef = useSharedValue(isExiting)
|
|
1027
1108
|
const exitCycleIdShared = useSharedValue(exitCycleIdRef.current)
|
|
1109
|
+
const isCompletingEnterRef = useSharedValue(false)
|
|
1110
|
+
const enterCycleIdShared = useSharedValue(enterCycleIdRef.current)
|
|
1028
1111
|
|
|
1029
1112
|
// start new exit cycle only on transition INTO exiting (not every render while exiting)
|
|
1030
1113
|
if (justStartedExiting) {
|
|
1031
1114
|
exitCycleIdRef.current++
|
|
1032
1115
|
exitCompletedRef.current = false
|
|
1033
1116
|
pendingExitKeysRef.current.clear()
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1117
|
+
enterCycleIdRef.current++
|
|
1118
|
+
pendingEnterKeysRef.current.clear()
|
|
1119
|
+
updateCycleIdRef.current++
|
|
1120
|
+
pendingUpdateKeysRef.current.clear()
|
|
1121
|
+
updateCompletedRef.current = true
|
|
1037
1122
|
}
|
|
1038
1123
|
// invalidate pending callbacks when exit is canceled/interrupted
|
|
1039
1124
|
if (justStoppedExiting) {
|
|
@@ -1045,11 +1130,28 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1045
1130
|
useIsomorphicLayoutEffect(() => {
|
|
1046
1131
|
isExitingRef.value = isExiting
|
|
1047
1132
|
exitCycleIdShared.value = exitCycleIdRef.current
|
|
1133
|
+
isCompletingEnterRef.value =
|
|
1134
|
+
!isExiting && justFinishedEntering && Boolean(onTransitionRef.current)
|
|
1135
|
+
enterCycleIdShared.value = enterCycleIdRef.current
|
|
1048
1136
|
if (justStartedExiting) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1137
|
+
isCompletingUpdateRef.value = false
|
|
1138
|
+
updateCycleIdShared.value = updateCycleIdRef.current
|
|
1139
|
+
}
|
|
1140
|
+
}, [
|
|
1141
|
+
isExiting,
|
|
1142
|
+
justFinishedEntering,
|
|
1143
|
+
exitCycleIdRef.current,
|
|
1144
|
+
enterCycleIdRef.current,
|
|
1145
|
+
justStartedExiting,
|
|
1146
|
+
])
|
|
1147
|
+
|
|
1148
|
+
// exit interrupted by a re-enter: report the exit as finished:false
|
|
1149
|
+
useIsomorphicLayoutEffect(() => {
|
|
1150
|
+
if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {
|
|
1151
|
+
exitStartedRef.current = false
|
|
1152
|
+
emit('end', 'exit', false)
|
|
1051
1153
|
}
|
|
1052
|
-
}, [
|
|
1154
|
+
}, [justStoppedExiting])
|
|
1053
1155
|
|
|
1054
1156
|
// track previous exiting state
|
|
1055
1157
|
React.useEffect(() => {
|
|
@@ -1083,7 +1185,7 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1083
1185
|
// so React's per-key style diff never touches the key again and the
|
|
1084
1186
|
// mapper's inline writes survive every commit — reanimated's Fabric
|
|
1085
1187
|
// commit hook provides this guarantee on native, web has no equivalent.
|
|
1086
|
-
// during enter the carry is the
|
|
1188
|
+
// during enter the carry is the enter-clause value (the mapper animates over it);
|
|
1087
1189
|
// a key appearing post-mount (a just-measured height) paints its value in
|
|
1088
1190
|
// the same commit the mapper first sees it, a frame before the mapper's
|
|
1089
1191
|
// first write.
|
|
@@ -1162,22 +1264,28 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1162
1264
|
// unmounts immediately (the dialog-exit-snap). mirrored into a plain ref so the
|
|
1163
1265
|
// emitter callback (which runs outside render) can read it.
|
|
1164
1266
|
const pseudoActiveRef = useRef(false)
|
|
1165
|
-
const isExitingJSRef = useRef(false)
|
|
1166
|
-
isExitingJSRef.current = isExiting
|
|
1167
1267
|
// the committed render is the worklet's source of truth whenever the emitter is not
|
|
1168
1268
|
// latched, so every render must publish its snapshot — not just the renders that drop
|
|
1169
1269
|
// a latch. animated keys live only in this snapshot after mount (staticStyles carries
|
|
1170
1270
|
// them during mount only), so a render that never publishes leaves the worklet reading
|
|
1171
1271
|
// an empty snapshot and the animated properties never reach the screen at all.
|
|
1272
|
+
// The first emitter snapshot can already contain the browser's real media-query state
|
|
1273
|
+
// while the first render still contains mediaQueryDefaultActive for hydration. Publish
|
|
1274
|
+
// that render behind the latch, but do not let it replace the fresher runtime snapshot.
|
|
1172
1275
|
const publishedSnapshotRef = useRef<object | null>(null)
|
|
1173
1276
|
useIsomorphicLayoutEffect(() => {
|
|
1277
|
+
const renderChanged = publishedSnapshotRef.current !== renderSnapshot.value
|
|
1174
1278
|
const droppingLatch =
|
|
1175
|
-
(isExiting ||
|
|
1279
|
+
(isExiting ||
|
|
1280
|
+
(!pseudoActiveRef.current &&
|
|
1281
|
+
publishedSnapshotRef.current !== null &&
|
|
1282
|
+
renderChanged)) &&
|
|
1283
|
+
emitterSnapshotRef.value !== null
|
|
1176
1284
|
// when the latch drops, keys the emitter owned that this render no longer has must
|
|
1177
1285
|
// be cleared too, otherwise reanimated keeps painting the stale emitted value
|
|
1178
1286
|
const emitterKeys = droppingLatch ? emitterKeysRef.current : null
|
|
1179
1287
|
|
|
1180
|
-
if (droppingLatch ||
|
|
1288
|
+
if (droppingLatch || renderChanged) {
|
|
1181
1289
|
const removedKeys = emitterKeys
|
|
1182
1290
|
? {
|
|
1183
1291
|
...renderSnapshot.value.removedKeys,
|
|
@@ -1309,35 +1417,42 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1309
1417
|
// Compute and register exit keys synchronously during render to avoid race conditions
|
|
1310
1418
|
// This must happen BEFORE useAnimatedStyle runs so callbacks have a populated set
|
|
1311
1419
|
const exitKeysRegistered = useRef(false)
|
|
1420
|
+
const enterKeysRegistered = useRef(false)
|
|
1421
|
+
|
|
1422
|
+
if (!isExiting && justFinishedEntering && onTransition) {
|
|
1423
|
+
const enterKeys = getGatedKeys(renderSnapshot.value)
|
|
1424
|
+
enterCycleIdRef.current++
|
|
1425
|
+
enterCompletedRef.current = false
|
|
1426
|
+
pendingEnterKeysRef.current = new Set(enterKeys)
|
|
1427
|
+
enterKeysRegistered.current = enterKeys.length > 0
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1312
1430
|
if (justStartedExiting && sendExitComplete) {
|
|
1313
1431
|
const exitKeys = getGatedKeys(renderSnapshot.value)
|
|
1314
1432
|
pendingExitKeysRef.current = new Set(exitKeys)
|
|
1315
1433
|
exitKeysRegistered.current = exitKeys.length > 0
|
|
1316
1434
|
}
|
|
1317
1435
|
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
// the
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
previousDidAnimateStyle !== didAnimateStyle &&
|
|
1436
|
+
// update cycle: a genuine in-place style change while mounted (not
|
|
1437
|
+
// entering or exiting). register the exact keys that can animate before
|
|
1438
|
+
// publishing the cycle to the worklet; shared values are only written
|
|
1439
|
+
// from the layout effect, never from the mapper that reads them.
|
|
1440
|
+
const previousUpdateStyleRef = useRef<string | null>(null)
|
|
1441
|
+
const updateStyle = onTransition ? JSON.stringify(style) : null
|
|
1442
|
+
const previousUpdateStyle = previousUpdateStyleRef.current
|
|
1443
|
+
const shouldRegisterUpdate =
|
|
1444
|
+
updateStyle !== null &&
|
|
1445
|
+
previousUpdateStyle !== null &&
|
|
1446
|
+
previousUpdateStyle !== updateStyle &&
|
|
1330
1447
|
!isExiting &&
|
|
1331
1448
|
!isEntering &&
|
|
1332
1449
|
!justFinishedEntering
|
|
1333
|
-
const
|
|
1334
|
-
? getGatedKeys(renderSnapshot.value)
|
|
1335
|
-
: []
|
|
1450
|
+
const updateKeys = shouldRegisterUpdate ? getGatedKeys(renderSnapshot.value) : []
|
|
1336
1451
|
|
|
1337
1452
|
useIsomorphicLayoutEffect(() => {
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1340
|
-
|
|
1453
|
+
if (updateStyle === null) {
|
|
1454
|
+
previousUpdateStyleRef.current = null
|
|
1455
|
+
isCompletingUpdateRef.value = false
|
|
1341
1456
|
return
|
|
1342
1457
|
}
|
|
1343
1458
|
|
|
@@ -1345,33 +1460,82 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1345
1460
|
// captured previous value prevents StrictMode's repeated effect from
|
|
1346
1461
|
// registering the same transition twice.
|
|
1347
1462
|
const previousStyleStillCurrent =
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
if (!
|
|
1351
|
-
|
|
1352
|
-
const cycleId = ++
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1463
|
+
previousUpdateStyleRef.current === previousUpdateStyle
|
|
1464
|
+
previousUpdateStyleRef.current = updateStyle
|
|
1465
|
+
if (!shouldRegisterUpdate || !previousStyleStillCurrent) return
|
|
1466
|
+
|
|
1467
|
+
const cycleId = ++updateCycleIdRef.current
|
|
1468
|
+
pendingUpdateKeysRef.current = new Set(updateKeys)
|
|
1469
|
+
updateCompletedRef.current = updateKeys.length === 0
|
|
1470
|
+
isCompletingUpdateRef.value = updateKeys.length > 0
|
|
1471
|
+
updateCycleIdShared.value = cycleId
|
|
1472
|
+
|
|
1473
|
+
// a superseded in-flight update reports finished:false before the new
|
|
1474
|
+
// cycle starts; a zero-key cycle reports an immediate start/end pair
|
|
1475
|
+
if (updateStartedRef.current) {
|
|
1476
|
+
emit('end', 'update', false)
|
|
1477
|
+
}
|
|
1478
|
+
updateStartedRef.current = true
|
|
1479
|
+
emit('start', 'update')
|
|
1480
|
+
if (updateKeys.length === 0) {
|
|
1481
|
+
updateStartedRef.current = false
|
|
1482
|
+
emit('end', 'update', true)
|
|
1359
1483
|
}
|
|
1360
|
-
}, [
|
|
1484
|
+
}, [updateStyle, shouldRegisterUpdate])
|
|
1361
1485
|
|
|
1362
1486
|
// handle zero-animation case in effect (after render commit)
|
|
1363
1487
|
React.useEffect(() => {
|
|
1364
1488
|
if (!justStartedExiting || !sendExitComplete) return
|
|
1365
1489
|
|
|
1490
|
+
// enter or update still in flight when exit begins is reported as finished:false
|
|
1491
|
+
if (enterStartedRef.current) {
|
|
1492
|
+
enterStartedRef.current = false
|
|
1493
|
+
emit('end', 'enter', false)
|
|
1494
|
+
}
|
|
1495
|
+
if (updateStartedRef.current) {
|
|
1496
|
+
updateStartedRef.current = false
|
|
1497
|
+
emit('end', 'update', false)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// emit exit start once per exit cycle
|
|
1501
|
+
if (onTransitionRef.current && !exitStartedRef.current) {
|
|
1502
|
+
exitStartedRef.current = true
|
|
1503
|
+
emit('start', 'exit')
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1366
1506
|
// if no keys were registered, complete immediately
|
|
1367
1507
|
if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
|
|
1368
1508
|
if (!exitCompletedRef.current) {
|
|
1369
1509
|
exitCompletedRef.current = true
|
|
1510
|
+
if (exitStartedRef.current) {
|
|
1511
|
+
exitStartedRef.current = false
|
|
1512
|
+
emit('end', 'exit', true)
|
|
1513
|
+
}
|
|
1370
1514
|
sendExitComplete()
|
|
1371
1515
|
}
|
|
1372
1516
|
}
|
|
1373
1517
|
}, [justStartedExiting, sendExitComplete])
|
|
1374
1518
|
|
|
1519
|
+
React.useEffect(() => {
|
|
1520
|
+
if (isExiting || !justFinishedEntering || !onTransition) return
|
|
1521
|
+
|
|
1522
|
+
// emit enter start once per enter cycle
|
|
1523
|
+
if (!enterStartedRef.current) {
|
|
1524
|
+
enterStartedRef.current = true
|
|
1525
|
+
emit('start', 'enter')
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (!enterKeysRegistered.current && pendingEnterKeysRef.current.size === 0) {
|
|
1529
|
+
if (!enterCompletedRef.current) {
|
|
1530
|
+
enterCompletedRef.current = true
|
|
1531
|
+
if (enterStartedRef.current) {
|
|
1532
|
+
enterStartedRef.current = false
|
|
1533
|
+
emit('end', 'enter', true)
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}, [isExiting, justFinishedEntering, onTransition])
|
|
1538
|
+
|
|
1375
1539
|
// the worklet's own record of which animated keys it has emitted (mirrors
|
|
1376
1540
|
// reanimated's per-view style history, which resets whenever a key leaves
|
|
1377
1541
|
// the output). a key not in here is fresh to the mapper no matter what
|
|
@@ -1413,11 +1577,13 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1413
1577
|
const animatedValues = snapshot.animated
|
|
1414
1578
|
const staticValues = snapshot.statics
|
|
1415
1579
|
|
|
1416
|
-
// read
|
|
1580
|
+
// read lifecycle state from shared values
|
|
1417
1581
|
const currentlyExiting = isExitingRef.value
|
|
1418
1582
|
const currentCycleId = exitCycleIdShared.value
|
|
1419
|
-
const
|
|
1420
|
-
const
|
|
1583
|
+
const currentlyCompletingEnter = isCompletingEnterRef.value
|
|
1584
|
+
const currentEnterCycleId = enterCycleIdShared.value
|
|
1585
|
+
const currentlyCompletingUpdate = isCompletingUpdateRef.value
|
|
1586
|
+
const currentUpdateCycleId = updateCycleIdShared.value
|
|
1421
1587
|
|
|
1422
1588
|
// Include static values from emitter (for hover/press style changes)
|
|
1423
1589
|
for (const key in staticValues) {
|
|
@@ -1449,10 +1615,13 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1449
1615
|
!!snapshot.gatedKeys[key],
|
|
1450
1616
|
currentlyExiting,
|
|
1451
1617
|
currentCycleId,
|
|
1452
|
-
|
|
1453
|
-
|
|
1618
|
+
currentlyCompletingEnter,
|
|
1619
|
+
currentEnterCycleId,
|
|
1620
|
+
currentlyCompletingUpdate,
|
|
1621
|
+
currentUpdateCycleId,
|
|
1454
1622
|
markExitKeyDone,
|
|
1455
|
-
|
|
1623
|
+
markEnterKeyDone,
|
|
1624
|
+
markUpdateKeyDone,
|
|
1456
1625
|
!!COLOR_STYLE_KEYS[key]
|
|
1457
1626
|
)
|
|
1458
1627
|
}
|
|
@@ -1487,10 +1656,13 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1487
1656
|
!!snapshot.gatedKeys[subKey],
|
|
1488
1657
|
currentlyExiting,
|
|
1489
1658
|
currentCycleId,
|
|
1490
|
-
|
|
1491
|
-
|
|
1659
|
+
currentlyCompletingEnter,
|
|
1660
|
+
currentEnterCycleId,
|
|
1661
|
+
currentlyCompletingUpdate,
|
|
1662
|
+
currentUpdateCycleId,
|
|
1492
1663
|
markExitKeyDone,
|
|
1493
|
-
|
|
1664
|
+
markEnterKeyDone,
|
|
1665
|
+
markUpdateKeyDone
|
|
1494
1666
|
),
|
|
1495
1667
|
})
|
|
1496
1668
|
} else {
|
|
@@ -1516,9 +1688,12 @@ export function createAnimations<A extends Record<string, TransitionConfig>>(
|
|
|
1516
1688
|
isExitingRef,
|
|
1517
1689
|
exitCycleIdShared,
|
|
1518
1690
|
markExitKeyDone,
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1691
|
+
isCompletingEnterRef,
|
|
1692
|
+
enterCycleIdShared,
|
|
1693
|
+
markEnterKeyDone,
|
|
1694
|
+
isCompletingUpdateRef,
|
|
1695
|
+
updateCycleIdShared,
|
|
1696
|
+
markUpdateKeyDone,
|
|
1522
1697
|
]
|
|
1523
1698
|
: undefined
|
|
1524
1699
|
)
|