@tamagui/animations-react-native 2.7.7 → 3.0.0-0.canary.1791056644527

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.
Files changed (47) hide show
  1. package/dist/cjs/createAnimations.native.cjs +863 -0
  2. package/dist/cjs/createAnimations.native.js +814 -597
  3. package/dist/cjs/createAnimations.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +19 -13
  5. package/dist/cjs/index.native.cjs +21 -0
  6. package/dist/cjs/index.native.js +12 -13
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/polyfill.cjs +2 -2
  9. package/dist/cjs/polyfill.native.cjs +5 -0
  10. package/dist/cjs/polyfill.native.js +2 -1
  11. package/dist/cjs/polyfill.native.js.map +1 -1
  12. package/dist/cjs/types.cjs +16 -0
  13. package/dist/cjs/types.native.cjs +18 -0
  14. package/dist/cjs/types.native.js +20 -0
  15. package/dist/cjs/types.native.js.map +1 -0
  16. package/dist/esm/createAnimations.native.js +792 -567
  17. package/dist/esm/createAnimations.native.js.map +1 -1
  18. package/dist/esm/index.js +6 -2
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/index.mjs +6 -2
  21. package/dist/esm/index.mjs.map +1 -1
  22. package/dist/esm/index.native.js +2 -2
  23. package/dist/esm/polyfill.mjs +2 -1
  24. package/dist/esm/polyfill.mjs.map +1 -1
  25. package/dist/esm/polyfill.native.js +2 -1
  26. package/dist/esm/polyfill.native.js.map +1 -1
  27. package/dist/esm/types.mjs +0 -0
  28. package/dist/esm/types.native.js +0 -0
  29. package/package.json +17 -11
  30. package/src/createAnimations.native.tsx +1351 -0
  31. package/src/index.native.ts +3 -0
  32. package/src/index.ts +20 -2
  33. package/src/types.ts +4 -0
  34. package/types/{createAnimations.d.ts → createAnimations.native.d.ts} +5 -14
  35. package/types/createAnimations.native.d.ts.map +11 -0
  36. package/types/index.d.ts +10 -2
  37. package/types/index.d.ts.map +2 -2
  38. package/types/index.native.d.ts +4 -0
  39. package/types/index.native.d.ts.map +11 -0
  40. package/types/types.d.ts +5 -0
  41. package/types/types.d.ts.map +11 -0
  42. package/dist/cjs/createAnimations.cjs +0 -566
  43. package/dist/esm/createAnimations.mjs +0 -524
  44. package/dist/esm/createAnimations.mjs.map +0 -1
  45. package/dist/esm/index.native.js.map +0 -1
  46. package/src/createAnimations.tsx +0 -831
  47. package/types/createAnimations.d.ts.map +0 -11
@@ -1,831 +0,0 @@
1
- import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'
2
- import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
3
- import { ResetPresence, usePresence } from '@tamagui/use-presence'
4
- import type {
5
- AnimatedNumberStrategy,
6
- AnimationDriver,
7
- TransitionProp,
8
- UniversalAnimatedNumber,
9
- UseAnimatedNumberReaction,
10
- UseAnimatedNumberStyle,
11
- } from '@tamagui/web'
12
- import { useEvent, useThemeWithState } from '@tamagui/web'
13
- import React from 'react'
14
- import { Animated, type Text, type View } from 'react-native'
15
-
16
- // detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys
17
- const isFabric =
18
- !isWeb && typeof global !== 'undefined' && !!global.__nativeFabricUIManager
19
-
20
- // Helper to resolve dynamic theme values like {dynamic: {dark: "value", light: undefined}}
21
- const resolveDynamicValue = (value: any, isDark: boolean): any => {
22
- if (value && typeof value === 'object' && 'dynamic' in value) {
23
- const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light
24
- return dynamicValue
25
- }
26
- return value
27
- }
28
-
29
- type AnimationsConfig<A extends object = any> = { [Key in keyof A]: AnimationConfig }
30
-
31
- type SpringConfig = { type?: 'spring' } & Partial<
32
- Pick<
33
- Animated.SpringAnimationConfig,
34
- | 'delay'
35
- | 'bounciness'
36
- | 'damping'
37
- | 'friction'
38
- | 'mass'
39
- | 'overshootClamping'
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
50
-
51
- const animatedStyleKey = {
52
- transform: true,
53
- opacity: true,
54
- }
55
-
56
- const colorStyleKey = {
57
- backgroundColor: true,
58
- color: true,
59
- borderColor: true,
60
- borderLeftColor: true,
61
- borderRightColor: true,
62
- borderTopColor: true,
63
- borderBottomColor: true,
64
- }
65
-
66
- // layout dimension keys. these must run on the JS driver (useNativeDriver:false)
67
- // because the native animated module can't drive layout props.
68
- const layoutStyleKey = {
69
- height: true,
70
- width: true,
71
- minHeight: true,
72
- maxHeight: true,
73
- minWidth: true,
74
- maxWidth: true,
75
- }
76
-
77
- function hasAnimatedLayoutKey(
78
- style: Record<string, any>,
79
- isDark: boolean,
80
- animateOnly?: string[]
81
- ) {
82
- for (const key in layoutStyleKey) {
83
- if (animateOnly && !animateOnly.includes(key)) continue
84
- if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true
85
- }
86
- return false
87
- }
88
-
89
- // a color string that RN's color interpolation can actually parse. var(...),
90
- // calc(...) and empty strings reach createInterpolationFromStringOutputRange /
91
- // mapStringToNumericComponents and throw ('.map of null' / 'outputRange must
92
- // contain color or value with numeric component'). those must be applied as a
93
- // static style instead of entering interpolation.
94
- function isAnimatableColor(value: unknown): value is string {
95
- if (typeof value !== 'string') return false
96
- if (value === '') return false
97
- if (value.includes('var(') || value.includes('calc(')) return false
98
- return true
99
- }
100
-
101
- // these style keys are costly to animate and only work with native driver on Fabric
102
- const costlyToAnimateStyleKey = {
103
- borderRadius: true,
104
- borderTopLeftRadius: true,
105
- borderTopRightRadius: true,
106
- borderBottomLeftRadius: true,
107
- borderBottomRightRadius: true,
108
- borderWidth: true,
109
- borderLeftWidth: true,
110
- borderRightWidth: true,
111
- borderTopWidth: true,
112
- borderBottomWidth: true,
113
- ...colorStyleKey,
114
- }
115
-
116
- type CreateAnimationsOptions = {
117
- // override native driver detection (default: auto-detect Fabric)
118
- useNativeDriver?: boolean
119
- }
120
-
121
- export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
122
- export const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text
123
-
124
- export function useAnimatedNumber(
125
- initial: number
126
- ): UniversalAnimatedNumber<Animated.Value> {
127
- const state = React.useRef(
128
- null as any as {
129
- val: Animated.Value
130
- composite: Animated.CompositeAnimation | null
131
- strategy: AnimatedNumberStrategy
132
- }
133
- )
134
- if (!state.current) {
135
- state.current = {
136
- composite: null,
137
- val: new Animated.Value(initial),
138
- strategy: { type: 'spring' },
139
- }
140
- }
141
-
142
- return {
143
- getInstance() {
144
- return state.current.val
145
- },
146
- getValue() {
147
- return state.current.val['_value']
148
- },
149
- stop() {
150
- state.current.composite?.stop()
151
- state.current.composite = null
152
- },
153
- setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {
154
- const val = state.current.val
155
-
156
- const handleFinish = onFinish
157
- ? ({ finished }) => (finished ? onFinish() : null)
158
- : undefined
159
-
160
- if (type === 'direct') {
161
- val.setValue(next)
162
- } else if (type === 'spring') {
163
- state.current.composite?.stop()
164
- const composite = Animated.spring(val, {
165
- ...config,
166
- toValue: next,
167
- useNativeDriver: isFabric,
168
- })
169
- composite.start(handleFinish)
170
- state.current.composite = composite
171
- } else {
172
- state.current.composite?.stop()
173
- const composite = Animated.timing(val, {
174
- ...config,
175
- toValue: next,
176
- useNativeDriver: isFabric,
177
- })
178
- composite.start(handleFinish)
179
- state.current.composite = composite
180
- }
181
- },
182
- }
183
- }
184
-
185
- type RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>
186
-
187
- export const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (
188
- { value },
189
- onValue
190
- ) => {
191
- const onChange = useEvent((current) => {
192
- onValue(current.value)
193
- })
194
-
195
- React.useEffect(() => {
196
- const id = value.getInstance().addListener(onChange)
197
- return () => {
198
- value.getInstance().removeListener(id)
199
- }
200
- }, [value, onChange])
201
- }
202
-
203
- export const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (
204
- value,
205
- getStyle
206
- ) => {
207
- return getStyle(value.getInstance())
208
- }
209
-
210
- export const useAnimatedNumbersStyle = (
211
- vals: RNAnimatedNum[],
212
- getStyle: (...currentValues: any[]) => any
213
- ): any => {
214
- return getStyle(...vals.map((v) => v.getInstance()))
215
- }
216
-
217
- export function createAnimations<A extends AnimationsConfig>(
218
- animations: A,
219
- options?: CreateAnimationsOptions
220
- ): AnimationDriver<A> {
221
- const nativeDriver = options?.useNativeDriver ?? isFabric
222
-
223
- return {
224
- isReactNative: true,
225
- inputStyle: 'value',
226
- outputStyle: 'inline',
227
- avoidReRenders: true,
228
- animations,
229
- needsCustomComponent: true,
230
- View: AnimatedView,
231
- Text: AnimatedText,
232
- useAnimatedNumber,
233
- useAnimatedNumberReaction,
234
- useAnimatedNumberStyle,
235
- useAnimatedNumbersStyle,
236
- usePresence,
237
- ResetPresence,
238
- useAnimations: ({
239
- props,
240
- onDidAnimate,
241
- style,
242
- componentState,
243
- presence,
244
- stateRef,
245
- useStyleEmitter,
246
- }) => {
247
- const isDisabled = isWeb && componentState.unmounted === true
248
- const isExiting = presence?.[0] === false
249
- const sendExitComplete = presence?.[1]
250
- const [, themeState] = useThemeWithState({})
251
- // Check scheme first, then fall back to checking theme name for 'dark'
252
- const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
253
-
254
- /** store Animated value of each key e.g: color: AnimatedValue */
255
- const animateStyles = React.useRef<Record<string, Animated.Value>>({})
256
- const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])
257
- const animationsState = React.useRef(
258
- new WeakMap<
259
- Animated.Value,
260
- {
261
- interpolation: Animated.AnimatedInterpolation<any>
262
- current?: number | string | undefined
263
- // only for colors
264
- animateToValue?: number
265
- }
266
- >()
267
- )
268
- const pseudoActiveRef = React.useRef(false)
269
-
270
- // exit cycle guards to prevent stale/duplicate completion
271
- const exitCycleIdRef = React.useRef(0)
272
- const exitCompletedRef = React.useRef(false)
273
- const wasExitingRef = React.useRef(false)
274
-
275
- // detect transition into/out of exiting state
276
- const justStartedExiting = isExiting && !wasExitingRef.current
277
- const justStoppedExiting = !isExiting && wasExitingRef.current
278
-
279
- // start new exit cycle only on transition INTO exiting
280
- if (justStartedExiting) {
281
- exitCycleIdRef.current++
282
- exitCompletedRef.current = false
283
- }
284
- // invalidate pending callbacks when exit is canceled/interrupted
285
- if (justStoppedExiting) {
286
- exitCycleIdRef.current++
287
- }
288
-
289
- const animateOnly = (props.animateOnly as string[]) || []
290
- const hasTransitionOnly = !!props.animateOnly
291
-
292
- // Track if we just finished entering (transition from entering to not entering)
293
- // must be declared before args array that uses justFinishedEntering
294
- const isEntering = !!componentState.unmounted
295
- const wasEnteringRef = React.useRef(isEntering)
296
- const justFinishedEntering = wasEnteringRef.current && !isEntering
297
- React.useEffect(() => {
298
- wasEnteringRef.current = isEntering
299
- })
300
-
301
- const args = [
302
- JSON.stringify(style),
303
- componentState,
304
- isExiting,
305
- !!onDidAnimate,
306
- isDark,
307
- justFinishedEntering,
308
- hasTransitionOnly,
309
- ]
310
-
311
- const res = React.useMemo(() => {
312
- const runners: Function[] = []
313
- const completions: Promise<void>[] = []
314
-
315
- // Determine animation state for enter/exit transitions
316
- // Use 'enter' if we're entering OR if we just finished entering
317
- const animationState: 'enter' | 'exit' | 'default' = isExiting
318
- ? 'exit'
319
- : isEntering || justFinishedEntering
320
- ? 'enter'
321
- : 'default'
322
-
323
- const nonAnimatedStyle = {}
324
- // animatedStyle owns every Animated.Value on the node. Fabric cannot mix
325
- // native- and JS-driven values inside that shared graph, so one layout
326
- // animation makes the whole node use the JS driver.
327
- const useNativeDriverForNode =
328
- nativeDriver &&
329
- !hasAnimatedLayoutKey(
330
- style,
331
- isDark,
332
- hasTransitionOnly ? animateOnly : undefined
333
- )
334
-
335
- // track which animated keys/transforms the incoming style actually
336
- // carries this pass, so entries that left the style can be dropped
337
- // below (an Animated.Value that persisted forever would keep painting
338
- // a stale pixel value, e.g. a released-to-auto accordion height)
339
- const seenAnimateKeys = new Set<string>()
340
- let sawTransform = false
341
- let transformCount = 0
342
-
343
- for (const key in style) {
344
- const rawVal = style[key]
345
- // Resolve dynamic theme values (like $theme-dark)
346
- const val = resolveDynamicValue(rawVal, isDark)
347
- if (val === undefined) continue
348
-
349
- if (isDisabled) {
350
- continue
351
- }
352
-
353
- if (
354
- animatedStyleKey[key] == null &&
355
- !costlyToAnimateStyleKey[key] &&
356
- !layoutStyleKey[key]
357
- ) {
358
- nonAnimatedStyle[key] = val
359
- continue
360
- }
361
-
362
- if (hasTransitionOnly && !animateOnly.includes(key)) {
363
- nonAnimatedStyle[key] = val
364
- continue
365
- }
366
-
367
- // layout dimension keys only animate numbers — 'auto' (an open
368
- // accordion at rest) and percent strings apply as static styles
369
- if (layoutStyleKey[key] && typeof val !== 'number') {
370
- nonAnimatedStyle[key] = val
371
- continue
372
- }
373
-
374
- // unparseable themed colors (var(), calc(), empty) crash RN
375
- // interpolation — apply them as a static style instead
376
- if (colorStyleKey[key] && !isAnimatableColor(val)) {
377
- nonAnimatedStyle[key] = val
378
- continue
379
- }
380
-
381
- if (key !== 'transform') {
382
- animateStyles.current[key] = update(key, animateStyles.current[key], val)
383
- seenAnimateKeys.add(key)
384
- continue
385
- }
386
- // key: 'transform'
387
- // for now just support one transform key
388
- if (!val) continue
389
- if (typeof val === 'string') {
390
- console.warn(`Warning: Tamagui can't animate string transforms yet!`)
391
- continue
392
- }
393
-
394
- sawTransform = true
395
- for (const transform of val) {
396
- if (!transform) continue
397
- const index = transformCount++
398
- // tkey: e.g: 'translateX'
399
- const tkey = Object.keys(transform)[0]
400
- const currentTransform = animatedTranforms.current[index]?.[tkey]
401
- animatedTranforms.current[index] = {
402
- [tkey]: update(tkey, currentTransform, transform[tkey]),
403
- }
404
- animatedTranforms.current = [...animatedTranforms.current]
405
- }
406
- }
407
-
408
- // drop stale Animated.Values whose keys left the incoming style, so the
409
- // key genuinely leaves the rendered style object (a released height goes
410
- // back to auto instead of staying pinned at its last pixel value). skip
411
- // while exiting (presence still animates the leaving keys) and while
412
- // disabled (the loop above intentionally skips every key). an active
413
- // pseudo owns the current emitted style until its matching release.
414
- if (!isExiting && !isDisabled && !pseudoActiveRef.current) {
415
- for (const k in animateStyles.current) {
416
- if (!seenAnimateKeys.has(k)) delete animateStyles.current[k]
417
- }
418
- if (!sawTransform) {
419
- if (animatedTranforms.current.length) animatedTranforms.current = []
420
- } else if (animatedTranforms.current.length > transformCount) {
421
- animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)
422
- }
423
- }
424
-
425
- const animatedTransformStyle =
426
- animatedTranforms.current.length > 0
427
- ? {
428
- transform: animatedTranforms.current.map((r) => {
429
- const key = Object.keys(r)[0]
430
- const val =
431
- animationsState.current!.get(r[key])?.interpolation || r[key]
432
- return { [key]: val }
433
- }),
434
- }
435
- : {}
436
-
437
- const animatedStyle = {
438
- ...Object.fromEntries(
439
- Object.entries(animateStyles.current).map(([k, v]) => [
440
- k,
441
- animationsState.current!.get(v)?.interpolation || v,
442
- ])
443
- ),
444
- ...animatedTransformStyle,
445
- }
446
-
447
- return {
448
- runners,
449
- completions,
450
- style: [nonAnimatedStyle, animatedStyle],
451
- }
452
-
453
- function update(
454
- key: string,
455
- animated: Animated.Value | undefined,
456
- valIn: string | number
457
- ) {
458
- const isColorStyleKey = colorStyleKey[key]
459
- const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)
460
- let animateToValue = val
461
- const value = animated || new Animated.Value(val)
462
- const curInterpolation = animationsState.current.get(value)
463
-
464
- let interpolateArgs: any
465
- if (type) {
466
- interpolateArgs = getInterpolated(
467
- curInterpolation?.current ?? value['_value'],
468
- val,
469
- type
470
- )
471
- animationsState.current!.set(value, {
472
- interpolation: value.interpolate(interpolateArgs),
473
- current: val,
474
- })
475
- }
476
-
477
- if (isColorStyleKey) {
478
- animateToValue = curInterpolation?.animateToValue ? 0 : 1
479
- interpolateArgs = getColorInterpolated(
480
- curInterpolation?.current as string,
481
- // valIn is the next color
482
- valIn as string,
483
- animateToValue
484
- )
485
- animationsState.current!.set(value, {
486
- current: valIn,
487
- interpolation: value.interpolate(interpolateArgs),
488
- animateToValue: curInterpolation?.animateToValue ? 0 : 1,
489
- })
490
- }
491
-
492
- if (value) {
493
- const animationConfig = getAnimationConfig(
494
- key,
495
- animations,
496
- props.transition,
497
- animationState
498
- )
499
-
500
- let resolve
501
- const promise = new Promise<void>((res) => {
502
- resolve = res
503
- })
504
- completions.push(promise)
505
-
506
- runners.push(() => {
507
- value.stopAnimation()
508
-
509
- function getAnimation() {
510
- return Animated[animationConfig.type || 'spring'](value, {
511
- toValue: animateToValue,
512
- ...animationConfig,
513
- useNativeDriver: useNativeDriverForNode,
514
- })
515
- }
516
-
517
- const animation = animationConfig.delay
518
- ? Animated.sequence([
519
- Animated.delay(animationConfig.delay),
520
- getAnimation(),
521
- ])
522
- : getAnimation()
523
-
524
- animation.start(({ finished }) => {
525
- // always resolve during exit (element is leaving anyway)
526
- // for non-exit, only resolve on successful completion
527
- if (finished || isExiting) {
528
- resolve()
529
- }
530
- })
531
- })
532
- }
533
-
534
- if (process.env.NODE_ENV === 'development') {
535
- if (props['debug'] === 'verbose') {
536
- // prettier-ignore
537
- console.info(
538
- ' 💠 animate',
539
- key,
540
- `from (${value['_value']}) to`,
541
- valIn,
542
- `(${val})`,
543
- 'type',
544
- type,
545
- 'interpolate',
546
- interpolateArgs
547
- )
548
- }
549
- }
550
- return value
551
- }
552
- }, args)
553
-
554
- // track previous exiting state
555
- React.useEffect(() => {
556
- wasExitingRef.current = isExiting
557
- })
558
-
559
- useIsomorphicLayoutEffect(() => {
560
- res.runners.forEach((r) => r())
561
-
562
- // capture current cycle id
563
- const cycleId = exitCycleIdRef.current
564
-
565
- // handle zero-completion case immediately
566
- if (res.completions.length === 0) {
567
- onDidAnimate?.()
568
- if (isExiting && !exitCompletedRef.current) {
569
- exitCompletedRef.current = true
570
- sendExitComplete?.()
571
- }
572
- return
573
- }
574
-
575
- let cancel = false
576
- Promise.all(res.completions).then(() => {
577
- if (cancel) return
578
- // guard against stale cycle completion
579
- if (isExiting && cycleId !== exitCycleIdRef.current) return
580
- if (isExiting && exitCompletedRef.current) return
581
-
582
- onDidAnimate?.()
583
- if (isExiting) {
584
- exitCompletedRef.current = true
585
- sendExitComplete?.()
586
- }
587
- })
588
- return () => {
589
- cancel = true
590
- }
591
- }, args)
592
-
593
- // avoidReRenders: receive style changes imperatively from tamagui
594
- // and update Animated.Values directly without React re-renders
595
- // reuses the same update() + runner pattern as the useMemo path
596
- useStyleEmitter?.((nextStyle, _effectiveTransition, pseudoActive) => {
597
- pseudoActiveRef.current = pseudoActive === true
598
- const runners: Function[] = []
599
- const seenAnimateKeys = new Set<string>()
600
- let transformCount = 0
601
- let animatedShapeChanged = false
602
- // nextStyle is the complete style for this node, so the emitter makes
603
- // the same single driver decision as the render path. include the
604
- // currently rendered graph because its stale keys are not removed until
605
- // the structural-change commit below.
606
- const useNativeDriverForNode =
607
- nativeDriver &&
608
- !hasAnimatedLayoutKey(nextStyle, isDark) &&
609
- !Object.keys(animateStyles.current).some((key) => layoutStyleKey[key])
610
-
611
- for (const key in nextStyle) {
612
- const rawVal = nextStyle[key]
613
- const val = resolveDynamicValue(rawVal, isDark)
614
- if (val === undefined) continue
615
-
616
- if (key === 'transform' && Array.isArray(val)) {
617
- for (const transform of val) {
618
- if (!transform) continue
619
- const index = transformCount++
620
- const tkey = Object.keys(transform)[0]
621
- const currentTransform = animatedTranforms.current[index]?.[tkey]
622
- if (!currentTransform) animatedShapeChanged = true
623
- animatedTranforms.current[index] = {
624
- [tkey]: update(tkey, currentTransform, transform[tkey]),
625
- }
626
- }
627
- } else if (
628
- animatedStyleKey[key] != null ||
629
- costlyToAnimateStyleKey[key] ||
630
- layoutStyleKey[key]
631
- ) {
632
- // layout keys only animate numbers ('auto'/percents are static);
633
- // unparseable themed colors can't be interpolated — skip both and
634
- // let the next render apply them statically
635
- if (layoutStyleKey[key] && typeof val !== 'number') continue
636
- if (colorStyleKey[key] && !isAnimatableColor(val)) continue
637
- if (!animateStyles.current[key]) animatedShapeChanged = true
638
- animateStyles.current[key] = update(key, animateStyles.current[key], val)
639
- seenAnimateKeys.add(key)
640
- }
641
- }
642
-
643
- // the emitter receives a complete style. keep the Animated style graph
644
- // equally complete, including a pseudo release that omits a pseudo-only
645
- // key. React Native needs a commit when that graph's shape changes.
646
- for (const key in animateStyles.current) {
647
- if (!seenAnimateKeys.has(key)) {
648
- delete animateStyles.current[key]
649
- animatedShapeChanged = true
650
- }
651
- }
652
- if (animatedTranforms.current.length > transformCount) {
653
- animatedTranforms.current = animatedTranforms.current.slice(0, transformCount)
654
- animatedShapeChanged = true
655
- }
656
-
657
- // run the queued animations immediately
658
- runners.forEach((r) => r())
659
-
660
- // pseudo state normally stays on the avoidReRenders path. adding or
661
- // removing a style key cannot be expressed by an existing Animated.Value,
662
- // so commit the pending state only for that structural change.
663
- if (animatedShapeChanged && stateRef.current.nextState) {
664
- stateRef.current.baseSetStateShallow?.(stateRef.current.nextState)
665
- }
666
-
667
- function update(
668
- key: string,
669
- animated: Animated.Value | undefined,
670
- valIn: string | number
671
- ) {
672
- const isColor = colorStyleKey[key]
673
- const [numVal, type] = isColor ? [0, undefined] : getValue(valIn)
674
- let animateToValue = numVal
675
- const value = animated || new Animated.Value(numVal)
676
- const curInterpolation = animationsState.current.get(value)
677
-
678
- if (type) {
679
- animationsState.current.set(value, {
680
- interpolation: value.interpolate(
681
- getInterpolated(
682
- curInterpolation?.current ?? value['_value'],
683
- numVal,
684
- type
685
- )
686
- ),
687
- current: numVal,
688
- })
689
- }
690
-
691
- if (isColor) {
692
- animateToValue = curInterpolation?.animateToValue ? 0 : 1
693
- animationsState.current.set(value, {
694
- current: valIn,
695
- interpolation: value.interpolate(
696
- getColorInterpolated(
697
- curInterpolation?.current as string,
698
- valIn as string,
699
- animateToValue
700
- )
701
- ),
702
- animateToValue: curInterpolation?.animateToValue ? 0 : 1,
703
- })
704
- }
705
-
706
- const animationConfig = getAnimationConfig(
707
- key,
708
- animations,
709
- props.transition,
710
- 'default'
711
- )
712
- runners.push(() => {
713
- value.stopAnimation()
714
- const anim = Animated[animationConfig.type || 'spring'](value, {
715
- toValue: animateToValue,
716
- ...animationConfig,
717
- useNativeDriver: useNativeDriverForNode,
718
- })
719
- ;(animationConfig.delay
720
- ? Animated.sequence([Animated.delay(animationConfig.delay), anim])
721
- : anim
722
- ).start()
723
- })
724
-
725
- return value
726
- }
727
- })
728
-
729
- if (process.env.NODE_ENV === 'development') {
730
- if (props['debug'] === 'verbose') {
731
- console.info(`Animated`, { response: res, inputStyle: style, isExiting })
732
- }
733
- }
734
-
735
- return res
736
- },
737
- }
738
- }
739
-
740
- function getColorInterpolated(
741
- currentColor: string | undefined,
742
- nextColor: string,
743
- animateToValue: number
744
- ) {
745
- const inputRange = [0, 1]
746
- const outputRange = [currentColor ? currentColor : nextColor, nextColor]
747
- if (animateToValue === 0) {
748
- // because we are animating from value 1 to 0, we need to put target color at the beginning
749
- outputRange.reverse()
750
- }
751
- return {
752
- inputRange,
753
- outputRange,
754
- }
755
- }
756
-
757
- function getInterpolated(current: number, next: number, postfix = 'deg') {
758
- if (next === current) {
759
- current = next - 0.000000001
760
- }
761
- const inputRange = [current, next]
762
- const outputRange = [`${current}${postfix}`, `${next}${postfix}`]
763
- if (next < current) {
764
- inputRange.reverse()
765
- outputRange.reverse()
766
- }
767
- return {
768
- inputRange,
769
- outputRange,
770
- }
771
- }
772
-
773
- function getAnimationConfig(
774
- key: string,
775
- animations: AnimationsConfig,
776
- transition?: TransitionProp,
777
- animationState: 'enter' | 'exit' | 'default' = 'default'
778
- ): AnimationConfig {
779
- const normalized = normalizeTransition(transition)
780
- const shortKey = transformShorthands[key]
781
-
782
- // Check for property-specific animation
783
- const propAnimation = normalized.properties[key] ?? normalized.properties[shortKey]
784
-
785
- let animationType: string | null = null
786
- let extraConf: any = {}
787
-
788
- if (typeof propAnimation === 'string') {
789
- // Direct animation name: { x: 'quick' }
790
- animationType = propAnimation
791
- } else if (propAnimation && typeof propAnimation === 'object') {
792
- // Config object: { x: { type: 'quick', delay: 100 } }
793
- // Use effective animation based on state if no explicit type in config
794
- animationType =
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 }
805
- }
806
-
807
- const found = animationType ? animations[animationType] : {}
808
- return {
809
- ...found,
810
- // Apply global spring config overrides (from transition={['bouncy', { stiffness: 1000 }]})
811
- ...normalized.config,
812
- // Property-specific config takes highest precedence
813
- ...extraConf,
814
- }
815
- }
816
-
817
- // try both combos
818
- const transformShorthands = {
819
- x: 'translateX',
820
- y: 'translateY',
821
- translateX: 'x',
822
- translateY: 'y',
823
- }
824
-
825
- function getValue(input: number | string, isColor = false) {
826
- if (typeof input !== 'string') {
827
- return [input] as const
828
- }
829
- const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []
830
- return [+number, after] as const
831
- }