@tamagui/animations-react-native 3.0.0-beta.1312.1 → 3.0.0-beta.1341.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 +184 -13
- package/dist/cjs/createAnimations.native.js +184 -13
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/esm/createAnimations.native.js +185 -14
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/package.json +6 -6
- package/src/createAnimations.native.tsx +396 -16
- package/types/createAnimations.native.d.ts.map +2 -2
|
@@ -12,6 +12,7 @@ import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
|
12
12
|
import type {
|
|
13
13
|
AnimatedNumberStrategy,
|
|
14
14
|
AnimationDriverWithAnimatedNumbers,
|
|
15
|
+
NativeTextMetrics,
|
|
15
16
|
TransitionProp,
|
|
16
17
|
UniversalAnimatedNumber,
|
|
17
18
|
UseAnimatedNumberReaction,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
Animated,
|
|
25
26
|
Easing,
|
|
26
27
|
processColor,
|
|
28
|
+
TextInput,
|
|
27
29
|
type ColorValue,
|
|
28
30
|
type Text,
|
|
29
31
|
type View,
|
|
@@ -71,8 +73,10 @@ const colorStyleKey = {
|
|
|
71
73
|
borderBottomColor: true,
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// layout
|
|
75
|
-
//
|
|
76
|
+
// layout dimensions. the native animated module has no whitelist entry for any
|
|
77
|
+
// of them, so a node animating one runs its whole Animated graph on the JS
|
|
78
|
+
// driver (useNativeDriver:false): Fabric cannot mix native- and JS-driven
|
|
79
|
+
// values on one node.
|
|
76
80
|
const layoutStyleKey = {
|
|
77
81
|
height: true,
|
|
78
82
|
width: true,
|
|
@@ -82,6 +86,18 @@ const layoutStyleKey = {
|
|
|
82
86
|
maxWidth: true,
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
// text metrics have no whitelist entry either, but unlike a height they sit on
|
|
90
|
+
// nearly every Text, so being covered by a broad `transition="medium"` must not
|
|
91
|
+
// be enough to take the node off the native driver: an opacity fade over text
|
|
92
|
+
// whose size is not changing would then run in JS. they join the Animated graph
|
|
93
|
+
// only while they are actually moving, and the node's driver follows them.
|
|
94
|
+
const textMetricStyleKey = {
|
|
95
|
+
fontSize: true,
|
|
96
|
+
lineHeight: true,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const jsDriverStyleKey = { ...layoutStyleKey, ...textMetricStyleKey }
|
|
100
|
+
|
|
85
101
|
function hasAnimatedLayoutKey(
|
|
86
102
|
style: Record<string, any>,
|
|
87
103
|
isDark: boolean,
|
|
@@ -94,6 +110,43 @@ function hasAnimatedLayoutKey(
|
|
|
94
110
|
return false
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
// A numeric authored lineHeight is a RATIO of the resolved font size, not a
|
|
114
|
+
// length. Core finalizes the destination to fontSize * ratio and reports the
|
|
115
|
+
// ratio back on `nativeTextMetrics`, so the painted leading has to be
|
|
116
|
+
// fontSize * ratio on EVERY frame, not only at the destination. This driver
|
|
117
|
+
// gets that exactly by never animating the leading itself: it animates the
|
|
118
|
+
// font size and the ratio, and paints their product as one Animated node.
|
|
119
|
+
// An absolute px leading, and any raw react-native style, keeps its own value
|
|
120
|
+
// and animates as an ordinary numeric key.
|
|
121
|
+
const getLeadingRatio = (metrics: { lineHeight?: unknown } | undefined | null) => {
|
|
122
|
+
const ratio = metrics?.lineHeight
|
|
123
|
+
return typeof ratio === 'number' && Number.isFinite(ratio) && ratio >= 0
|
|
124
|
+
? ratio
|
|
125
|
+
: undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// the product needs both factors as numbers in the style it is painting into.
|
|
129
|
+
const paintsLeadingProduct = (
|
|
130
|
+
style: Record<string, any>,
|
|
131
|
+
ratio: number | undefined
|
|
132
|
+
): ratio is number =>
|
|
133
|
+
ratio !== undefined &&
|
|
134
|
+
typeof style.fontSize === 'number' &&
|
|
135
|
+
typeof style.lineHeight === 'number'
|
|
136
|
+
|
|
137
|
+
// a leading multiplied off an ANCESTOR's live font size: this node has none of
|
|
138
|
+
// its own, core reports the ratio, and react-native inherits the size itself.
|
|
139
|
+
const inheritedFontSizeNode = (
|
|
140
|
+
style: Record<string, any>,
|
|
141
|
+
ratio: number | undefined,
|
|
142
|
+
inheritedText: { fontSize: unknown; driver: string } | null | undefined
|
|
143
|
+
) =>
|
|
144
|
+
ratio !== undefined &&
|
|
145
|
+
typeof style.fontSize !== 'number' &&
|
|
146
|
+
inheritedText?.driver === 'react-native'
|
|
147
|
+
? (inheritedText.fontSize as Animated.Value)
|
|
148
|
+
: undefined
|
|
149
|
+
|
|
97
150
|
// Only colors accepted by RN's own parser can enter interpolation. CSS-wide
|
|
98
151
|
// keywords, unresolved tokens, var()/calc(), and empty strings otherwise reach
|
|
99
152
|
// createInterpolationFromStringOutputRange / mapStringToNumericComponents and
|
|
@@ -119,6 +172,11 @@ const costlyToAnimateStyleKey = {
|
|
|
119
172
|
|
|
120
173
|
export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
|
|
121
174
|
export const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text
|
|
175
|
+
// a TextInput never inherits font size from an ancestor Text on native, so the
|
|
176
|
+
// binding hook has to hand it a host that accepts animated nodes of its own.
|
|
177
|
+
// built on first use: the compiler evaluates tamagui.config.ts against a
|
|
178
|
+
// react-native stub whose Animated cannot make one, and never binds any text.
|
|
179
|
+
let animatedTextInput: Animated.AnimatedComponent<typeof TextInput> | undefined
|
|
122
180
|
|
|
123
181
|
export function useAnimatedNumber(
|
|
124
182
|
initial: number
|
|
@@ -264,6 +322,33 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
264
322
|
useAnimatedNumbersStyle,
|
|
265
323
|
usePresence,
|
|
266
324
|
ResetPresence,
|
|
325
|
+
|
|
326
|
+
// binds a descendant that has no font size of its own to the ancestor's
|
|
327
|
+
// animated one. no clock here: the font size node is the ancestor's, and a
|
|
328
|
+
// ratio leading is a multiplication of it, so both land on the same frame.
|
|
329
|
+
useTextMetrics: ({ inheritedText, lineHeight }) => {
|
|
330
|
+
const node =
|
|
331
|
+
inheritedText?.driver === 'react-native'
|
|
332
|
+
? (inheritedText.fontSize as Animated.Value)
|
|
333
|
+
: null
|
|
334
|
+
// an absolute or `normal` leading still needs the font size bound, it
|
|
335
|
+
// just keeps the leading the caller already resolved.
|
|
336
|
+
const ratio = typeof lineHeight === 'number' ? lineHeight : null
|
|
337
|
+
const style = React.useMemo(
|
|
338
|
+
() =>
|
|
339
|
+
node
|
|
340
|
+
? ratio === null
|
|
341
|
+
? { fontSize: node }
|
|
342
|
+
: { fontSize: node, lineHeight: Animated.multiply(node, ratio) }
|
|
343
|
+
: null,
|
|
344
|
+
[node, ratio]
|
|
345
|
+
)
|
|
346
|
+
const textChannel = inheritedText ?? null
|
|
347
|
+
if (!style) return { style: null, textChannel }
|
|
348
|
+
animatedTextInput ||= Animated.createAnimatedComponent(TextInput)
|
|
349
|
+
return { style, textChannel, Text: AnimatedText, TextInput: animatedTextInput }
|
|
350
|
+
},
|
|
351
|
+
|
|
267
352
|
useAnimations: ({
|
|
268
353
|
props,
|
|
269
354
|
onTransition,
|
|
@@ -273,6 +358,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
273
358
|
stateRef,
|
|
274
359
|
styleState,
|
|
275
360
|
useStyleEmitter,
|
|
361
|
+
inheritedText,
|
|
276
362
|
}) => {
|
|
277
363
|
const isDisabled = isWeb && componentState.unmounted === true
|
|
278
364
|
const isExiting = presence?.[0] === false
|
|
@@ -302,6 +388,72 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
302
388
|
|
|
303
389
|
/** store Animated value of each key e.g: color: AnimatedValue */
|
|
304
390
|
const animateStyles = React.useRef<Record<string, Animated.Value>>({})
|
|
391
|
+
// a ratio leading animates the RATIO, never the leading, and paints
|
|
392
|
+
// fontSize * ratio as one node. the ratio is not a style key, so it lives
|
|
393
|
+
// outside animateStyles (which the stale-key sweep and the emitter's
|
|
394
|
+
// structural-change check both treat as the rendered style's own keys).
|
|
395
|
+
const leadingRatioValue = React.useRef<Animated.Value | null>(null)
|
|
396
|
+
const leadingRatioRef = React.useRef<number | undefined>(undefined)
|
|
397
|
+
// whether the style React last committed paints the derived product. the
|
|
398
|
+
// emitter can only re-target existing Animated.Values, so a pass that
|
|
399
|
+
// disagrees with this has to force a commit to swap the node itself.
|
|
400
|
+
const paintsDerivedLeading = React.useRef(false)
|
|
401
|
+
// this subtree's live font size, published for descendants that inherit
|
|
402
|
+
// it. the channel's identity IS the node's, so a consumer that keeps the
|
|
403
|
+
// same channel keeps the same derived node.
|
|
404
|
+
const textChannelRef = React.useRef<{ fontSize: unknown; driver: string } | null>(
|
|
405
|
+
null
|
|
406
|
+
)
|
|
407
|
+
// core reports a numeric (ratio) leading on nativeTextMetrics; a px length
|
|
408
|
+
// and 'normal' are reported as themselves and stay independent keys.
|
|
409
|
+
leadingRatioRef.current = getLeadingRatio(styleState?.nativeTextMetrics)
|
|
410
|
+
// the text metrics the last pass painted, so the next one can tell a real
|
|
411
|
+
// change from a value that is simply present. a metric only joins the
|
|
412
|
+
// Animated graph while it is moving (see textMetricStyleKey), which is
|
|
413
|
+
// what keeps an ordinary opacity or transform animation over unchanged
|
|
414
|
+
// text on the native driver.
|
|
415
|
+
const paintedTextRef = React.useRef<{
|
|
416
|
+
fontSize?: unknown
|
|
417
|
+
lineHeight?: unknown
|
|
418
|
+
ratio?: number
|
|
419
|
+
}>({})
|
|
420
|
+
// a value already in the graph keeps moving until it arrives, so a
|
|
421
|
+
// re-render mid-flight never drops it back to a static style and snaps.
|
|
422
|
+
// the first pass has painted nothing yet, so nothing is moving there
|
|
423
|
+
// either: the metrics land as plain numbers and the node mounts on the
|
|
424
|
+
// native driver.
|
|
425
|
+
const movesTextValue = (key: 'fontSize' | 'lineHeight', target: unknown) => {
|
|
426
|
+
if (typeof target !== 'number') return false
|
|
427
|
+
const node = animateStyles.current[key]
|
|
428
|
+
const last = node ? (node['_value'] as unknown) : paintedTextRef.current[key]
|
|
429
|
+
return last !== undefined && last !== target
|
|
430
|
+
}
|
|
431
|
+
const movesLeadingRatio = (ratio: number | undefined) => {
|
|
432
|
+
if (ratio === undefined) return false
|
|
433
|
+
const node = leadingRatioValue.current
|
|
434
|
+
const last = node ? (node['_value'] as number) : paintedTextRef.current.ratio
|
|
435
|
+
return last !== undefined && last !== ratio
|
|
436
|
+
}
|
|
437
|
+
// the font size descendants read as this node's live size. it outlives
|
|
438
|
+
// any one animation: dropping it whenever the size came to rest would
|
|
439
|
+
// swap every descendant between Text and Animated.Text, remounting their
|
|
440
|
+
// subtrees, and a TextInput's focus and contents with them, every time an
|
|
441
|
+
// animation started or finished.
|
|
442
|
+
const fontSizeNode = React.useRef<Animated.Value | null>(null)
|
|
443
|
+
const animatedValueFor = (key: string) => {
|
|
444
|
+
const painted = animateStyles.current[key]
|
|
445
|
+
if (painted) return painted
|
|
446
|
+
if (key === 'fontSize' && fontSizeNode.current) return fontSizeNode.current
|
|
447
|
+
// a metric that was at rest is out of the graph, so the value it
|
|
448
|
+
// rejoins with has to start at what the last pass painted: a fresh
|
|
449
|
+
// Animated.Value starts AT its target, and would snap the metric.
|
|
450
|
+
if (textMetricStyleKey[key])
|
|
451
|
+
return new Animated.Value(paintedTextRef.current[key] as number)
|
|
452
|
+
return undefined
|
|
453
|
+
}
|
|
454
|
+
const leadingRatioValueFrom = () =>
|
|
455
|
+
leadingRatioValue.current ??
|
|
456
|
+
new Animated.Value(paintedTextRef.current.ratio as number)
|
|
305
457
|
const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])
|
|
306
458
|
const animationsState = React.useRef(
|
|
307
459
|
new WeakMap<
|
|
@@ -359,6 +511,8 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
359
511
|
!!onTransition,
|
|
360
512
|
isDark,
|
|
361
513
|
justFinishedEntering,
|
|
514
|
+
leadingRatioRef.current,
|
|
515
|
+
inheritedText,
|
|
362
516
|
]
|
|
363
517
|
|
|
364
518
|
const res = React.useMemo(() => {
|
|
@@ -381,11 +535,38 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
381
535
|
)
|
|
382
536
|
|
|
383
537
|
const nonAnimatedStyle = {}
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
538
|
+
// the leading is derived only while something can actually move it.
|
|
539
|
+
// when neither key is covered by the transition both fall through to
|
|
540
|
+
// nonAnimatedStyle and land together in one commit, which is already
|
|
541
|
+
// coherent and keeps the node on the native driver.
|
|
542
|
+
const paintedRatio = leadingRatioRef.current
|
|
543
|
+
const paintsProduct = paintsLeadingProduct(style, paintedRatio)
|
|
544
|
+
const movesFontSize = movesTextValue('fontSize', style.fontSize)
|
|
545
|
+
const movesLeading = paintsProduct
|
|
546
|
+
? movesLeadingRatio(paintedRatio)
|
|
547
|
+
: movesTextValue('lineHeight', style.lineHeight)
|
|
548
|
+
// a metric that has arrived leaves the graph here rather than in the
|
|
549
|
+
// sweep at the end of the pass, so the node goes back to the native
|
|
550
|
+
// driver in the same commit that stops moving it. the same three
|
|
551
|
+
// conditions as that sweep: an exit and a latched pseudo both still own
|
|
552
|
+
// the keys they are painting.
|
|
553
|
+
if (!isExiting && !isDisabled && !pseudoActiveRef.current) {
|
|
554
|
+
if (!movesFontSize) delete animateStyles.current.fontSize
|
|
555
|
+
if (!movesLeading) delete animateStyles.current.lineHeight
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// a font size a transition can move is published to descendants whether
|
|
559
|
+
// or not it happens to be moving right now, so their host component
|
|
560
|
+
// does not change under them when it starts.
|
|
561
|
+
if (
|
|
562
|
+
isDisabled ||
|
|
563
|
+
typeof style.fontSize !== 'number' ||
|
|
564
|
+
!getTransitionForKey(resolved, 'fontSize')
|
|
565
|
+
) {
|
|
566
|
+
fontSizeNode.current = null
|
|
567
|
+
} else if (!fontSizeNode.current) {
|
|
568
|
+
fontSizeNode.current = new Animated.Value(style.fontSize)
|
|
569
|
+
}
|
|
389
570
|
|
|
390
571
|
// track which animated keys/transforms the incoming style actually
|
|
391
572
|
// carries this pass, so entries that left the style can be dropped
|
|
@@ -395,6 +576,34 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
395
576
|
let sawTransform = false
|
|
396
577
|
let transformCount = 0
|
|
397
578
|
|
|
579
|
+
const derivesLeading =
|
|
580
|
+
!isDisabled &&
|
|
581
|
+
paintsProduct &&
|
|
582
|
+
(movesFontSize || movesLeading) &&
|
|
583
|
+
(!!getTransitionForKey(resolved, 'fontSize') ||
|
|
584
|
+
!!getTransitionForKey(resolved, 'lineHeight'))
|
|
585
|
+
// an inherited font size: core leaves this node's own fontSize out of
|
|
586
|
+
// the style and reports the ratio, so the leading is the ANCESTOR's
|
|
587
|
+
// live font size times this node's ratio. react-native inherits the
|
|
588
|
+
// size itself, and a length leading is inherited as a length.
|
|
589
|
+
const inheritedFontSize = isDisabled
|
|
590
|
+
? undefined
|
|
591
|
+
: inheritedFontSizeNode(style, paintedRatio, inheritedText)
|
|
592
|
+
|
|
593
|
+
// animatedStyle owns every Animated.Value on the node. Fabric cannot mix
|
|
594
|
+
// native- and JS-driven values inside that shared graph, so one layout
|
|
595
|
+
// animation, one text metric on the move, or a leading multiplied off an
|
|
596
|
+
// ancestor's font size, makes the whole node use the JS driver. a metric
|
|
597
|
+
// another pass left in the graph counts too, or its next animation would
|
|
598
|
+
// start on a driver the node no longer runs on.
|
|
599
|
+
const useNativeDriverForNode =
|
|
600
|
+
nativeDriver &&
|
|
601
|
+
!hasAnimatedLayoutKey(style, isDark, resolved) &&
|
|
602
|
+
!movesFontSize &&
|
|
603
|
+
!movesLeading &&
|
|
604
|
+
!inheritedFontSize &&
|
|
605
|
+
!Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])
|
|
606
|
+
|
|
398
607
|
for (const key in style) {
|
|
399
608
|
const rawVal = style[key]
|
|
400
609
|
// Resolve dynamic theme values from flat theme clauses.
|
|
@@ -405,10 +614,33 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
405
614
|
continue
|
|
406
615
|
}
|
|
407
616
|
|
|
617
|
+
// fontSize and lineHeight are owned by the derived-leading block below
|
|
618
|
+
if (
|
|
619
|
+
(derivesLeading || inheritedFontSize) &&
|
|
620
|
+
(key === 'fontSize' || key === 'lineHeight')
|
|
621
|
+
) {
|
|
622
|
+
// unless the size is the factor at rest: the product multiplies the
|
|
623
|
+
// number it paints, and that number has to be painted
|
|
624
|
+
if (key === 'fontSize' && derivesLeading && !movesFontSize) {
|
|
625
|
+
nonAnimatedStyle[key] = val
|
|
626
|
+
}
|
|
627
|
+
continue
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// a text metric at rest stays out of the Animated graph entirely: it
|
|
631
|
+
// paints as a plain style, and the node keeps the native driver
|
|
632
|
+
if (
|
|
633
|
+
textMetricStyleKey[key] &&
|
|
634
|
+
!(key === 'fontSize' ? movesFontSize : movesLeading)
|
|
635
|
+
) {
|
|
636
|
+
nonAnimatedStyle[key] = val
|
|
637
|
+
continue
|
|
638
|
+
}
|
|
639
|
+
|
|
408
640
|
if (
|
|
409
641
|
animatedStyleKey[key] == null &&
|
|
410
642
|
!costlyToAnimateStyleKey[key] &&
|
|
411
|
-
!
|
|
643
|
+
!jsDriverStyleKey[key]
|
|
412
644
|
) {
|
|
413
645
|
nonAnimatedStyle[key] = val
|
|
414
646
|
continue
|
|
@@ -424,7 +656,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
424
656
|
|
|
425
657
|
// layout dimension keys only animate numbers — 'auto' (an open
|
|
426
658
|
// accordion at rest) and percent strings apply as static styles
|
|
427
|
-
if (
|
|
659
|
+
if (jsDriverStyleKey[key] && typeof val !== 'number') {
|
|
428
660
|
nonAnimatedStyle[key] = val
|
|
429
661
|
continue
|
|
430
662
|
}
|
|
@@ -437,7 +669,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
437
669
|
}
|
|
438
670
|
|
|
439
671
|
if (key !== 'transform') {
|
|
440
|
-
animateStyles.current[key] = update(key,
|
|
672
|
+
animateStyles.current[key] = update(key, animatedValueFor(key), val)
|
|
441
673
|
seenAnimateKeys.add(key)
|
|
442
674
|
continue
|
|
443
675
|
}
|
|
@@ -463,6 +695,64 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
463
695
|
}
|
|
464
696
|
}
|
|
465
697
|
|
|
698
|
+
// the derived leading: fontSize and the ratio each animate on their own
|
|
699
|
+
// resolved entry, and the painted leading is their product, so
|
|
700
|
+
// lineHeight is fontSize * ratio on every frame by construction rather
|
|
701
|
+
// than by two solvers happening to agree. a key the transition does not
|
|
702
|
+
// cover gets snapConfig here exactly as it would anywhere else, which
|
|
703
|
+
// makes `transition="fontSize 300ms"` move the leading with the size and
|
|
704
|
+
// `transition="lineHeight 300ms"` move the ratio over a size that has
|
|
705
|
+
// already arrived.
|
|
706
|
+
// the ratio is a factor exactly like the font size: a value of its own
|
|
707
|
+
// while it moves, the number it already paints while it does not
|
|
708
|
+
const ratioFactor = (): Animated.Value | number => {
|
|
709
|
+
if (!movesLeading) {
|
|
710
|
+
leadingRatioValue.current = null
|
|
711
|
+
return paintedRatio!
|
|
712
|
+
}
|
|
713
|
+
leadingRatioValue.current = update(
|
|
714
|
+
'lineHeight',
|
|
715
|
+
leadingRatioValueFrom(),
|
|
716
|
+
paintedRatio!
|
|
717
|
+
)
|
|
718
|
+
return leadingRatioValue.current
|
|
719
|
+
}
|
|
720
|
+
let derivedLeading: ReturnType<typeof Animated.multiply> | undefined
|
|
721
|
+
if (derivesLeading) {
|
|
722
|
+
// a factor at rest multiplies as the plain number it already paints,
|
|
723
|
+
// so the product does not carry a value nothing is moving
|
|
724
|
+
let fontSizeFactor: Animated.Value | number = style.fontSize
|
|
725
|
+
if (movesFontSize) {
|
|
726
|
+
fontSizeFactor = update(
|
|
727
|
+
'fontSize',
|
|
728
|
+
animatedValueFor('fontSize'),
|
|
729
|
+
style.fontSize
|
|
730
|
+
)
|
|
731
|
+
animateStyles.current.fontSize = fontSizeFactor
|
|
732
|
+
seenAnimateKeys.add('fontSize')
|
|
733
|
+
}
|
|
734
|
+
derivedLeading = Animated.multiply(fontSizeFactor, ratioFactor())
|
|
735
|
+
} else if (inheritedFontSize) {
|
|
736
|
+
derivedLeading = Animated.multiply(inheritedFontSize, ratioFactor())
|
|
737
|
+
} else if (!isDisabled) {
|
|
738
|
+
leadingRatioValue.current = null
|
|
739
|
+
}
|
|
740
|
+
paintsDerivedLeading.current = !!derivedLeading
|
|
741
|
+
|
|
742
|
+
// what descendants inherit from this node: its own animated font size
|
|
743
|
+
// when it has one, nothing when it has a font size of its own that is
|
|
744
|
+
// not animating (react-native inherits that statically), and otherwise
|
|
745
|
+
// whatever it inherited itself.
|
|
746
|
+
const ownFontSizeNode = fontSizeNode.current
|
|
747
|
+
const textChannel = ownFontSizeNode
|
|
748
|
+
? textChannelRef.current?.fontSize === ownFontSizeNode
|
|
749
|
+
? textChannelRef.current
|
|
750
|
+
: { fontSize: ownFontSizeNode, driver: 'react-native' }
|
|
751
|
+
: typeof style.fontSize === 'number'
|
|
752
|
+
? null
|
|
753
|
+
: (inheritedText ?? null)
|
|
754
|
+
textChannelRef.current = textChannel
|
|
755
|
+
|
|
466
756
|
// drop stale Animated.Values whose keys left the incoming style, so the
|
|
467
757
|
// key genuinely leaves the rendered style object (a released height goes
|
|
468
758
|
// back to auto instead of staying pinned at its last pixel value). skip
|
|
@@ -499,12 +789,22 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
499
789
|
animationsState.current!.get(v)?.interpolation || v,
|
|
500
790
|
])
|
|
501
791
|
),
|
|
792
|
+
...(derivedLeading ? { lineHeight: derivedLeading } : null),
|
|
502
793
|
...animatedTransformStyle,
|
|
503
794
|
}
|
|
504
795
|
|
|
796
|
+
if (!isDisabled) {
|
|
797
|
+
paintedTextRef.current = {
|
|
798
|
+
fontSize: style.fontSize,
|
|
799
|
+
lineHeight: style.lineHeight,
|
|
800
|
+
ratio: paintedRatio,
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
505
804
|
return {
|
|
506
805
|
runners,
|
|
507
806
|
completions,
|
|
807
|
+
textChannel,
|
|
508
808
|
style: [nonAnimatedStyle, animatedStyle],
|
|
509
809
|
}
|
|
510
810
|
|
|
@@ -719,7 +1019,16 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
719
1019
|
// avoidReRenders: receive style changes imperatively from tamagui
|
|
720
1020
|
// and update Animated.Values directly without React re-renders
|
|
721
1021
|
// reuses the same update() + runner pattern as the useMemo path
|
|
722
|
-
|
|
1022
|
+
// the fourth argument is the emitted style's own nativeTextMetrics: the
|
|
1023
|
+
// emitter re-runs getSplitStyles outside render, so the ratio that
|
|
1024
|
+
// describes the style it hands over is never the ratio the last render
|
|
1025
|
+
// resolved (a pseudo can replace the leading with an absolute length).
|
|
1026
|
+
const onEmittedStyle = (
|
|
1027
|
+
nextStyle: Record<string, any>,
|
|
1028
|
+
emittedTransition: TransitionProp | null | undefined,
|
|
1029
|
+
pseudoActive?: boolean,
|
|
1030
|
+
nextMetrics?: NativeTextMetrics
|
|
1031
|
+
) => {
|
|
723
1032
|
pseudoActiveRef.current = pseudoActive === true
|
|
724
1033
|
const runners: Function[] = []
|
|
725
1034
|
const seenAnimateKeys = new Set<string>()
|
|
@@ -731,6 +1040,15 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
731
1040
|
resolveTransition(emittedTransition ?? effectiveTransition, { animations }),
|
|
732
1041
|
'default'
|
|
733
1042
|
)
|
|
1043
|
+
const emittedRatio = getLeadingRatio(nextMetrics)
|
|
1044
|
+
const emitterPaintsProduct = paintsLeadingProduct(nextStyle, emittedRatio)
|
|
1045
|
+
// the same rule as the render path: a text metric joins the graph only
|
|
1046
|
+
// while it is moving, so a pseudo that only changes an opacity leaves
|
|
1047
|
+
// the node's font size out of it and keeps the native driver
|
|
1048
|
+
const emitterMovesFontSize = movesTextValue('fontSize', nextStyle.fontSize)
|
|
1049
|
+
const emitterMovesLeading = emitterPaintsProduct
|
|
1050
|
+
? movesLeadingRatio(emittedRatio)
|
|
1051
|
+
: movesTextValue('lineHeight', nextStyle.lineHeight)
|
|
734
1052
|
// nextStyle is the complete style for this node, so the emitter makes
|
|
735
1053
|
// the same single driver decision as the render path. include the
|
|
736
1054
|
// currently rendered graph because its stale keys are not removed until
|
|
@@ -738,13 +1056,35 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
738
1056
|
const useNativeDriverForNode =
|
|
739
1057
|
nativeDriver &&
|
|
740
1058
|
!hasAnimatedLayoutKey(nextStyle, isDark, emittedResolved) &&
|
|
741
|
-
!
|
|
1059
|
+
!emitterMovesFontSize &&
|
|
1060
|
+
!emitterMovesLeading &&
|
|
1061
|
+
!inheritedFontSizeNode(nextStyle, emittedRatio, inheritedText) &&
|
|
1062
|
+
!Object.keys(animateStyles.current).some((key) => jsDriverStyleKey[key])
|
|
1063
|
+
|
|
1064
|
+
const emitterDerivesLeading =
|
|
1065
|
+
emitterPaintsProduct &&
|
|
1066
|
+
(emitterMovesFontSize || emitterMovesLeading) &&
|
|
1067
|
+
(!!getTransitionForKey(emittedResolved, 'fontSize') ||
|
|
1068
|
+
!!getTransitionForKey(emittedResolved, 'lineHeight'))
|
|
742
1069
|
|
|
743
1070
|
for (const key in nextStyle) {
|
|
744
1071
|
const rawVal = nextStyle[key]
|
|
745
1072
|
const val = resolveDynamicValue(rawVal, isDark)
|
|
746
1073
|
if (val === undefined) continue
|
|
747
1074
|
|
|
1075
|
+
if (emitterDerivesLeading && (key === 'fontSize' || key === 'lineHeight')) {
|
|
1076
|
+
continue
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// a metric at rest is not the emitter's to animate. it either already
|
|
1080
|
+
// paints as a plain style or the commit below hands it to one.
|
|
1081
|
+
if (
|
|
1082
|
+
textMetricStyleKey[key] &&
|
|
1083
|
+
!(key === 'fontSize' ? emitterMovesFontSize : emitterMovesLeading)
|
|
1084
|
+
) {
|
|
1085
|
+
continue
|
|
1086
|
+
}
|
|
1087
|
+
|
|
748
1088
|
if (key === 'transform' && Array.isArray(val)) {
|
|
749
1089
|
for (const transform of val) {
|
|
750
1090
|
if (!transform) continue
|
|
@@ -759,19 +1099,52 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
759
1099
|
} else if (
|
|
760
1100
|
animatedStyleKey[key] != null ||
|
|
761
1101
|
costlyToAnimateStyleKey[key] ||
|
|
762
|
-
|
|
1102
|
+
jsDriverStyleKey[key]
|
|
763
1103
|
) {
|
|
764
1104
|
// layout keys only animate numbers ('auto'/percents are static);
|
|
765
1105
|
// unparseable themed colors can't be interpolated — skip both and
|
|
766
1106
|
// let the next render apply them statically
|
|
767
|
-
if (
|
|
1107
|
+
if (jsDriverStyleKey[key] && typeof val !== 'number') continue
|
|
768
1108
|
if (colorStyleKey[key] && !isAnimatableColor(val)) continue
|
|
769
1109
|
if (!animateStyles.current[key]) animatedShapeChanged = true
|
|
770
|
-
animateStyles.current[key] = update(key,
|
|
1110
|
+
animateStyles.current[key] = update(key, animatedValueFor(key), val)
|
|
771
1111
|
seenAnimateKeys.add(key)
|
|
772
1112
|
}
|
|
773
1113
|
}
|
|
774
1114
|
|
|
1115
|
+
// the emitter can only re-target Animated.Values the committed style
|
|
1116
|
+
// already paints. re-targeting the font size and the ratio keeps the
|
|
1117
|
+
// product moving without a commit; a pass that disagrees with the
|
|
1118
|
+
// committed style about whether the leading IS a product (a pseudo that
|
|
1119
|
+
// overrides it with an absolute length, or the first pseudo pass on a
|
|
1120
|
+
// node whose render never derived) needs the node swapped, which only a
|
|
1121
|
+
// commit can do.
|
|
1122
|
+
if (emitterDerivesLeading) {
|
|
1123
|
+
if (emitterMovesFontSize) {
|
|
1124
|
+
if (!animateStyles.current.fontSize) animatedShapeChanged = true
|
|
1125
|
+
animateStyles.current.fontSize = update(
|
|
1126
|
+
'fontSize',
|
|
1127
|
+
animatedValueFor('fontSize'),
|
|
1128
|
+
nextStyle.fontSize as number
|
|
1129
|
+
)
|
|
1130
|
+
seenAnimateKeys.add('fontSize')
|
|
1131
|
+
}
|
|
1132
|
+
// a factor at rest is left out: it multiplies as the number the
|
|
1133
|
+
// painted product already carries. giving one a value the committed
|
|
1134
|
+
// style does not paint is the shape change that commits.
|
|
1135
|
+
if (emitterMovesLeading) {
|
|
1136
|
+
if (!leadingRatioValue.current) animatedShapeChanged = true
|
|
1137
|
+
leadingRatioValue.current = update(
|
|
1138
|
+
'lineHeight',
|
|
1139
|
+
leadingRatioValueFrom(),
|
|
1140
|
+
emittedRatio!
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (emitterDerivesLeading !== paintsDerivedLeading.current) {
|
|
1145
|
+
animatedShapeChanged = true
|
|
1146
|
+
}
|
|
1147
|
+
|
|
775
1148
|
// the emitter receives a complete style. keep the Animated style graph
|
|
776
1149
|
// equally complete, including a pseudo release that omits a pseudo-only
|
|
777
1150
|
// key. React Native needs a commit when that graph's shape changes.
|
|
@@ -786,6 +1159,12 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
786
1159
|
animatedShapeChanged = true
|
|
787
1160
|
}
|
|
788
1161
|
|
|
1162
|
+
paintedTextRef.current = {
|
|
1163
|
+
fontSize: nextStyle.fontSize,
|
|
1164
|
+
lineHeight: nextStyle.lineHeight,
|
|
1165
|
+
ratio: emittedRatio,
|
|
1166
|
+
}
|
|
1167
|
+
|
|
789
1168
|
// run the queued animations immediately
|
|
790
1169
|
runners.forEach((r) => r())
|
|
791
1170
|
|
|
@@ -856,7 +1235,8 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
856
1235
|
|
|
857
1236
|
return value
|
|
858
1237
|
}
|
|
859
|
-
}
|
|
1238
|
+
}
|
|
1239
|
+
useStyleEmitter?.(onEmittedStyle)
|
|
860
1240
|
|
|
861
1241
|
if (process.env.NODE_ENV === 'development') {
|
|
862
1242
|
if (props['debug'] === 'verbose') {
|