@tamagui/animations-react-native 2.7.7 → 3.0.0-beta.1097.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.native.cjs +692 -0
- package/dist/cjs/createAnimations.native.js +643 -597
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +19 -13
- package/dist/cjs/index.native.cjs +21 -0
- package/dist/cjs/index.native.js +12 -13
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/polyfill.cjs +2 -2
- package/dist/cjs/polyfill.native.cjs +5 -0
- package/dist/cjs/polyfill.native.js +2 -1
- package/dist/cjs/polyfill.native.js.map +1 -1
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.native.cjs +18 -0
- package/dist/cjs/types.native.js +20 -0
- package/dist/cjs/types.native.js.map +1 -0
- package/dist/esm/createAnimations.native.js +621 -567
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +6 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/polyfill.mjs +2 -1
- package/dist/esm/polyfill.mjs.map +1 -1
- package/dist/esm/polyfill.native.js +2 -1
- package/dist/esm/polyfill.native.js.map +1 -1
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/types.native.js +0 -0
- package/package.json +15 -10
- package/src/{createAnimations.tsx → createAnimations.native.tsx} +277 -137
- package/src/index.native.ts +3 -0
- package/src/index.ts +20 -2
- package/src/types.ts +4 -0
- package/types/{createAnimations.d.ts → createAnimations.native.d.ts} +5 -14
- package/types/createAnimations.native.d.ts.map +11 -0
- package/types/index.d.ts +10 -2
- package/types/index.d.ts.map +2 -2
- package/types/index.native.d.ts +4 -0
- package/types/index.native.d.ts.map +11 -0
- package/types/types.d.ts +5 -0
- package/types/types.d.ts.map +11 -0
- package/dist/cjs/createAnimations.cjs +0 -566
- package/dist/esm/createAnimations.mjs +0 -524
- package/dist/esm/createAnimations.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/types/createAnimations.d.ts.map +0 -11
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
easingToBezier,
|
|
3
|
+
forAnimationState,
|
|
4
|
+
getTransitionForKey,
|
|
5
|
+
resolveTransition,
|
|
6
|
+
type AnimationsConfig,
|
|
7
|
+
type ResolvedEntry,
|
|
8
|
+
type ResolvedTransition,
|
|
9
|
+
} from '@tamagui/animation-helpers'
|
|
2
10
|
import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
3
11
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
4
12
|
import type {
|
|
5
13
|
AnimatedNumberStrategy,
|
|
6
|
-
|
|
14
|
+
AnimationDriverWithAnimatedNumbers,
|
|
7
15
|
TransitionProp,
|
|
8
16
|
UniversalAnimatedNumber,
|
|
9
17
|
UseAnimatedNumberReaction,
|
|
10
18
|
UseAnimatedNumberStyle,
|
|
11
19
|
} from '@tamagui/web'
|
|
12
|
-
import { useEvent
|
|
20
|
+
import { useEvent } from '@tamagui/web'
|
|
21
|
+
import { useThemeWithState } from '@tamagui/web/internal-runtime'
|
|
13
22
|
import React from 'react'
|
|
14
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
Animated,
|
|
25
|
+
Easing,
|
|
26
|
+
processColor,
|
|
27
|
+
type ColorValue,
|
|
28
|
+
type Text,
|
|
29
|
+
type View,
|
|
30
|
+
} from 'react-native'
|
|
31
|
+
|
|
32
|
+
import type { CreateAnimationsOptions } from './types'
|
|
15
33
|
|
|
16
34
|
// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys
|
|
17
35
|
const isFabric =
|
|
@@ -26,27 +44,17 @@ const resolveDynamicValue = (value: any, isDark: boolean): any => {
|
|
|
26
44
|
return value
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| 'speed'
|
|
41
|
-
| 'stiffness'
|
|
42
|
-
| 'tension'
|
|
43
|
-
| 'velocity'
|
|
44
|
-
>
|
|
45
|
-
>
|
|
46
|
-
|
|
47
|
-
type TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>
|
|
48
|
-
|
|
49
|
-
type AnimationConfig = SpringConfig | TimingConfig
|
|
47
|
+
/** what `getAnimationConfig` hands to `Animated.spring` / `Animated.timing` */
|
|
48
|
+
type AnimationConfig =
|
|
49
|
+
| ({ type: 'spring'; delay?: number } & Partial<
|
|
50
|
+
Pick<
|
|
51
|
+
Animated.SpringAnimationConfig,
|
|
52
|
+
'damping' | 'mass' | 'overshootClamping' | 'stiffness' | 'velocity'
|
|
53
|
+
>
|
|
54
|
+
>)
|
|
55
|
+
| ({ type: 'timing'; delay?: number } & Partial<
|
|
56
|
+
Pick<Animated.TimingAnimationConfig, 'duration' | 'easing'>
|
|
57
|
+
>)
|
|
50
58
|
|
|
51
59
|
const animatedStyleKey = {
|
|
52
60
|
transform: true,
|
|
@@ -77,25 +85,21 @@ const layoutStyleKey = {
|
|
|
77
85
|
function hasAnimatedLayoutKey(
|
|
78
86
|
style: Record<string, any>,
|
|
79
87
|
isDark: boolean,
|
|
80
|
-
|
|
88
|
+
resolved: ResolvedTransition
|
|
81
89
|
) {
|
|
82
90
|
for (const key in layoutStyleKey) {
|
|
83
|
-
if (
|
|
91
|
+
if (!getTransitionForKey(resolved, key)) continue
|
|
84
92
|
if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true
|
|
85
93
|
}
|
|
86
94
|
return false
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
//
|
|
90
|
-
// calc(
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// static style instead of entering interpolation.
|
|
97
|
+
// Only colors accepted by RN's own parser can enter interpolation. CSS-wide
|
|
98
|
+
// keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach
|
|
99
|
+
// createInterpolationFromStringOutputRange / mapStringToNumericComponents and
|
|
100
|
+
// throw. Those values must be applied as static styles.
|
|
94
101
|
function isAnimatableColor(value: unknown): value is string {
|
|
95
|
-
|
|
96
|
-
if (value === '') return false
|
|
97
|
-
if (value.includes('var(') || value.includes('calc(')) return false
|
|
98
|
-
return true
|
|
102
|
+
return typeof value === 'string' && processColor(value as ColorValue) != null
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
// these style keys are costly to animate and only work with native driver on Fabric
|
|
@@ -113,11 +117,6 @@ const costlyToAnimateStyleKey = {
|
|
|
113
117
|
...colorStyleKey,
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
type CreateAnimationsOptions = {
|
|
117
|
-
// override native driver detection (default: auto-detect Fabric)
|
|
118
|
-
useNativeDriver?: boolean
|
|
119
|
-
}
|
|
120
|
-
|
|
121
120
|
export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
|
|
122
121
|
export const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text
|
|
123
122
|
|
|
@@ -158,7 +157,12 @@ export function useAnimatedNumber(
|
|
|
158
157
|
: undefined
|
|
159
158
|
|
|
160
159
|
if (type === 'direct') {
|
|
160
|
+
state.current.composite?.stop()
|
|
161
|
+
state.current.composite = null
|
|
161
162
|
val.setValue(next)
|
|
163
|
+
// a direct set finishes the moment it lands. not calling back stranded
|
|
164
|
+
// everything waiting on it (sheet snap, presence completion).
|
|
165
|
+
onFinish?.()
|
|
162
166
|
} else if (type === 'spring') {
|
|
163
167
|
state.current.composite?.stop()
|
|
164
168
|
const composite = Animated.spring(val, {
|
|
@@ -204,7 +208,33 @@ export const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (
|
|
|
204
208
|
value,
|
|
205
209
|
getStyle
|
|
206
210
|
) => {
|
|
207
|
-
|
|
211
|
+
const instance = value.getInstance()
|
|
212
|
+
const animatedStyle = getStyle(instance)
|
|
213
|
+
const usesAnimatedNode = hasAnimatedNode(animatedStyle)
|
|
214
|
+
const [current, setCurrent] = React.useState(value.getValue())
|
|
215
|
+
|
|
216
|
+
// preserve the native animated-node path for direct mappings. callbacks
|
|
217
|
+
// that do arithmetic require numeric values, so drive those through the
|
|
218
|
+
// value listener and render the computed style.
|
|
219
|
+
React.useEffect(() => {
|
|
220
|
+
if (usesAnimatedNode) return
|
|
221
|
+
|
|
222
|
+
const id = instance.addListener(({ value: next }) => {
|
|
223
|
+
setCurrent(next)
|
|
224
|
+
})
|
|
225
|
+
return () => {
|
|
226
|
+
instance.removeListener(id)
|
|
227
|
+
}
|
|
228
|
+
}, [instance, usesAnimatedNode])
|
|
229
|
+
|
|
230
|
+
return usesAnimatedNode ? animatedStyle : getStyle(current)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasAnimatedNode(value: unknown): boolean {
|
|
234
|
+
if (!value || typeof value !== 'object') return false
|
|
235
|
+
if (typeof (value as any).__getValue === 'function') return true
|
|
236
|
+
if (Array.isArray(value)) return value.some(hasAnimatedNode)
|
|
237
|
+
return Object.values(value).some(hasAnimatedNode)
|
|
208
238
|
}
|
|
209
239
|
|
|
210
240
|
export const useAnimatedNumbersStyle = (
|
|
@@ -217,11 +247,10 @@ export const useAnimatedNumbersStyle = (
|
|
|
217
247
|
export function createAnimations<A extends AnimationsConfig>(
|
|
218
248
|
animations: A,
|
|
219
249
|
options?: CreateAnimationsOptions
|
|
220
|
-
):
|
|
250
|
+
): AnimationDriverWithAnimatedNumbers<A> {
|
|
221
251
|
const nativeDriver = options?.useNativeDriver ?? isFabric
|
|
222
252
|
|
|
223
253
|
return {
|
|
224
|
-
isReactNative: true,
|
|
225
254
|
inputStyle: 'value',
|
|
226
255
|
outputStyle: 'inline',
|
|
227
256
|
avoidReRenders: true,
|
|
@@ -237,16 +266,36 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
237
266
|
ResetPresence,
|
|
238
267
|
useAnimations: ({
|
|
239
268
|
props,
|
|
240
|
-
|
|
269
|
+
onTransition,
|
|
241
270
|
style,
|
|
242
271
|
componentState,
|
|
243
272
|
presence,
|
|
244
273
|
stateRef,
|
|
274
|
+
styleState,
|
|
245
275
|
useStyleEmitter,
|
|
246
276
|
}) => {
|
|
247
277
|
const isDisabled = isWeb && componentState.unmounted === true
|
|
248
278
|
const isExiting = presence?.[0] === false
|
|
249
279
|
const sendExitComplete = presence?.[1]
|
|
280
|
+
const onTransitionRef = React.useRef(onTransition)
|
|
281
|
+
onTransitionRef.current = onTransition
|
|
282
|
+
const emit = (
|
|
283
|
+
phase: 'start' | 'end',
|
|
284
|
+
cause: 'enter' | 'exit' | 'update',
|
|
285
|
+
finished?: boolean
|
|
286
|
+
) => {
|
|
287
|
+
onTransitionRef.current?.(
|
|
288
|
+
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
// createComponent merges a colocated `transition` out of the active
|
|
292
|
+
// pseudo style (`enterStyle={{ opacity: 0, transition: '200ms' }}`), so
|
|
293
|
+
// this is the one that applies right now, not the base prop.
|
|
294
|
+
const effectiveTransition = (styleState?.effectiveTransition ?? props.transition) as
|
|
295
|
+
| TransitionProp
|
|
296
|
+
| null
|
|
297
|
+
| undefined
|
|
298
|
+
|
|
250
299
|
const [, themeState] = useThemeWithState({})
|
|
251
300
|
// Check scheme first, then fall back to checking theme name for 'dark'
|
|
252
301
|
const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
|
|
@@ -272,6 +321,13 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
272
321
|
const exitCompletedRef = React.useRef(false)
|
|
273
322
|
const wasExitingRef = React.useRef(false)
|
|
274
323
|
|
|
324
|
+
// onTransition lifecycle bookkeeping
|
|
325
|
+
const enterStartedRef = React.useRef(false)
|
|
326
|
+
const exitStartedRef = React.useRef(false)
|
|
327
|
+
const updateInFlightRef = React.useRef(false)
|
|
328
|
+
const updateCycleIdRef = React.useRef(0)
|
|
329
|
+
const prevStyleSigRef = React.useRef<string | null>(null)
|
|
330
|
+
|
|
275
331
|
// detect transition into/out of exiting state
|
|
276
332
|
const justStartedExiting = isExiting && !wasExitingRef.current
|
|
277
333
|
const justStoppedExiting = !isExiting && wasExitingRef.current
|
|
@@ -286,9 +342,6 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
286
342
|
exitCycleIdRef.current++
|
|
287
343
|
}
|
|
288
344
|
|
|
289
|
-
const animateOnly = (props.animateOnly as string[]) || []
|
|
290
|
-
const hasTransitionOnly = !!props.animateOnly
|
|
291
|
-
|
|
292
345
|
// Track if we just finished entering (transition from entering to not entering)
|
|
293
346
|
// must be declared before args array that uses justFinishedEntering
|
|
294
347
|
const isEntering = !!componentState.unmounted
|
|
@@ -300,12 +353,12 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
300
353
|
|
|
301
354
|
const args = [
|
|
302
355
|
JSON.stringify(style),
|
|
356
|
+
JSON.stringify(effectiveTransition),
|
|
303
357
|
componentState,
|
|
304
358
|
isExiting,
|
|
305
|
-
!!
|
|
359
|
+
!!onTransition,
|
|
306
360
|
isDark,
|
|
307
361
|
justFinishedEntering,
|
|
308
|
-
hasTransitionOnly,
|
|
309
362
|
]
|
|
310
363
|
|
|
311
364
|
const res = React.useMemo(() => {
|
|
@@ -320,17 +373,19 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
320
373
|
? 'enter'
|
|
321
374
|
: 'default'
|
|
322
375
|
|
|
376
|
+
// which style keys animate at all is the transition's own decision, so
|
|
377
|
+
// a property list narrows this the same way it narrows css
|
|
378
|
+
const resolved = forAnimationState(
|
|
379
|
+
resolveTransition(effectiveTransition, { animations }),
|
|
380
|
+
animationState
|
|
381
|
+
)
|
|
382
|
+
|
|
323
383
|
const nonAnimatedStyle = {}
|
|
324
384
|
// animatedStyle owns every Animated.Value on the node. Fabric cannot mix
|
|
325
385
|
// native- and JS-driven values inside that shared graph, so one layout
|
|
326
386
|
// animation makes the whole node use the JS driver.
|
|
327
387
|
const useNativeDriverForNode =
|
|
328
|
-
nativeDriver &&
|
|
329
|
-
!hasAnimatedLayoutKey(
|
|
330
|
-
style,
|
|
331
|
-
isDark,
|
|
332
|
-
hasTransitionOnly ? animateOnly : undefined
|
|
333
|
-
)
|
|
388
|
+
nativeDriver && !hasAnimatedLayoutKey(style, isDark, resolved)
|
|
334
389
|
|
|
335
390
|
// track which animated keys/transforms the incoming style actually
|
|
336
391
|
// carries this pass, so entries that left the style can be dropped
|
|
@@ -342,7 +397,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
342
397
|
|
|
343
398
|
for (const key in style) {
|
|
344
399
|
const rawVal = style[key]
|
|
345
|
-
// Resolve dynamic theme values
|
|
400
|
+
// Resolve dynamic theme values from flat theme clauses.
|
|
346
401
|
const val = resolveDynamicValue(rawVal, isDark)
|
|
347
402
|
if (val === undefined) continue
|
|
348
403
|
|
|
@@ -359,7 +414,10 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
359
414
|
continue
|
|
360
415
|
}
|
|
361
416
|
|
|
362
|
-
|
|
417
|
+
// `transform` is a container, not a property: its parts each resolve
|
|
418
|
+
// on their own below, and a part no entry covers gets `snapConfig`.
|
|
419
|
+
// the array cannot be split into animated and static halves here.
|
|
420
|
+
if (key !== 'transform' && !getTransitionForKey(resolved, key)) {
|
|
363
421
|
nonAnimatedStyle[key] = val
|
|
364
422
|
continue
|
|
365
423
|
}
|
|
@@ -371,7 +429,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
371
429
|
continue
|
|
372
430
|
}
|
|
373
431
|
|
|
374
|
-
// unparseable
|
|
432
|
+
// unparseable colors crash RN
|
|
375
433
|
// interpolation — apply them as a static style instead
|
|
376
434
|
if (colorStyleKey[key] && !isAnimatableColor(val)) {
|
|
377
435
|
nonAnimatedStyle[key] = val
|
|
@@ -493,7 +551,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
493
551
|
const animationConfig = getAnimationConfig(
|
|
494
552
|
key,
|
|
495
553
|
animations,
|
|
496
|
-
|
|
554
|
+
effectiveTransition,
|
|
497
555
|
animationState
|
|
498
556
|
)
|
|
499
557
|
|
|
@@ -506,22 +564,19 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
506
564
|
runners.push(() => {
|
|
507
565
|
value.stopAnimation()
|
|
508
566
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
? Animated.sequence([
|
|
519
|
-
|
|
520
|
-
getAnimation(),
|
|
521
|
-
])
|
|
522
|
-
: getAnimation()
|
|
567
|
+
// `delay` drives the sequence below, so it must not also ride
|
|
568
|
+
// along in the config or every delayed animation waits twice
|
|
569
|
+
const { type, delay, ...config } = animationConfig
|
|
570
|
+
const animation = Animated[type || 'spring'](value, {
|
|
571
|
+
toValue: animateToValue,
|
|
572
|
+
...config,
|
|
573
|
+
useNativeDriver: useNativeDriverForNode,
|
|
574
|
+
})
|
|
575
|
+
const animation2 = delay
|
|
576
|
+
? Animated.sequence([Animated.delay(delay), animation])
|
|
577
|
+
: animation
|
|
523
578
|
|
|
524
|
-
|
|
579
|
+
animation2.start(({ finished }) => {
|
|
525
580
|
// always resolve during exit (element is leaving anyway)
|
|
526
581
|
// for non-exit, only resolve on successful completion
|
|
527
582
|
if (finished || isExiting) {
|
|
@@ -556,15 +611,74 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
556
611
|
wasExitingRef.current = isExiting
|
|
557
612
|
})
|
|
558
613
|
|
|
614
|
+
// exit interrupted by a re-enter: report the exit as finished:false
|
|
615
|
+
useIsomorphicLayoutEffect(() => {
|
|
616
|
+
if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {
|
|
617
|
+
exitStartedRef.current = false
|
|
618
|
+
emit('end', 'exit', false)
|
|
619
|
+
}
|
|
620
|
+
}, [justStoppedExiting])
|
|
621
|
+
|
|
559
622
|
useIsomorphicLayoutEffect(() => {
|
|
560
623
|
res.runners.forEach((r) => r())
|
|
561
624
|
|
|
562
625
|
// capture current cycle id
|
|
563
626
|
const cycleId = exitCycleIdRef.current
|
|
564
627
|
|
|
565
|
-
|
|
628
|
+
const cause: 'enter' | 'exit' | 'update' = isExiting
|
|
629
|
+
? 'exit'
|
|
630
|
+
: isEntering || justFinishedEntering
|
|
631
|
+
? 'enter'
|
|
632
|
+
: 'update'
|
|
633
|
+
|
|
634
|
+
// interruptions: an enter or update still in flight when exit begins is
|
|
635
|
+
// reported as finished:false (its own completion promise won't resolve
|
|
636
|
+
// because the animation was stopped, not finished).
|
|
637
|
+
if (cause === 'exit') {
|
|
638
|
+
if (enterStartedRef.current) {
|
|
639
|
+
enterStartedRef.current = false
|
|
640
|
+
emit('end', 'enter', false)
|
|
641
|
+
}
|
|
642
|
+
if (updateInFlightRef.current) {
|
|
643
|
+
updateInFlightRef.current = false
|
|
644
|
+
updateCycleIdRef.current++
|
|
645
|
+
emit('end', 'update', false)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// in-place update: a genuine style change while mounted (not entering or
|
|
650
|
+
// exiting). guard on the style signature so lifecycle-only re-renders
|
|
651
|
+
// don't register as updates.
|
|
652
|
+
if (cause === 'update') {
|
|
653
|
+
const sig = args[0] as string
|
|
654
|
+
if (prevStyleSigRef.current === null || prevStyleSigRef.current === sig) {
|
|
655
|
+
prevStyleSigRef.current = sig
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
prevStyleSigRef.current = sig
|
|
659
|
+
if (res.completions.length === 0) return
|
|
660
|
+
if (updateInFlightRef.current) {
|
|
661
|
+
// superseded before finishing
|
|
662
|
+
emit('end', 'update', false)
|
|
663
|
+
}
|
|
664
|
+
updateInFlightRef.current = true
|
|
665
|
+
const uid = ++updateCycleIdRef.current
|
|
666
|
+
emit('start', 'update')
|
|
667
|
+
Promise.all(res.completions).then(() => {
|
|
668
|
+
if (uid !== updateCycleIdRef.current) return
|
|
669
|
+
updateInFlightRef.current = false
|
|
670
|
+
emit('end', 'update', true)
|
|
671
|
+
})
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// keep the update signature current while entering/exiting
|
|
676
|
+
prevStyleSigRef.current = args[0] as string
|
|
677
|
+
|
|
678
|
+
// handle zero-completion case immediately (enter/exit report a pair)
|
|
566
679
|
if (res.completions.length === 0) {
|
|
567
|
-
|
|
680
|
+
emit('start', cause)
|
|
681
|
+
emit('end', cause, true)
|
|
568
682
|
if (isExiting && !exitCompletedRef.current) {
|
|
569
683
|
exitCompletedRef.current = true
|
|
570
684
|
sendExitComplete?.()
|
|
@@ -572,40 +686,58 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
572
686
|
return
|
|
573
687
|
}
|
|
574
688
|
|
|
575
|
-
|
|
689
|
+
// enter/exit start (once per cycle; re-runs continue the same animation)
|
|
690
|
+
if (cause === 'enter' && !enterStartedRef.current) {
|
|
691
|
+
enterStartedRef.current = true
|
|
692
|
+
emit('start', 'enter')
|
|
693
|
+
}
|
|
694
|
+
if (cause === 'exit' && !exitStartedRef.current) {
|
|
695
|
+
exitStartedRef.current = true
|
|
696
|
+
emit('start', 'exit')
|
|
697
|
+
}
|
|
698
|
+
|
|
576
699
|
Promise.all(res.completions).then(() => {
|
|
577
|
-
if (cancel) return
|
|
578
700
|
// guard against stale cycle completion
|
|
579
701
|
if (isExiting && cycleId !== exitCycleIdRef.current) return
|
|
580
702
|
if (isExiting && exitCompletedRef.current) return
|
|
581
703
|
|
|
582
|
-
onDidAnimate?.()
|
|
583
704
|
if (isExiting) {
|
|
705
|
+
if (exitStartedRef.current) {
|
|
706
|
+
exitStartedRef.current = false
|
|
707
|
+
// exit 'end' fires immediately before presence safeToRemove
|
|
708
|
+
emit('end', 'exit', true)
|
|
709
|
+
}
|
|
584
710
|
exitCompletedRef.current = true
|
|
585
711
|
sendExitComplete?.()
|
|
712
|
+
} else if (enterStartedRef.current) {
|
|
713
|
+
enterStartedRef.current = false
|
|
714
|
+
emit('end', 'enter', true)
|
|
586
715
|
}
|
|
587
716
|
})
|
|
588
|
-
return () => {
|
|
589
|
-
cancel = true
|
|
590
|
-
}
|
|
591
717
|
}, args)
|
|
592
718
|
|
|
593
719
|
// avoidReRenders: receive style changes imperatively from tamagui
|
|
594
720
|
// and update Animated.Values directly without React re-renders
|
|
595
721
|
// reuses the same update() + runner pattern as the useMemo path
|
|
596
|
-
useStyleEmitter?.((nextStyle,
|
|
722
|
+
useStyleEmitter?.((nextStyle, emittedTransition, pseudoActive) => {
|
|
597
723
|
pseudoActiveRef.current = pseudoActive === true
|
|
598
724
|
const runners: Function[] = []
|
|
599
725
|
const seenAnimateKeys = new Set<string>()
|
|
600
726
|
let transformCount = 0
|
|
601
727
|
let animatedShapeChanged = false
|
|
728
|
+
// the emitter runs on a mounted node, so `default` is the state, but
|
|
729
|
+
// the transition is the one it was handed
|
|
730
|
+
const emittedResolved = forAnimationState(
|
|
731
|
+
resolveTransition(emittedTransition ?? effectiveTransition, { animations }),
|
|
732
|
+
'default'
|
|
733
|
+
)
|
|
602
734
|
// nextStyle is the complete style for this node, so the emitter makes
|
|
603
735
|
// the same single driver decision as the render path. include the
|
|
604
736
|
// currently rendered graph because its stale keys are not removed until
|
|
605
737
|
// the structural-change commit below.
|
|
606
738
|
const useNativeDriverForNode =
|
|
607
739
|
nativeDriver &&
|
|
608
|
-
!hasAnimatedLayoutKey(nextStyle, isDark) &&
|
|
740
|
+
!hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&
|
|
609
741
|
!Object.keys(animateStyles.current).some((key) => layoutStyleKey[key])
|
|
610
742
|
|
|
611
743
|
for (const key in nextStyle) {
|
|
@@ -703,23 +835,23 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
703
835
|
})
|
|
704
836
|
}
|
|
705
837
|
|
|
838
|
+
// the emitter runs for pseudo-state changes on a mounted node, so
|
|
839
|
+
// `default` is the state, but the transition is the one it was handed
|
|
706
840
|
const animationConfig = getAnimationConfig(
|
|
707
841
|
key,
|
|
708
842
|
animations,
|
|
709
|
-
|
|
843
|
+
emittedTransition ?? effectiveTransition,
|
|
710
844
|
'default'
|
|
711
845
|
)
|
|
712
846
|
runners.push(() => {
|
|
713
847
|
value.stopAnimation()
|
|
714
|
-
const
|
|
848
|
+
const { type, delay, ...config } = animationConfig
|
|
849
|
+
const anim = Animated[type || 'spring'](value, {
|
|
715
850
|
toValue: animateToValue,
|
|
716
|
-
...
|
|
851
|
+
...config,
|
|
717
852
|
useNativeDriver: useNativeDriverForNode,
|
|
718
853
|
})
|
|
719
|
-
;(
|
|
720
|
-
? Animated.sequence([Animated.delay(animationConfig.delay), anim])
|
|
721
|
-
: anim
|
|
722
|
-
).start()
|
|
854
|
+
;(delay ? Animated.sequence([Animated.delay(delay), anim]) : anim).start()
|
|
723
855
|
})
|
|
724
856
|
|
|
725
857
|
return value
|
|
@@ -770,62 +902,70 @@ function getInterpolated(current: number, next: number, postfix = 'deg') {
|
|
|
770
902
|
}
|
|
771
903
|
}
|
|
772
904
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
propAnimation.type || getEffectiveAnimation(normalized, animationState)
|
|
796
|
-
extraConf = propAnimation
|
|
797
|
-
} else {
|
|
798
|
-
// Fall back to effective animation based on state (enter/exit/default)
|
|
799
|
-
animationType = getEffectiveAnimation(normalized, animationState)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Apply global delay if no property-specific delay
|
|
803
|
-
if (normalized.delay && !extraConf.delay) {
|
|
804
|
-
extraConf = { ...extraConf, delay: normalized.delay }
|
|
905
|
+
/**
|
|
906
|
+
* one resolved entry as a react-native Animated config.
|
|
907
|
+
*
|
|
908
|
+
* springs go in as stiffness/damping/mass, which is the parameterization RN
|
|
909
|
+
* actually integrates. `bounciness`/`speed` and `tension`/`friction` are older
|
|
910
|
+
* spellings of the same two numbers, so nothing is lost by not using them.
|
|
911
|
+
*/
|
|
912
|
+
function entryToRN(entry: ResolvedEntry): AnimationConfig {
|
|
913
|
+
const extra = entry.timing.kind === 'spring' ? entry.timing.extra : undefined
|
|
914
|
+
|
|
915
|
+
if (entry.timing.kind === 'spring') {
|
|
916
|
+
return {
|
|
917
|
+
type: 'spring',
|
|
918
|
+
stiffness: entry.timing.stiffness,
|
|
919
|
+
damping: entry.timing.damping,
|
|
920
|
+
mass: entry.timing.mass,
|
|
921
|
+
...(typeof extra?.velocity === 'number' ? { velocity: extra.velocity } : null),
|
|
922
|
+
...(typeof extra?.overshootClamping === 'boolean'
|
|
923
|
+
? { overshootClamping: extra.overshootClamping }
|
|
924
|
+
: null),
|
|
925
|
+
...(entry.delayMs ? { delay: entry.delayMs } : null),
|
|
926
|
+
}
|
|
805
927
|
}
|
|
806
928
|
|
|
807
|
-
const
|
|
929
|
+
const bezier = easingToBezier(entry.timing.easing)
|
|
808
930
|
return {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
//
|
|
813
|
-
...
|
|
931
|
+
type: 'timing',
|
|
932
|
+
duration: entry.timing.durationMs,
|
|
933
|
+
// `linear()` and `steps()` have no bezier equivalent; RN's default easing
|
|
934
|
+
// is the honest answer rather than a curve we made up
|
|
935
|
+
...(bezier
|
|
936
|
+
? { easing: Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) }
|
|
937
|
+
: null),
|
|
938
|
+
...(entry.delayMs ? { delay: entry.delayMs } : null),
|
|
814
939
|
}
|
|
815
940
|
}
|
|
816
941
|
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
942
|
+
// a key the transition does not cover does not animate. snapping is what css
|
|
943
|
+
// does for an unlisted property, so the drivers have to agree on it too.
|
|
944
|
+
const snapConfig: AnimationConfig = { type: 'timing', duration: 0 }
|
|
945
|
+
|
|
946
|
+
function getAnimationConfig(
|
|
947
|
+
key: string,
|
|
948
|
+
animations: AnimationsConfig,
|
|
949
|
+
transition?: TransitionProp | null,
|
|
950
|
+
animationState: 'enter' | 'exit' | 'default' = 'default'
|
|
951
|
+
): AnimationConfig {
|
|
952
|
+
const resolved = forAnimationState(
|
|
953
|
+
resolveTransition(transition, { animations }),
|
|
954
|
+
animationState
|
|
955
|
+
)
|
|
956
|
+
const entry = getTransitionForKey(resolved, key)
|
|
957
|
+
return entry ? entryToRN(entry) : snapConfig
|
|
823
958
|
}
|
|
824
959
|
|
|
825
960
|
function getValue(input: number | string, isColor = false) {
|
|
826
961
|
if (typeof input !== 'string') {
|
|
827
962
|
return [input] as const
|
|
828
963
|
}
|
|
829
|
-
|
|
964
|
+
// the unit is optional: unitless numbers reach here as strings (scale, and
|
|
965
|
+
// any bare token value), and the number may be fractional. matching only
|
|
966
|
+
// `[-0-9]+` followed by a required unit read "1.5deg" as 5 and gave NaN for
|
|
967
|
+
// "0.95", and an Animated animation toward NaN never calls its completion
|
|
968
|
+
// callback, which strands whatever waits on it.
|
|
969
|
+
const [_, number, after] = input.match(/(-?(?:\d+\.?\d*|\.\d+))(deg|%|px)?/) ?? []
|
|
830
970
|
return [+number, after] as const
|
|
831
971
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
import '
|
|
1
|
+
import type { AnimationsConfig } from '@tamagui/animation-helpers'
|
|
2
|
+
import type { AnimationDriverWithAnimatedNumbers } from '@tamagui/web'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import type { CreateAnimationsOptions } from './types'
|
|
5
|
+
|
|
6
|
+
export type { CreateAnimationsOptions }
|
|
7
|
+
|
|
8
|
+
const unsupportedWebDriverMessage =
|
|
9
|
+
'[@tamagui/animations-react-native] This animation driver only works on native. On web, use @tamagui/animations-css, @tamagui/animations-motion, or @tamagui/animations-reanimated.'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The web half of the native driver. It keeps the same signature so a shared
|
|
13
|
+
* config typechecks on both platforms, and throws if anything actually reaches
|
|
14
|
+
* for it: `Animated` lives in react-native, which web is not tied to.
|
|
15
|
+
*/
|
|
16
|
+
export const createAnimations = <A extends AnimationsConfig>(
|
|
17
|
+
_animations: A,
|
|
18
|
+
_options?: CreateAnimationsOptions
|
|
19
|
+
): AnimationDriverWithAnimatedNumbers<A> => {
|
|
20
|
+
throw new Error(unsupportedWebDriverMessage)
|
|
21
|
+
}
|