@tamagui/animations-react-native 3.0.0-beta.922.1 → 3.0.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.native.cjs +243 -74
- package/dist/cjs/createAnimations.native.js +243 -74
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.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 +242 -73
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/types.native.js +0 -0
- package/package.json +9 -8
- package/src/{createAnimations.tsx → createAnimations.native.tsx} +524 -132
- package/src/index.native.ts +1 -1
- package/src/index.ts +15 -2
- package/src/types.ts +4 -0
- package/types/{createAnimations.d.ts → createAnimations.native.d.ts} +6 -15
- 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 +1 -1
- package/types/index.native.d.ts.map +1 -1
- package/types/types.d.ts +5 -0
- package/types/types.d.ts.map +11 -0
- package/dist/cjs/createAnimations.cjs +0 -617
- package/dist/esm/createAnimations.mjs +0 -584
- package/dist/esm/createAnimations.mjs.map +0 -1
- package/types/createAnimations.d.ts.map +0 -11
|
@@ -1,9 +1,18 @@
|
|
|
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,
|
|
15
|
+
NativeTextMetrics,
|
|
7
16
|
TransitionProp,
|
|
8
17
|
UniversalAnimatedNumber,
|
|
9
18
|
UseAnimatedNumberReaction,
|
|
@@ -12,13 +21,9 @@ import type {
|
|
|
12
21
|
import { useEvent } from '@tamagui/web'
|
|
13
22
|
import { useThemeWithState } from '@tamagui/web/internal-runtime'
|
|
14
23
|
import React from 'react'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type ColorValue,
|
|
19
|
-
type Text,
|
|
20
|
-
type View,
|
|
21
|
-
} from 'react-native'
|
|
24
|
+
import { Animated, Easing, processColor, TextInput, type ColorValue } from 'react-native'
|
|
25
|
+
|
|
26
|
+
import type { CreateAnimationsOptions } from './types'
|
|
22
27
|
|
|
23
28
|
// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys
|
|
24
29
|
const isFabric =
|
|
@@ -33,27 +38,17 @@ const resolveDynamicValue = (value: any, isDark: boolean): any => {
|
|
|
33
38
|
return value
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
| 'speed'
|
|
48
|
-
| 'stiffness'
|
|
49
|
-
| 'tension'
|
|
50
|
-
| 'velocity'
|
|
51
|
-
>
|
|
52
|
-
>
|
|
53
|
-
|
|
54
|
-
type TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>
|
|
55
|
-
|
|
56
|
-
type AnimationConfig = SpringConfig | TimingConfig
|
|
41
|
+
/** what `getAnimationConfig` hands to `Animated.spring` / `Animated.timing` */
|
|
42
|
+
type AnimationConfig =
|
|
43
|
+
| ({ type: 'spring'; delay?: number } & Partial<
|
|
44
|
+
Pick<
|
|
45
|
+
Animated.SpringAnimationConfig,
|
|
46
|
+
'damping' | 'mass' | 'overshootClamping' | 'stiffness' | 'velocity'
|
|
47
|
+
>
|
|
48
|
+
>)
|
|
49
|
+
| ({ type: 'timing'; delay?: number } & Partial<
|
|
50
|
+
Pick<Animated.TimingAnimationConfig, 'duration' | 'easing'>
|
|
51
|
+
>)
|
|
57
52
|
|
|
58
53
|
const animatedStyleKey = {
|
|
59
54
|
transform: true,
|
|
@@ -70,8 +65,10 @@ const colorStyleKey = {
|
|
|
70
65
|
borderBottomColor: true,
|
|
71
66
|
}
|
|
72
67
|
|
|
73
|
-
// layout
|
|
74
|
-
//
|
|
68
|
+
// layout dimensions. the native animated module has no whitelist entry for any
|
|
69
|
+
// of them, so a node animating one runs its whole Animated graph on the JS
|
|
70
|
+
// driver (useNativeDriver:false): Fabric cannot mix native- and JS-driven
|
|
71
|
+
// values on one node.
|
|
75
72
|
const layoutStyleKey = {
|
|
76
73
|
height: true,
|
|
77
74
|
width: true,
|
|
@@ -81,18 +78,67 @@ const layoutStyleKey = {
|
|
|
81
78
|
maxWidth: true,
|
|
82
79
|
}
|
|
83
80
|
|
|
81
|
+
// text metrics have no whitelist entry either, but unlike a height they sit on
|
|
82
|
+
// nearly every Text, so being covered by a broad `transition="medium"` must not
|
|
83
|
+
// be enough to take the node off the native driver: an opacity fade over text
|
|
84
|
+
// whose size is not changing would then run in JS. they join the Animated graph
|
|
85
|
+
// only while they are actually moving, and the node's driver follows them.
|
|
86
|
+
const textMetricStyleKey = {
|
|
87
|
+
fontSize: true,
|
|
88
|
+
lineHeight: true,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const jsDriverStyleKey = { ...layoutStyleKey, ...textMetricStyleKey }
|
|
92
|
+
|
|
84
93
|
function hasAnimatedLayoutKey(
|
|
85
94
|
style: Record<string, any>,
|
|
86
95
|
isDark: boolean,
|
|
87
|
-
|
|
96
|
+
resolved: ResolvedTransition
|
|
88
97
|
) {
|
|
89
98
|
for (const key in layoutStyleKey) {
|
|
90
|
-
if (
|
|
99
|
+
if (!getTransitionForKey(resolved, key)) continue
|
|
91
100
|
if (typeof resolveDynamicValue(style[key], isDark) === 'number') return true
|
|
92
101
|
}
|
|
93
102
|
return false
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
// A numeric authored lineHeight is a RATIO of the resolved font size, not a
|
|
106
|
+
// length. Core finalizes the destination to fontSize * ratio and reports the
|
|
107
|
+
// ratio back on `nativeTextMetrics`, so the painted leading has to be
|
|
108
|
+
// fontSize * ratio on EVERY frame, not only at the destination. This driver
|
|
109
|
+
// gets that exactly by never animating the leading itself: it animates the
|
|
110
|
+
// font size and the ratio, and paints their product as one Animated node.
|
|
111
|
+
// An absolute px leading, and any raw react-native style, keeps its own value
|
|
112
|
+
// and animates as an ordinary numeric key.
|
|
113
|
+
const getLeadingRatio = (metrics: { lineHeight?: unknown } | undefined | null) => {
|
|
114
|
+
const ratio = metrics?.lineHeight
|
|
115
|
+
return typeof ratio === 'number' && Number.isFinite(ratio) && ratio >= 0
|
|
116
|
+
? ratio
|
|
117
|
+
: undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// the product needs both factors as numbers in the style it is painting into.
|
|
121
|
+
const paintsLeadingProduct = (
|
|
122
|
+
style: Record<string, any>,
|
|
123
|
+
ratio: number | undefined
|
|
124
|
+
): ratio is number =>
|
|
125
|
+
ratio !== undefined &&
|
|
126
|
+
typeof style.fontSize === 'number' &&
|
|
127
|
+
typeof style.lineHeight === 'number'
|
|
128
|
+
|
|
129
|
+
// a leading multiplied off an ANCESTOR's live font size: this node has none of
|
|
130
|
+
// its own, core reports the ratio, and react-native inherits the size itself.
|
|
131
|
+
const inheritedFontSizeNode = (
|
|
132
|
+
style: Record<string, any>,
|
|
133
|
+
ratio: number | undefined,
|
|
134
|
+
inheritedText: { fontSize: unknown; driver: string } | null | undefined
|
|
135
|
+
) =>
|
|
136
|
+
ratio !== undefined &&
|
|
137
|
+
typeof style.fontSize !== 'number' &&
|
|
138
|
+
inheritedText?.driver === 'react-native'
|
|
139
|
+
? (inheritedText.fontSize as Animated.Value)
|
|
140
|
+
: undefined
|
|
141
|
+
|
|
96
142
|
// Only colors accepted by RN's own parser can enter interpolation. CSS-wide
|
|
97
143
|
// keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach
|
|
98
144
|
// createInterpolationFromStringOutputRange / mapStringToNumericComponents and
|
|
@@ -116,13 +162,15 @@ const costlyToAnimateStyleKey = {
|
|
|
116
162
|
...colorStyleKey,
|
|
117
163
|
}
|
|
118
164
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
export const AnimatedView: typeof Animated.View = Animated.View
|
|
166
|
+
export const AnimatedText: typeof Animated.Text = Animated.Text
|
|
167
|
+
// a TextInput never inherits font size from an ancestor Text on native, so the
|
|
168
|
+
// binding hook has to hand it a host that accepts animated nodes of its own.
|
|
169
|
+
// built on first use: the compiler evaluates tamagui.config.ts against a
|
|
170
|
+
// react-native stub whose Animated cannot make one, and never binds any text.
|
|
171
|
+
let animatedTextInput:
|
|
172
|
+
| ReturnType<typeof Animated.createAnimatedComponent<typeof TextInput>>
|
|
173
|
+
| undefined
|
|
126
174
|
|
|
127
175
|
export function useAnimatedNumber(
|
|
128
176
|
initial: number
|
|
@@ -161,7 +209,12 @@ export function useAnimatedNumber(
|
|
|
161
209
|
: undefined
|
|
162
210
|
|
|
163
211
|
if (type === 'direct') {
|
|
212
|
+
state.current.composite?.stop()
|
|
213
|
+
state.current.composite = null
|
|
164
214
|
val.setValue(next)
|
|
215
|
+
// a direct set finishes the moment it lands. not calling back stranded
|
|
216
|
+
// everything waiting on it (sheet snap, presence completion).
|
|
217
|
+
onFinish?.()
|
|
165
218
|
} else if (type === 'spring') {
|
|
166
219
|
state.current.composite?.stop()
|
|
167
220
|
const composite = Animated.spring(val, {
|
|
@@ -263,6 +316,33 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
263
316
|
useAnimatedNumbersStyle,
|
|
264
317
|
usePresence,
|
|
265
318
|
ResetPresence,
|
|
319
|
+
|
|
320
|
+
// binds a descendant that has no font size of its own to the ancestor's
|
|
321
|
+
// animated one. no clock here: the font size node is the ancestor's, and a
|
|
322
|
+
// ratio leading is a multiplication of it, so both land on the same frame.
|
|
323
|
+
useTextMetrics: ({ inheritedText, lineHeight }) => {
|
|
324
|
+
const node =
|
|
325
|
+
inheritedText?.driver === 'react-native'
|
|
326
|
+
? (inheritedText.fontSize as Animated.Value)
|
|
327
|
+
: null
|
|
328
|
+
// an absolute or `normal` leading still needs the font size bound, it
|
|
329
|
+
// just keeps the leading the caller already resolved.
|
|
330
|
+
const ratio = typeof lineHeight === 'number' ? lineHeight : null
|
|
331
|
+
const style = React.useMemo(
|
|
332
|
+
() =>
|
|
333
|
+
node
|
|
334
|
+
? ratio === null
|
|
335
|
+
? { fontSize: node }
|
|
336
|
+
: { fontSize: node, lineHeight: Animated.multiply(node, ratio) }
|
|
337
|
+
: null,
|
|
338
|
+
[node, ratio]
|
|
339
|
+
)
|
|
340
|
+
const textChannel = inheritedText ?? null
|
|
341
|
+
if (!style) return { style: null, textChannel }
|
|
342
|
+
animatedTextInput ||= Animated.createAnimatedComponent(TextInput)
|
|
343
|
+
return { style, textChannel, Text: AnimatedText, TextInput: animatedTextInput }
|
|
344
|
+
},
|
|
345
|
+
|
|
266
346
|
useAnimations: ({
|
|
267
347
|
props,
|
|
268
348
|
onTransition,
|
|
@@ -270,7 +350,9 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
270
350
|
componentState,
|
|
271
351
|
presence,
|
|
272
352
|
stateRef,
|
|
353
|
+
styleState,
|
|
273
354
|
useStyleEmitter,
|
|
355
|
+
inheritedText,
|
|
274
356
|
}) => {
|
|
275
357
|
const isDisabled = isWeb && componentState.unmounted === true
|
|
276
358
|
const isExiting = presence?.[0] === false
|
|
@@ -286,12 +368,86 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
286
368
|
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
287
369
|
)
|
|
288
370
|
}
|
|
371
|
+
// createComponent merges a colocated `transition` out of the active
|
|
372
|
+
// pseudo style (`enterStyle={{ opacity: 0, transition: '200ms' }}`), so
|
|
373
|
+
// this is the one that applies right now, not the base prop.
|
|
374
|
+
const effectiveTransition = (styleState?.effectiveTransition ?? props.transition) as
|
|
375
|
+
| TransitionProp
|
|
376
|
+
| null
|
|
377
|
+
| undefined
|
|
378
|
+
|
|
289
379
|
const [, themeState] = useThemeWithState({})
|
|
290
380
|
// Check scheme first, then fall back to checking theme name for 'dark'
|
|
291
381
|
const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
|
|
292
382
|
|
|
293
383
|
/** store Animated value of each key e.g: color: AnimatedValue */
|
|
294
384
|
const animateStyles = React.useRef<Record<string, Animated.Value>>({})
|
|
385
|
+
// a ratio leading animates the RATIO, never the leading, and paints
|
|
386
|
+
// fontSize * ratio as one node. the ratio is not a style key, so it lives
|
|
387
|
+
// outside animateStyles (which the stale-key sweep and the emitter's
|
|
388
|
+
// structural-change check both treat as the rendered style's own keys).
|
|
389
|
+
const leadingRatioValue = React.useRef<Animated.Value | null>(null)
|
|
390
|
+
const leadingRatioRef = React.useRef<number | undefined>(undefined)
|
|
391
|
+
// whether the style React last committed paints the derived product. the
|
|
392
|
+
// emitter can only re-target existing Animated.Values, so a pass that
|
|
393
|
+
// disagrees with this has to force a commit to swap the node itself.
|
|
394
|
+
const paintsDerivedLeading = React.useRef(false)
|
|
395
|
+
// this subtree's live font size, published for descendants that inherit
|
|
396
|
+
// it. the channel's identity IS the node's, so a consumer that keeps the
|
|
397
|
+
// same channel keeps the same derived node.
|
|
398
|
+
const textChannelRef = React.useRef<{ fontSize: unknown; driver: string } | null>(
|
|
399
|
+
null
|
|
400
|
+
)
|
|
401
|
+
// core reports a numeric (ratio) leading on nativeTextMetrics; a px length
|
|
402
|
+
// and 'normal' are reported as themselves and stay independent keys.
|
|
403
|
+
leadingRatioRef.current = getLeadingRatio(styleState?.nativeTextMetrics)
|
|
404
|
+
// the text metrics the last pass painted, so the next one can tell a real
|
|
405
|
+
// change from a value that is simply present. a metric only joins the
|
|
406
|
+
// Animated graph while it is moving (see textMetricStyleKey), which is
|
|
407
|
+
// what keeps an ordinary opacity or transform animation over unchanged
|
|
408
|
+
// text on the native driver.
|
|
409
|
+
const paintedTextRef = React.useRef<{
|
|
410
|
+
fontSize?: unknown
|
|
411
|
+
lineHeight?: unknown
|
|
412
|
+
ratio?: number
|
|
413
|
+
}>({})
|
|
414
|
+
// a value already in the graph keeps moving until it arrives, so a
|
|
415
|
+
// re-render mid-flight never drops it back to a static style and snaps.
|
|
416
|
+
// the first pass has painted nothing yet, so nothing is moving there
|
|
417
|
+
// either: the metrics land as plain numbers and the node mounts on the
|
|
418
|
+
// native driver.
|
|
419
|
+
const movesTextValue = (key: 'fontSize' | 'lineHeight', target: unknown) => {
|
|
420
|
+
if (typeof target !== 'number') return false
|
|
421
|
+
const node = animateStyles.current[key]
|
|
422
|
+
const last = node ? (node['_value'] as unknown) : paintedTextRef.current[key]
|
|
423
|
+
return last !== undefined && last !== target
|
|
424
|
+
}
|
|
425
|
+
const movesLeadingRatio = (ratio: number | undefined) => {
|
|
426
|
+
if (ratio === undefined) return false
|
|
427
|
+
const node = leadingRatioValue.current
|
|
428
|
+
const last = node ? (node['_value'] as number) : paintedTextRef.current.ratio
|
|
429
|
+
return last !== undefined && last !== ratio
|
|
430
|
+
}
|
|
431
|
+
// the font size descendants read as this node's live size. it outlives
|
|
432
|
+
// any one animation: dropping it whenever the size came to rest would
|
|
433
|
+
// swap every descendant between Text and Animated.Text, remounting their
|
|
434
|
+
// subtrees, and a TextInput's focus and contents with them, every time an
|
|
435
|
+
// animation started or finished.
|
|
436
|
+
const fontSizeNode = React.useRef<Animated.Value | null>(null)
|
|
437
|
+
const animatedValueFor = (key: string) => {
|
|
438
|
+
const painted = animateStyles.current[key]
|
|
439
|
+
if (painted) return painted
|
|
440
|
+
if (key === 'fontSize' && fontSizeNode.current) return fontSizeNode.current
|
|
441
|
+
// a metric that was at rest is out of the graph, so the value it
|
|
442
|
+
// rejoins with has to start at what the last pass painted: a fresh
|
|
443
|
+
// Animated.Value starts AT its target, and would snap the metric.
|
|
444
|
+
if (textMetricStyleKey[key])
|
|
445
|
+
return new Animated.Value(paintedTextRef.current[key] as number)
|
|
446
|
+
return undefined
|
|
447
|
+
}
|
|
448
|
+
const leadingRatioValueFrom = () =>
|
|
449
|
+
leadingRatioValue.current ??
|
|
450
|
+
new Animated.Value(paintedTextRef.current.ratio as number)
|
|
295
451
|
const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])
|
|
296
452
|
const animationsState = React.useRef(
|
|
297
453
|
new WeakMap<
|
|
@@ -332,9 +488,6 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
332
488
|
exitCycleIdRef.current++
|
|
333
489
|
}
|
|
334
490
|
|
|
335
|
-
const animateOnly = (props.animateOnly as string[]) || []
|
|
336
|
-
const hasTransitionOnly = !!props.animateOnly
|
|
337
|
-
|
|
338
491
|
// Track if we just finished entering (transition from entering to not entering)
|
|
339
492
|
// must be declared before args array that uses justFinishedEntering
|
|
340
493
|
const isEntering = !!componentState.unmounted
|
|
@@ -346,12 +499,14 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
346
499
|
|
|
347
500
|
const args = [
|
|
348
501
|
JSON.stringify(style),
|
|
502
|
+
JSON.stringify(effectiveTransition),
|
|
349
503
|
componentState,
|
|
350
504
|
isExiting,
|
|
351
505
|
!!onTransition,
|
|
352
506
|
isDark,
|
|
353
507
|
justFinishedEntering,
|
|
354
|
-
|
|
508
|
+
leadingRatioRef.current,
|
|
509
|
+
inheritedText,
|
|
355
510
|
]
|
|
356
511
|
|
|
357
512
|
const res = React.useMemo(() => {
|
|
@@ -366,17 +521,46 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
366
521
|
? 'enter'
|
|
367
522
|
: 'default'
|
|
368
523
|
|
|
524
|
+
// which style keys animate at all is the transition's own decision, so
|
|
525
|
+
// a property list narrows this the same way it narrows css
|
|
526
|
+
const resolved = forAnimationState(
|
|
527
|
+
resolveTransition(effectiveTransition, { animations }),
|
|
528
|
+
animationState
|
|
529
|
+
)
|
|
530
|
+
|
|
369
531
|
const nonAnimatedStyle = {}
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
532
|
+
// the leading is derived only while something can actually move it.
|
|
533
|
+
// when neither key is covered by the transition both fall through to
|
|
534
|
+
// nonAnimatedStyle and land together in one commit, which is already
|
|
535
|
+
// coherent and keeps the node on the native driver.
|
|
536
|
+
const paintedRatio = leadingRatioRef.current
|
|
537
|
+
const paintsProduct = paintsLeadingProduct(style, paintedRatio)
|
|
538
|
+
const movesFontSize = movesTextValue('fontSize', style.fontSize)
|
|
539
|
+
const movesLeading = paintsProduct
|
|
540
|
+
? movesLeadingRatio(paintedRatio)
|
|
541
|
+
: movesTextValue('lineHeight', style.lineHeight)
|
|
542
|
+
// a metric that has arrived leaves the graph here rather than in the
|
|
543
|
+
// sweep at the end of the pass, so the node goes back to the native
|
|
544
|
+
// driver in the same commit that stops moving it. the same three
|
|
545
|
+
// conditions as that sweep: an exit and a latched pseudo both still own
|
|
546
|
+
// the keys they are painting.
|
|
547
|
+
if (!isExiting && !isDisabled && !pseudoActiveRef.current) {
|
|
548
|
+
if (!movesFontSize) delete animateStyles.current.fontSize
|
|
549
|
+
if (!movesLeading) delete animateStyles.current.lineHeight
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// a font size a transition can move is published to descendants whether
|
|
553
|
+
// or not it happens to be moving right now, so their host component
|
|
554
|
+
// does not change under them when it starts.
|
|
555
|
+
if (
|
|
556
|
+
isDisabled ||
|
|
557
|
+
typeof style.fontSize !== 'number' ||
|
|
558
|
+
!getTransitionForKey(resolved, 'fontSize')
|
|
559
|
+
) {
|
|
560
|
+
fontSizeNode.current = null
|
|
561
|
+
} else if (!fontSizeNode.current) {
|
|
562
|
+
fontSizeNode.current = new Animated.Value(style.fontSize)
|
|
563
|
+
}
|
|
380
564
|
|
|
381
565
|
// track which animated keys/transforms the incoming style actually
|
|
382
566
|
// carries this pass, so entries that left the style can be dropped
|
|
@@ -386,6 +570,34 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
386
570
|
let sawTransform = false
|
|
387
571
|
let transformCount = 0
|
|
388
572
|
|
|
573
|
+
const derivesLeading =
|
|
574
|
+
!isDisabled &&
|
|
575
|
+
paintsProduct &&
|
|
576
|
+
(movesFontSize || movesLeading) &&
|
|
577
|
+
(!!getTransitionForKey(resolved, 'fontSize') ||
|
|
578
|
+
!!getTransitionForKey(resolved, 'lineHeight'))
|
|
579
|
+
// an inherited font size: core leaves this node's own fontSize out of
|
|
580
|
+
// the style and reports the ratio, so the leading is the ANCESTOR's
|
|
581
|
+
// live font size times this node's ratio. react-native inherits the
|
|
582
|
+
// size itself, and a length leading is inherited as a length.
|
|
583
|
+
const inheritedFontSize = isDisabled
|
|
584
|
+
? undefined
|
|
585
|
+
: inheritedFontSizeNode(style, paintedRatio, inheritedText)
|
|
586
|
+
|
|
587
|
+
// animatedStyle owns every Animated.Value on the node. Fabric cannot mix
|
|
588
|
+
// native- and JS-driven values inside that shared graph, so one layout
|
|
589
|
+
// animation, one text metric on the move, or a leading multiplied off an
|
|
590
|
+
// ancestor's font size, makes the whole node use the JS driver. a metric
|
|
591
|
+
// another pass left in the graph counts too, or its next animation would
|
|
592
|
+
// start on a driver the node no longer runs on.
|
|
593
|
+
const useNativeDriverForNode =
|
|
594
|
+
nativeDriver &&
|
|
595
|
+
!hasAnimatedLayoutKey(style, isDark, resolved) &&
|
|
596
|
+
!movesFontSize &&
|
|
597
|
+
!movesLeading &&
|
|
598
|
+
!inheritedFontSize &&
|
|
599
|
+
!Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])
|
|
600
|
+
|
|
389
601
|
for (const key in style) {
|
|
390
602
|
const rawVal = style[key]
|
|
391
603
|
// Resolve dynamic theme values from flat theme clauses.
|
|
@@ -396,23 +608,49 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
396
608
|
continue
|
|
397
609
|
}
|
|
398
610
|
|
|
611
|
+
// fontSize and lineHeight are owned by the derived-leading block below
|
|
612
|
+
if (
|
|
613
|
+
(derivesLeading || inheritedFontSize) &&
|
|
614
|
+
(key === 'fontSize' || key === 'lineHeight')
|
|
615
|
+
) {
|
|
616
|
+
// unless the size is the factor at rest: the product multiplies the
|
|
617
|
+
// number it paints, and that number has to be painted
|
|
618
|
+
if (key === 'fontSize' && derivesLeading && !movesFontSize) {
|
|
619
|
+
nonAnimatedStyle[key] = val
|
|
620
|
+
}
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// a text metric at rest stays out of the Animated graph entirely: it
|
|
625
|
+
// paints as a plain style, and the node keeps the native driver
|
|
626
|
+
if (
|
|
627
|
+
textMetricStyleKey[key] &&
|
|
628
|
+
!(key === 'fontSize' ? movesFontSize : movesLeading)
|
|
629
|
+
) {
|
|
630
|
+
nonAnimatedStyle[key] = val
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
|
|
399
634
|
if (
|
|
400
635
|
animatedStyleKey[key] == null &&
|
|
401
636
|
!costlyToAnimateStyleKey[key] &&
|
|
402
|
-
!
|
|
637
|
+
!jsDriverStyleKey[key]
|
|
403
638
|
) {
|
|
404
639
|
nonAnimatedStyle[key] = val
|
|
405
640
|
continue
|
|
406
641
|
}
|
|
407
642
|
|
|
408
|
-
|
|
643
|
+
// `transform` is a container, not a property: its parts each resolve
|
|
644
|
+
// on their own below, and a part no entry covers gets `snapConfig`.
|
|
645
|
+
// the array cannot be split into animated and static halves here.
|
|
646
|
+
if (key !== 'transform' && !getTransitionForKey(resolved, key)) {
|
|
409
647
|
nonAnimatedStyle[key] = val
|
|
410
648
|
continue
|
|
411
649
|
}
|
|
412
650
|
|
|
413
651
|
// layout dimension keys only animate numbers — 'auto' (an open
|
|
414
652
|
// accordion at rest) and percent strings apply as static styles
|
|
415
|
-
if (
|
|
653
|
+
if (jsDriverStyleKey[key] && typeof val !== 'number') {
|
|
416
654
|
nonAnimatedStyle[key] = val
|
|
417
655
|
continue
|
|
418
656
|
}
|
|
@@ -425,7 +663,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
425
663
|
}
|
|
426
664
|
|
|
427
665
|
if (key !== 'transform') {
|
|
428
|
-
animateStyles.current[key] = update(key,
|
|
666
|
+
animateStyles.current[key] = update(key, animatedValueFor(key), val)
|
|
429
667
|
seenAnimateKeys.add(key)
|
|
430
668
|
continue
|
|
431
669
|
}
|
|
@@ -451,6 +689,64 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
451
689
|
}
|
|
452
690
|
}
|
|
453
691
|
|
|
692
|
+
// the derived leading: fontSize and the ratio each animate on their own
|
|
693
|
+
// resolved entry, and the painted leading is their product, so
|
|
694
|
+
// lineHeight is fontSize * ratio on every frame by construction rather
|
|
695
|
+
// than by two solvers happening to agree. a key the transition does not
|
|
696
|
+
// cover gets snapConfig here exactly as it would anywhere else, which
|
|
697
|
+
// makes `transition="fontSize 300ms"` move the leading with the size and
|
|
698
|
+
// `transition="lineHeight 300ms"` move the ratio over a size that has
|
|
699
|
+
// already arrived.
|
|
700
|
+
// the ratio is a factor exactly like the font size: a value of its own
|
|
701
|
+
// while it moves, the number it already paints while it does not
|
|
702
|
+
const ratioFactor = (): Animated.Value | number => {
|
|
703
|
+
if (!movesLeading) {
|
|
704
|
+
leadingRatioValue.current = null
|
|
705
|
+
return paintedRatio!
|
|
706
|
+
}
|
|
707
|
+
leadingRatioValue.current = update(
|
|
708
|
+
'lineHeight',
|
|
709
|
+
leadingRatioValueFrom(),
|
|
710
|
+
paintedRatio!
|
|
711
|
+
)
|
|
712
|
+
return leadingRatioValue.current
|
|
713
|
+
}
|
|
714
|
+
let derivedLeading: ReturnType<typeof Animated.multiply> | undefined
|
|
715
|
+
if (derivesLeading) {
|
|
716
|
+
// a factor at rest multiplies as the plain number it already paints,
|
|
717
|
+
// so the product does not carry a value nothing is moving
|
|
718
|
+
let fontSizeFactor: Animated.Value | number = style.fontSize
|
|
719
|
+
if (movesFontSize) {
|
|
720
|
+
fontSizeFactor = update(
|
|
721
|
+
'fontSize',
|
|
722
|
+
animatedValueFor('fontSize'),
|
|
723
|
+
style.fontSize
|
|
724
|
+
)
|
|
725
|
+
animateStyles.current.fontSize = fontSizeFactor
|
|
726
|
+
seenAnimateKeys.add('fontSize')
|
|
727
|
+
}
|
|
728
|
+
derivedLeading = Animated.multiply(fontSizeFactor, ratioFactor())
|
|
729
|
+
} else if (inheritedFontSize) {
|
|
730
|
+
derivedLeading = Animated.multiply(inheritedFontSize, ratioFactor())
|
|
731
|
+
} else if (!isDisabled) {
|
|
732
|
+
leadingRatioValue.current = null
|
|
733
|
+
}
|
|
734
|
+
paintsDerivedLeading.current = !!derivedLeading
|
|
735
|
+
|
|
736
|
+
// what descendants inherit from this node: its own animated font size
|
|
737
|
+
// when it has one, nothing when it has a font size of its own that is
|
|
738
|
+
// not animating (react-native inherits that statically), and otherwise
|
|
739
|
+
// whatever it inherited itself.
|
|
740
|
+
const ownFontSizeNode = fontSizeNode.current
|
|
741
|
+
const textChannel = ownFontSizeNode
|
|
742
|
+
? textChannelRef.current?.fontSize === ownFontSizeNode
|
|
743
|
+
? textChannelRef.current
|
|
744
|
+
: { fontSize: ownFontSizeNode, driver: 'react-native' }
|
|
745
|
+
: typeof style.fontSize === 'number'
|
|
746
|
+
? null
|
|
747
|
+
: (inheritedText ?? null)
|
|
748
|
+
textChannelRef.current = textChannel
|
|
749
|
+
|
|
454
750
|
// drop stale Animated.Values whose keys left the incoming style, so the
|
|
455
751
|
// key genuinely leaves the rendered style object (a released height goes
|
|
456
752
|
// back to auto instead of staying pinned at its last pixel value). skip
|
|
@@ -487,12 +783,22 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
487
783
|
animationsState.current!.get(v)?.interpolation || v,
|
|
488
784
|
])
|
|
489
785
|
),
|
|
786
|
+
...(derivedLeading ? { lineHeight: derivedLeading } : null),
|
|
490
787
|
...animatedTransformStyle,
|
|
491
788
|
}
|
|
492
789
|
|
|
790
|
+
if (!isDisabled) {
|
|
791
|
+
paintedTextRef.current = {
|
|
792
|
+
fontSize: style.fontSize,
|
|
793
|
+
lineHeight: style.lineHeight,
|
|
794
|
+
ratio: paintedRatio,
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
493
798
|
return {
|
|
494
799
|
runners,
|
|
495
800
|
completions,
|
|
801
|
+
textChannel,
|
|
496
802
|
style: [nonAnimatedStyle, animatedStyle],
|
|
497
803
|
}
|
|
498
804
|
|
|
@@ -539,7 +845,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
539
845
|
const animationConfig = getAnimationConfig(
|
|
540
846
|
key,
|
|
541
847
|
animations,
|
|
542
|
-
|
|
848
|
+
effectiveTransition,
|
|
543
849
|
animationState
|
|
544
850
|
)
|
|
545
851
|
|
|
@@ -552,22 +858,19 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
552
858
|
runners.push(() => {
|
|
553
859
|
value.stopAnimation()
|
|
554
860
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
? Animated.sequence([
|
|
565
|
-
|
|
566
|
-
getAnimation(),
|
|
567
|
-
])
|
|
568
|
-
: getAnimation()
|
|
861
|
+
// `delay` drives the sequence below, so it must not also ride
|
|
862
|
+
// along in the config or every delayed animation waits twice
|
|
863
|
+
const { type, delay, ...config } = animationConfig
|
|
864
|
+
const animation = Animated[type || 'spring'](value, {
|
|
865
|
+
toValue: animateToValue,
|
|
866
|
+
...config,
|
|
867
|
+
useNativeDriver: useNativeDriverForNode,
|
|
868
|
+
})
|
|
869
|
+
const animation2 = delay
|
|
870
|
+
? Animated.sequence([Animated.delay(delay), animation])
|
|
871
|
+
: animation
|
|
569
872
|
|
|
570
|
-
|
|
873
|
+
animation2.start(({ finished }) => {
|
|
571
874
|
// always resolve during exit (element is leaving anyway)
|
|
572
875
|
// for non-exit, only resolve on successful completion
|
|
573
876
|
if (finished || isExiting) {
|
|
@@ -710,26 +1013,72 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
710
1013
|
// avoidReRenders: receive style changes imperatively from tamagui
|
|
711
1014
|
// and update Animated.Values directly without React re-renders
|
|
712
1015
|
// reuses the same update() + runner pattern as the useMemo path
|
|
713
|
-
|
|
1016
|
+
// the fourth argument is the emitted style's own nativeTextMetrics: the
|
|
1017
|
+
// emitter re-runs getSplitStyles outside render, so the ratio that
|
|
1018
|
+
// describes the style it hands over is never the ratio the last render
|
|
1019
|
+
// resolved (a pseudo can replace the leading with an absolute length).
|
|
1020
|
+
const onEmittedStyle = (
|
|
1021
|
+
nextStyle: Record<string, any>,
|
|
1022
|
+
emittedTransition: TransitionProp | null | undefined,
|
|
1023
|
+
pseudoActive?: boolean,
|
|
1024
|
+
nextMetrics?: NativeTextMetrics
|
|
1025
|
+
) => {
|
|
714
1026
|
pseudoActiveRef.current = pseudoActive === true
|
|
715
1027
|
const runners: Function[] = []
|
|
716
1028
|
const seenAnimateKeys = new Set<string>()
|
|
717
1029
|
let transformCount = 0
|
|
718
1030
|
let animatedShapeChanged = false
|
|
1031
|
+
// the emitter runs on a mounted node, so `default` is the state, but
|
|
1032
|
+
// the transition is the one it was handed
|
|
1033
|
+
const emittedResolved = forAnimationState(
|
|
1034
|
+
resolveTransition(emittedTransition ?? effectiveTransition, { animations }),
|
|
1035
|
+
'default'
|
|
1036
|
+
)
|
|
1037
|
+
const emittedRatio = getLeadingRatio(nextMetrics)
|
|
1038
|
+
const emitterPaintsProduct = paintsLeadingProduct(nextStyle, emittedRatio)
|
|
1039
|
+
// the same rule as the render path: a text metric joins the graph only
|
|
1040
|
+
// while it is moving, so a pseudo that only changes an opacity leaves
|
|
1041
|
+
// the node's font size out of it and keeps the native driver
|
|
1042
|
+
const emitterMovesFontSize = movesTextValue('fontSize', nextStyle.fontSize)
|
|
1043
|
+
const emitterMovesLeading = emitterPaintsProduct
|
|
1044
|
+
? movesLeadingRatio(emittedRatio)
|
|
1045
|
+
: movesTextValue('lineHeight', nextStyle.lineHeight)
|
|
719
1046
|
// nextStyle is the complete style for this node, so the emitter makes
|
|
720
1047
|
// the same single driver decision as the render path. include the
|
|
721
1048
|
// currently rendered graph because its stale keys are not removed until
|
|
722
1049
|
// the structural-change commit below.
|
|
723
1050
|
const useNativeDriverForNode =
|
|
724
1051
|
nativeDriver &&
|
|
725
|
-
!hasAnimatedLayoutKey(nextStyle, isDark) &&
|
|
726
|
-
!
|
|
1052
|
+
!hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&
|
|
1053
|
+
!emitterMovesFontSize &&
|
|
1054
|
+
!emitterMovesLeading &&
|
|
1055
|
+
!inheritedFontSizeNode(nextStyle, emittedRatio, inheritedText) &&
|
|
1056
|
+
!Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])
|
|
1057
|
+
|
|
1058
|
+
const emitterDerivesLeading =
|
|
1059
|
+
emitterPaintsProduct &&
|
|
1060
|
+
(emitterMovesFontSize || emitterMovesLeading) &&
|
|
1061
|
+
(!!getTransitionForKey(emittedResolved, 'fontSize') ||
|
|
1062
|
+
!!getTransitionForKey(emittedResolved, 'lineHeight'))
|
|
727
1063
|
|
|
728
1064
|
for (const key in nextStyle) {
|
|
729
1065
|
const rawVal = nextStyle[key]
|
|
730
1066
|
const val = resolveDynamicValue(rawVal, isDark)
|
|
731
1067
|
if (val === undefined) continue
|
|
732
1068
|
|
|
1069
|
+
if (emitterDerivesLeading && (key === 'fontSize' || key === 'lineHeight')) {
|
|
1070
|
+
continue
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// a metric at rest is not the emitter's to animate. it either already
|
|
1074
|
+
// paints as a plain style or the commit below hands it to one.
|
|
1075
|
+
if (
|
|
1076
|
+
textMetricStyleKey[key] &&
|
|
1077
|
+
!(key === 'fontSize' ? emitterMovesFontSize : emitterMovesLeading)
|
|
1078
|
+
) {
|
|
1079
|
+
continue
|
|
1080
|
+
}
|
|
1081
|
+
|
|
733
1082
|
if (key === 'transform' && Array.isArray(val)) {
|
|
734
1083
|
for (const transform of val) {
|
|
735
1084
|
if (!transform) continue
|
|
@@ -744,19 +1093,52 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
744
1093
|
} else if (
|
|
745
1094
|
animatedStyleKey[key] != null ||
|
|
746
1095
|
costlyToAnimateStyleKey[key] ||
|
|
747
|
-
|
|
1096
|
+
jsDriverStyleKey[key]
|
|
748
1097
|
) {
|
|
749
1098
|
// layout keys only animate numbers ('auto'/percents are static);
|
|
750
1099
|
// unparseable themed colors can't be interpolated — skip both and
|
|
751
1100
|
// let the next render apply them statically
|
|
752
|
-
if (
|
|
1101
|
+
if (jsDriverStyleKey[key] && typeof val !== 'number') continue
|
|
753
1102
|
if (colorStyleKey[key] && !isAnimatableColor(val)) continue
|
|
754
1103
|
if (!animateStyles.current[key]) animatedShapeChanged = true
|
|
755
|
-
animateStyles.current[key] = update(key,
|
|
1104
|
+
animateStyles.current[key] = update(key, animatedValueFor(key), val)
|
|
756
1105
|
seenAnimateKeys.add(key)
|
|
757
1106
|
}
|
|
758
1107
|
}
|
|
759
1108
|
|
|
1109
|
+
// the emitter can only re-target Animated.Values the committed style
|
|
1110
|
+
// already paints. re-targeting the font size and the ratio keeps the
|
|
1111
|
+
// product moving without a commit; a pass that disagrees with the
|
|
1112
|
+
// committed style about whether the leading IS a product (a pseudo that
|
|
1113
|
+
// overrides it with an absolute length, or the first pseudo pass on a
|
|
1114
|
+
// node whose render never derived) needs the node swapped, which only a
|
|
1115
|
+
// commit can do.
|
|
1116
|
+
if (emitterDerivesLeading) {
|
|
1117
|
+
if (emitterMovesFontSize) {
|
|
1118
|
+
if (!animateStyles.current.fontSize) animatedShapeChanged = true
|
|
1119
|
+
animateStyles.current.fontSize = update(
|
|
1120
|
+
'fontSize',
|
|
1121
|
+
animatedValueFor('fontSize'),
|
|
1122
|
+
nextStyle.fontSize as number
|
|
1123
|
+
)
|
|
1124
|
+
seenAnimateKeys.add('fontSize')
|
|
1125
|
+
}
|
|
1126
|
+
// a factor at rest is left out: it multiplies as the number the
|
|
1127
|
+
// painted product already carries. giving one a value the committed
|
|
1128
|
+
// style does not paint is the shape change that commits.
|
|
1129
|
+
if (emitterMovesLeading) {
|
|
1130
|
+
if (!leadingRatioValue.current) animatedShapeChanged = true
|
|
1131
|
+
leadingRatioValue.current = update(
|
|
1132
|
+
'lineHeight',
|
|
1133
|
+
leadingRatioValueFrom(),
|
|
1134
|
+
emittedRatio!
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (emitterDerivesLeading !== paintsDerivedLeading.current) {
|
|
1139
|
+
animatedShapeChanged = true
|
|
1140
|
+
}
|
|
1141
|
+
|
|
760
1142
|
// the emitter receives a complete style. keep the Animated style graph
|
|
761
1143
|
// equally complete, including a pseudo release that omits a pseudo-only
|
|
762
1144
|
// key. React Native needs a commit when that graph's shape changes.
|
|
@@ -771,6 +1153,12 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
771
1153
|
animatedShapeChanged = true
|
|
772
1154
|
}
|
|
773
1155
|
|
|
1156
|
+
paintedTextRef.current = {
|
|
1157
|
+
fontSize: nextStyle.fontSize,
|
|
1158
|
+
lineHeight: nextStyle.lineHeight,
|
|
1159
|
+
ratio: emittedRatio,
|
|
1160
|
+
}
|
|
1161
|
+
|
|
774
1162
|
// run the queued animations immediately
|
|
775
1163
|
runners.forEach((r) => r())
|
|
776
1164
|
|
|
@@ -820,28 +1208,29 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
820
1208
|
})
|
|
821
1209
|
}
|
|
822
1210
|
|
|
1211
|
+
// the emitter runs for pseudo-state changes on a mounted node, so
|
|
1212
|
+
// `default` is the state, but the transition is the one it was handed
|
|
823
1213
|
const animationConfig = getAnimationConfig(
|
|
824
1214
|
key,
|
|
825
1215
|
animations,
|
|
826
|
-
|
|
1216
|
+
emittedTransition ?? effectiveTransition,
|
|
827
1217
|
'default'
|
|
828
1218
|
)
|
|
829
1219
|
runners.push(() => {
|
|
830
1220
|
value.stopAnimation()
|
|
831
|
-
const
|
|
1221
|
+
const { type, delay, ...config } = animationConfig
|
|
1222
|
+
const anim = Animated[type || 'spring'](value, {
|
|
832
1223
|
toValue: animateToValue,
|
|
833
|
-
...
|
|
1224
|
+
...config,
|
|
834
1225
|
useNativeDriver: useNativeDriverForNode,
|
|
835
1226
|
})
|
|
836
|
-
;(
|
|
837
|
-
? Animated.sequence([Animated.delay(animationConfig.delay), anim])
|
|
838
|
-
: anim
|
|
839
|
-
).start()
|
|
1227
|
+
;(delay ? Animated.sequence([Animated.delay(delay), anim]) : anim).start()
|
|
840
1228
|
})
|
|
841
1229
|
|
|
842
1230
|
return value
|
|
843
1231
|
}
|
|
844
|
-
}
|
|
1232
|
+
}
|
|
1233
|
+
useStyleEmitter?.(onEmittedStyle)
|
|
845
1234
|
|
|
846
1235
|
if (process.env.NODE_ENV === 'development') {
|
|
847
1236
|
if (props['debug'] === 'verbose') {
|
|
@@ -887,56 +1276,59 @@ function getInterpolated(current: number, next: number, postfix = 'deg') {
|
|
|
887
1276
|
}
|
|
888
1277
|
}
|
|
889
1278
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
propAnimation.type || getEffectiveAnimation(normalized, animationState)
|
|
913
|
-
extraConf = propAnimation
|
|
914
|
-
} else {
|
|
915
|
-
// Fall back to effective animation based on state (enter/exit/default)
|
|
916
|
-
animationType = getEffectiveAnimation(normalized, animationState)
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Apply global delay if no property-specific delay
|
|
920
|
-
if (normalized.delay && !extraConf.delay) {
|
|
921
|
-
extraConf = { ...extraConf, delay: normalized.delay }
|
|
1279
|
+
/**
|
|
1280
|
+
* one resolved entry as a react-native Animated config.
|
|
1281
|
+
*
|
|
1282
|
+
* springs go in as stiffness/damping/mass, which is the parameterization RN
|
|
1283
|
+
* actually integrates. `bounciness`/`speed` and `tension`/`friction` are older
|
|
1284
|
+
* spellings of the same two numbers, so nothing is lost by not using them.
|
|
1285
|
+
*/
|
|
1286
|
+
function entryToRN(entry: ResolvedEntry): AnimationConfig {
|
|
1287
|
+
const extra = entry.timing.kind === 'spring' ? entry.timing.extra : undefined
|
|
1288
|
+
|
|
1289
|
+
if (entry.timing.kind === 'spring') {
|
|
1290
|
+
return {
|
|
1291
|
+
type: 'spring',
|
|
1292
|
+
stiffness: entry.timing.stiffness,
|
|
1293
|
+
damping: entry.timing.damping,
|
|
1294
|
+
mass: entry.timing.mass,
|
|
1295
|
+
...(typeof extra?.velocity === 'number' ? { velocity: extra.velocity } : null),
|
|
1296
|
+
...(typeof extra?.overshootClamping === 'boolean'
|
|
1297
|
+
? { overshootClamping: extra.overshootClamping }
|
|
1298
|
+
: null),
|
|
1299
|
+
...(entry.delayMs ? { delay: entry.delayMs } : null),
|
|
1300
|
+
}
|
|
922
1301
|
}
|
|
923
1302
|
|
|
924
|
-
const
|
|
1303
|
+
const bezier = easingToBezier(entry.timing.easing)
|
|
925
1304
|
return {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
//
|
|
930
|
-
...
|
|
1305
|
+
type: 'timing',
|
|
1306
|
+
duration: entry.timing.durationMs,
|
|
1307
|
+
// `linear()` and `steps()` have no bezier equivalent; RN's default easing
|
|
1308
|
+
// is the honest answer rather than a curve we made up
|
|
1309
|
+
...(bezier
|
|
1310
|
+
? { easing: Easing.bezier(bezier[0], bezier[1], bezier[2], bezier[3]) }
|
|
1311
|
+
: null),
|
|
1312
|
+
...(entry.delayMs ? { delay: entry.delayMs } : null),
|
|
931
1313
|
}
|
|
932
1314
|
}
|
|
933
1315
|
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1316
|
+
// a key the transition does not cover does not animate. snapping is what css
|
|
1317
|
+
// does for an unlisted property, so the drivers have to agree on it too.
|
|
1318
|
+
const snapConfig: AnimationConfig = { type: 'timing', duration: 0 }
|
|
1319
|
+
|
|
1320
|
+
function getAnimationConfig(
|
|
1321
|
+
key: string,
|
|
1322
|
+
animations: AnimationsConfig,
|
|
1323
|
+
transition?: TransitionProp | null,
|
|
1324
|
+
animationState: 'enter' | 'exit' | 'default' = 'default'
|
|
1325
|
+
): AnimationConfig {
|
|
1326
|
+
const resolved = forAnimationState(
|
|
1327
|
+
resolveTransition(transition, { animations }),
|
|
1328
|
+
animationState
|
|
1329
|
+
)
|
|
1330
|
+
const entry = getTransitionForKey(resolved, key)
|
|
1331
|
+
return entry ? entryToRN(entry) : snapConfig
|
|
940
1332
|
}
|
|
941
1333
|
|
|
942
1334
|
function getValue(input: number | string, isColor = false) {
|