@rootnative/inertia 0.0.9 → 0.0.10
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/CHANGELOG.md +68 -1
- package/README.md +7 -7
- package/dist/applyBounds-BYGiHrYO.d.mts +18 -0
- package/dist/applyBounds-BYGiHrYO.d.ts +18 -0
- package/dist/{chunk-E3ALFSH2.js → chunk-2UGWAB6N.js} +78 -70
- package/dist/{chunk-ZBHQPVWE.js → chunk-3ZNHPT5Z.js} +2 -2
- package/dist/{chunk-IHVMNOS4.js → chunk-46766TBM.js} +2 -2
- package/dist/{chunk-MUT6BTZS.js → chunk-4EZ5DBL3.js} +10 -7
- package/dist/{chunk-MFAFB4K7.mjs → chunk-6FCATUMY.mjs} +8 -10
- package/dist/{chunk-6YIUETBF.mjs → chunk-7BH3CAO5.mjs} +1 -1
- package/dist/{chunk-JBK5ZLMH.mjs → chunk-CUHOCONB.mjs} +1 -1
- package/dist/{chunk-M4766VUV.mjs → chunk-DRA2K53O.mjs} +1 -1
- package/dist/{chunk-L4JFECXU.mjs → chunk-F4HMCXAX.mjs} +6 -3
- package/dist/{chunk-W7NTRSPD.mjs → chunk-HEAWMZEM.mjs} +105 -66
- package/dist/{chunk-IX6SEOSK.mjs → chunk-JENYDTXH.mjs} +52 -44
- package/dist/chunk-KFHZMFAX.js +23 -0
- package/dist/{chunk-6AN5X3YE.js → chunk-KXU6VH2Y.js} +10 -11
- package/dist/{chunk-WNVHPMBI.js → chunk-N33GSDNQ.js} +106 -65
- package/dist/chunk-OH46TGEA.mjs +20 -0
- package/dist/{chunk-QNLSCOR5.mjs → chunk-OLOSU2BD.mjs} +1 -1
- package/dist/chunk-PLKXCJIB.js +8 -0
- package/dist/{chunk-GSUC4HWP.mjs → chunk-QXKPIENP.mjs} +1 -1
- package/dist/chunk-UBHQJFUW.js +8 -0
- package/dist/{chunk-6NOCY764.js → chunk-VKXSL3ZL.js} +2 -2
- package/dist/chunk-XKNEQDAT.js +8 -0
- package/dist/{chunk-EN4PN7X3.mjs → chunk-Z23AMUSW.mjs} +1 -1
- package/dist/gestureLayer/index.d.mts +3 -3
- package/dist/gestureLayer/index.d.ts +3 -3
- package/dist/gestureLayer/index.js +12 -18
- package/dist/gestureLayer/index.mjs +5 -11
- package/dist/index.d.mts +87 -45
- package/dist/index.d.ts +87 -45
- package/dist/index.js +114 -91
- package/dist/index.mjs +66 -59
- package/dist/motion/FlatList.d.mts +1 -1
- package/dist/motion/FlatList.d.ts +1 -1
- package/dist/motion/FlatList.js +5 -5
- package/dist/motion/FlatList.mjs +4 -4
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +5 -5
- package/dist/motion/Image.mjs +4 -4
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +5 -5
- package/dist/motion/Pressable.mjs +4 -4
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +5 -5
- package/dist/motion/ScrollView.mjs +4 -4
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +5 -5
- package/dist/motion/Text.mjs +4 -4
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +5 -5
- package/dist/motion/View.mjs +4 -4
- package/dist/touch/index.d.mts +2 -1
- package/dist/touch/index.d.ts +2 -1
- package/dist/touch/index.js +16 -31
- package/dist/touch/index.mjs +11 -29
- package/dist/{types-DyJpG64F.d.mts → types-08mG8fNk.d.mts} +10 -3
- package/dist/{types-DyJpG64F.d.ts → types-08mG8fNk.d.ts} +10 -3
- package/dist/{useGesture-DbH46EBp.d.ts → useGesture-D1lHeQQ6.d.ts} +1 -1
- package/dist/{useGesture-CnZQnYHH.d.mts → useGesture-DjszaHWz.d.mts} +1 -1
- package/package.json +4 -2
- package/src/config/namedTransitions.ts +7 -9
- package/src/gestureLayer/index.ts +3 -0
- package/src/gestureLayer/useGestureLayer.ts +6 -9
- package/src/gestures/focusVisibility.ts +19 -17
- package/src/gestures/index.ts +1 -1
- package/src/index.ts +9 -0
- package/src/layout/resolveLayout.ts +4 -1
- package/src/layout/useSharedLayout.ts +2 -1
- package/src/motion/createMotionComponent.tsx +92 -44
- package/src/presence/Presence.tsx +5 -7
- package/src/touch/applyBounds.ts +30 -0
- package/src/touch/index.ts +1 -0
- package/src/touch/useTouchDrag.ts +15 -45
- package/src/transitions/constants.ts +14 -0
- package/src/transitions/easing.ts +23 -11
- package/src/transitions/index.ts +5 -0
- package/src/transitions/keys.ts +0 -2
- package/src/transitions/resolve.ts +86 -11
- package/src/transitions/resolveSequence.ts +3 -1
- package/src/transitions/runtime.ts +4 -7
- package/src/transitions/spring.ts +7 -2
- package/src/types.ts +9 -2
- package/src/values/extrapolation.ts +21 -0
- package/src/values/index.ts +1 -0
- package/src/values/useGesture.ts +9 -2
- package/src/values/useInterpolatedStyle.ts +5 -12
- package/src/values/useShadow.ts +18 -7
- package/src/values/useSpring.ts +34 -25
- package/src/values/useTransform.ts +24 -20
- package/src/values/useTranslateStyle.ts +31 -0
- package/dist/chunk-24NUL23E.js +0 -8
- package/dist/chunk-4XBX7KOD.js +0 -8
- package/dist/chunk-Z5USXWTE.js +0 -8
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
useNamedTransitions,
|
|
22
22
|
useShouldReduceMotion,
|
|
23
23
|
} from '../config'
|
|
24
|
-
import { isFocusVisible } from '../gestures'
|
|
24
|
+
import { installFocusVisibility, isFocusVisible } from '../gestures'
|
|
25
25
|
import {
|
|
26
26
|
layersToPayload,
|
|
27
27
|
normalizeBoxShadow,
|
|
@@ -47,6 +47,7 @@ import { useStaggerDelay } from '../stagger'
|
|
|
47
47
|
import {
|
|
48
48
|
applyDelay,
|
|
49
49
|
isTopLevelTransition,
|
|
50
|
+
repeatIterationsOf,
|
|
50
51
|
resolveAnimatableValue,
|
|
51
52
|
resolveTransition,
|
|
52
53
|
stableSig,
|
|
@@ -271,6 +272,18 @@ const SHARED_STYLE_KEY_SET = new Set<AnimatableKey>(SHARED_STYLE_KEYS)
|
|
|
271
272
|
*/
|
|
272
273
|
type AnimatableSlotValue = number | string | BoxShadowPayload
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The keys one instance animates plus the three flags the worklet branches
|
|
277
|
+
* on. Grown monotonically on the JS thread and mirrored to the UI thread
|
|
278
|
+
* through a shared value — see `activeSetSV` in the factory.
|
|
279
|
+
*/
|
|
280
|
+
type ActiveKeySet = {
|
|
281
|
+
keys: readonly AnimatableKey[]
|
|
282
|
+
hasTransform: boolean
|
|
283
|
+
hasShadowOffset: boolean
|
|
284
|
+
hasBoxShadow: boolean
|
|
285
|
+
}
|
|
286
|
+
|
|
274
287
|
/**
|
|
275
288
|
* Resting `boxShadow` — no shadow at all. Frozen and hoisted so every
|
|
276
289
|
* primitive shares one reference and an untouched slot never allocates.
|
|
@@ -603,10 +616,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
603
616
|
// render silently dropped the new key (its SV updated, but the
|
|
604
617
|
// worklet — which iterates this set — never read it).
|
|
605
618
|
//
|
|
606
|
-
// Growing-only keeps the worklet stable: the
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
619
|
+
// Growing-only keeps the worklet stable: the set object only changes on
|
|
620
|
+
// the renders that actually add a key. The worklet reads it through a
|
|
621
|
+
// shared value (`activeSetSV`, below), so an expansion crosses to the UI
|
|
622
|
+
// thread once per growth and nothing churns frame-to-frame.
|
|
610
623
|
const hasLayoutId = layoutId !== undefined
|
|
611
624
|
|
|
612
625
|
// The static `style`, flattened at most once per render and only when
|
|
@@ -668,14 +681,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
668
681
|
}
|
|
669
682
|
}
|
|
670
683
|
|
|
671
|
-
const
|
|
672
|
-
const hasTransformRef = useRef<boolean>(false)
|
|
673
|
-
const hasShadowOffsetRef = useRef<boolean>(false)
|
|
674
|
-
const hasBoxShadowRef = useRef<boolean>(false)
|
|
684
|
+
const activeSetRef = useRef<ActiveKeySet | null>(null)
|
|
675
685
|
// Expand the active set only when this render touched a key we haven't
|
|
676
|
-
// recorded yet. When nothing new appears we keep the existing
|
|
677
|
-
// identity so the
|
|
678
|
-
const prevActive =
|
|
686
|
+
// recorded yet. When nothing new appears we keep the existing object
|
|
687
|
+
// identity so the JS-side readers below see a stable value.
|
|
688
|
+
const prevActive = activeSetRef.current?.keys ?? null
|
|
679
689
|
let grew = prevActive === null
|
|
680
690
|
if (!grew && prevActive) {
|
|
681
691
|
for (const k of touched) {
|
|
@@ -688,14 +698,13 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
688
698
|
if (grew) {
|
|
689
699
|
const merged = new Set<AnimatableKey>(prevActive ?? [])
|
|
690
700
|
for (const k of touched) merged.add(k)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
hasBoxShadowRef.current = activeKeysRef.current.includes('boxShadow')
|
|
701
|
+
const keys = ALL_KEYS.filter((k) => merged.has(k))
|
|
702
|
+
activeSetRef.current = {
|
|
703
|
+
keys,
|
|
704
|
+
hasTransform: keys.some((k) => TRANSFORM_KEY_SET.has(k)),
|
|
705
|
+
hasShadowOffset: keys.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
|
|
706
|
+
hasBoxShadow: keys.includes('boxShadow'),
|
|
707
|
+
}
|
|
699
708
|
// Animating `boxShadow` alongside the native `shadow*` keys puts two
|
|
700
709
|
// shadow systems on one element; whichever the underlying view resolves
|
|
701
710
|
// last wins, and the result reads as a bug rather than a choice.
|
|
@@ -703,10 +712,8 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
703
712
|
// decides it, and this block only runs when that set grows.
|
|
704
713
|
if (
|
|
705
714
|
__DEV__ &&
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
(k) => k !== 'boxShadow' && k.startsWith('shadow'),
|
|
709
|
-
)
|
|
715
|
+
activeSetRef.current.hasBoxShadow &&
|
|
716
|
+
keys.some((k) => k !== 'boxShadow' && k.startsWith('shadow'))
|
|
710
717
|
) {
|
|
711
718
|
warnOnce(
|
|
712
719
|
'boxShadow-with-native-shadow',
|
|
@@ -717,6 +724,25 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
717
724
|
)
|
|
718
725
|
}
|
|
719
726
|
}
|
|
727
|
+
const activeKeys = activeSetRef.current!.keys
|
|
728
|
+
|
|
729
|
+
// The worklet's copy of the active set. It must be a shared value, not
|
|
730
|
+
// the ref above: react-native-worklets clones a captured plain object
|
|
731
|
+
// once, when the worklet is first serialised, and in dev it freezes the
|
|
732
|
+
// original so later writes to `.current` are dropped with a warning. A
|
|
733
|
+
// ref read inside the worklet therefore only ever sees the mount-time
|
|
734
|
+
// set, and a key added after mount drives its shared value but never
|
|
735
|
+
// reaches the screen (Worklet rule 2). The Jest mock runs the worklet on
|
|
736
|
+
// the JS thread, so only a device shows the difference. The write lives
|
|
737
|
+
// in an effect because Reanimated's strict mode warns on a shared-value
|
|
738
|
+
// write during render; it is declared before the value-driving effects
|
|
739
|
+
// below, so the grown set reaches the UI thread in the same commit as
|
|
740
|
+
// the first animation of the new key.
|
|
741
|
+
const activeSetSV = useSharedValue<ActiveKeySet>(activeSetRef.current!)
|
|
742
|
+
const activeSet = activeSetRef.current!
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
if (activeSetSV.value !== activeSet) activeSetSV.value = activeSet
|
|
745
|
+
}, [activeSetSV, activeSet])
|
|
720
746
|
|
|
721
747
|
// Which keys a record actually drives *right now*. Everything else in
|
|
722
748
|
// the active set is there only because a gesture sub-state, an `exit`
|
|
@@ -741,7 +767,6 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
741
767
|
// Resting values pulled off the static `style`, for active keys nothing
|
|
742
768
|
// has driven. Gated so the common `animate`-only instance never flattens
|
|
743
769
|
// a style it has no use for.
|
|
744
|
-
const activeKeys = activeKeysRef.current!
|
|
745
770
|
const everDriven = everDrivenRef.current
|
|
746
771
|
const styleResting: Partial<Record<AnimatableKey, AnimatableSlotValue>> =
|
|
747
772
|
{}
|
|
@@ -857,7 +882,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
857
882
|
|
|
858
883
|
const styleRestingSig = stableSig(styleResting)
|
|
859
884
|
useEffect(() => {
|
|
860
|
-
for (const key of
|
|
885
|
+
for (const key of activeKeys) {
|
|
861
886
|
if (everDrivenRef.current.has(key)) continue
|
|
862
887
|
sharedValues[key].value = (styleResting[key] ??
|
|
863
888
|
DEFAULT_RESTING[key]) as never
|
|
@@ -926,6 +951,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
926
951
|
(isExiting ? '|exit' : '') +
|
|
927
952
|
(shouldReduceMotion ? '|rm' : '')
|
|
928
953
|
const transitionSig = stableSig(transition)
|
|
954
|
+
// The callback factory skips callback installation when there is no
|
|
955
|
+
// `onAnimationEnd` and nothing to settle, so a handler that appears
|
|
956
|
+
// after mount needs the effect to run again. Keyed on presence, not
|
|
957
|
+
// identity: a changing handler is read through `onAnimationEndRef`.
|
|
958
|
+
const hasAnimationEnd = onAnimationEnd !== undefined
|
|
929
959
|
|
|
930
960
|
// Stable ref to the live `safeToRemove` so the effect's settle-counter
|
|
931
961
|
// closure can reach the latest <Presence> binding without retriggering.
|
|
@@ -943,6 +973,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
943
973
|
return
|
|
944
974
|
}
|
|
945
975
|
|
|
976
|
+
// `done` doubles as the run token. When this effect re-runs, the
|
|
977
|
+
// cleanup below sets it, so the callbacks of the superseded animations
|
|
978
|
+
// — which Reanimated fires with `finished: false` as they are replaced
|
|
979
|
+
// — cannot drain this run's counter and release <Presence> early.
|
|
946
980
|
let pending = 0
|
|
947
981
|
let done = false
|
|
948
982
|
const onSettle = () => {
|
|
@@ -1095,8 +1129,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1095
1129
|
if (isExiting && pending === 0) {
|
|
1096
1130
|
safeToRemoveRef.current?.()
|
|
1097
1131
|
}
|
|
1132
|
+
return () => {
|
|
1133
|
+
done = true
|
|
1134
|
+
}
|
|
1098
1135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1099
|
-
}, [baseSig, transitionSig])
|
|
1136
|
+
}, [baseSig, transitionSig, hasAnimationEnd])
|
|
1100
1137
|
|
|
1101
1138
|
// Per-layer progress: when a sub-state activation flips, animate its
|
|
1102
1139
|
// progress SV 0↔1 with the layer's own transition (or the parent
|
|
@@ -1162,7 +1199,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1162
1199
|
if (!hasLayoutId) return undefined
|
|
1163
1200
|
const out: SharedStyleSnapshot = {}
|
|
1164
1201
|
let any = false
|
|
1165
|
-
for (const key of
|
|
1202
|
+
for (const key of activeKeys) {
|
|
1166
1203
|
if (!SHARED_STYLE_KEY_SET.has(key)) continue
|
|
1167
1204
|
const v = sharedValues[key].value
|
|
1168
1205
|
// Structured slots can't be carried, and a key nothing ever gave a
|
|
@@ -1186,10 +1223,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1186
1223
|
const carry = sharedLayout.carry
|
|
1187
1224
|
|
|
1188
1225
|
const animatedStyle = useAnimatedStyle(() => {
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1226
|
+
const activeSet = activeSetSV.value
|
|
1227
|
+
const activeKeys = activeSet.keys
|
|
1228
|
+
const hasTransform = activeSet.hasTransform
|
|
1229
|
+
const hasShadowOffset = activeSet.hasShadowOffset
|
|
1230
|
+
const hasBoxShadow = activeSet.hasBoxShadow
|
|
1193
1231
|
const out: Record<string, unknown> = {}
|
|
1194
1232
|
const transform: Array<Record<string, unknown>> = []
|
|
1195
1233
|
// shadow-offset reassembly buffers. The two synthetic axis SVs feed in
|
|
@@ -1448,7 +1486,7 @@ type SharedValueMap = Record<AnimatableKey, SharedValue<AnimatableSlotValue>>
|
|
|
1448
1486
|
* The ESLint exhaustive-deps rule can't see through the loop, hence the
|
|
1449
1487
|
* disable — the invariant it would check for us is asserted above instead.
|
|
1450
1488
|
*
|
|
1451
|
-
* Unused shared values are cheap; the worklet skips them via `
|
|
1489
|
+
* Unused shared values are cheap; the worklet skips them via `activeSetSV`.
|
|
1452
1490
|
* Color keys are seeded with the initial color string so Reanimated's value
|
|
1453
1491
|
* setter recognizes the slot as a color from the first `withSpring` /
|
|
1454
1492
|
* `withTiming` call.
|
|
@@ -1805,12 +1843,9 @@ function stepCountOf(v: AnimatableValue<number | string> | undefined): number {
|
|
|
1805
1843
|
*/
|
|
1806
1844
|
function totalIterationsOf(cfg: TransitionConfig | undefined): number {
|
|
1807
1845
|
if (!cfg || cfg.type === 'no-animation' || cfg.type === 'decay') return 1
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
if (typeof r === 'number') return r
|
|
1812
|
-
if (r.count === 'infinite') return Number.POSITIVE_INFINITY
|
|
1813
|
-
return r.count
|
|
1846
|
+
// Shares the resolver's count rule, so a `repeat: 0` that the resolver
|
|
1847
|
+
// treats as "run once" is also counted as one iteration here.
|
|
1848
|
+
return repeatIterationsOf(cfg.repeat)
|
|
1814
1849
|
}
|
|
1815
1850
|
|
|
1816
1851
|
/**
|
|
@@ -1880,16 +1915,23 @@ function resolveAnimateInput(
|
|
|
1880
1915
|
variants: VariantsMap<unknown> | undefined,
|
|
1881
1916
|
controllerKey: string | undefined,
|
|
1882
1917
|
): AnimateStyle<unknown> | undefined {
|
|
1918
|
+
if (controllerKey !== undefined && animate !== undefined) {
|
|
1919
|
+
warnOnce(
|
|
1920
|
+
'controller-and-animate',
|
|
1921
|
+
'[inertia] Both `controller` and `animate` are set on the same ' +
|
|
1922
|
+
'primitive. The controller drives the animation and `animate` is ' +
|
|
1923
|
+
'ignored — remove one of the two.',
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
1883
1926
|
if (controllerKey !== undefined && variants && controllerKey in variants) {
|
|
1884
1927
|
return variants[controllerKey]
|
|
1885
1928
|
}
|
|
1886
1929
|
if (typeof animate === 'string') {
|
|
1887
1930
|
if (variants && animate in variants) return variants[animate]
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1931
|
+
warnOnce(
|
|
1932
|
+
`missing-variant:${animate}`,
|
|
1933
|
+
`[inertia] animate="${animate}" but no matching variant. Did you forget to pass \`variants\`?`,
|
|
1934
|
+
)
|
|
1893
1935
|
return undefined
|
|
1894
1936
|
}
|
|
1895
1937
|
return animate as AnimateStyle<unknown> | undefined
|
|
@@ -2047,6 +2089,12 @@ function useGestureHandlers(
|
|
|
2047
2089
|
const hasFocused = gesture?.focused ? 1 : 0
|
|
2048
2090
|
const hasFocusVisible = gesture?.focusVisible ? 1 : 0
|
|
2049
2091
|
const hasHovered = gesture?.hovered ? 1 : 0
|
|
2092
|
+
// The web modality listeners attach on the first mount that tracks
|
|
2093
|
+
// `focusVisible` — see `focusVisibility.ts` for why mount time is early
|
|
2094
|
+
// enough and why import time is not an option.
|
|
2095
|
+
useEffect(() => {
|
|
2096
|
+
if (hasFocusVisible) installFocusVisibility()
|
|
2097
|
+
}, [hasFocusVisible])
|
|
2050
2098
|
return useMemo(() => {
|
|
2051
2099
|
if (!gesture) return {}
|
|
2052
2100
|
const handlers: GestureHandlers = {}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
useState,
|
|
11
11
|
} from 'react'
|
|
12
|
+
import { warnOnce } from '../internal/warnOnce'
|
|
12
13
|
import { PresenceContext, type PresenceContextValue } from './PresenceContext'
|
|
13
14
|
|
|
14
15
|
interface RenderEntry {
|
|
@@ -38,11 +39,10 @@ export function Presence({ children }: { children: ReactNode }) {
|
|
|
38
39
|
Children.forEach(children, (child) => {
|
|
39
40
|
if (!isValidElement(child)) return
|
|
40
41
|
if (child.key === null) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
42
|
+
warnOnce(
|
|
43
|
+
'presence-keyless-child',
|
|
44
|
+
'[inertia] <Presence> children must have a `key`. Skipping a keyless child.',
|
|
45
|
+
)
|
|
46
46
|
return
|
|
47
47
|
}
|
|
48
48
|
out.push(child)
|
|
@@ -224,5 +224,3 @@ function PresenceItem({
|
|
|
224
224
|
</PresenceContext.Provider>
|
|
225
225
|
)
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
declare const __DEV__: boolean
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
|
|
3
|
+
* bound is scaled by `elastic` instead of hard-clamped, giving a rubber-band
|
|
4
|
+
* feel. `min` / `max` may be `undefined` to leave that side unbounded.
|
|
5
|
+
*
|
|
6
|
+
* Worklet, so it runs both inside a gesture-handler pan handler on the UI
|
|
7
|
+
* thread (`useDrag`) and from a JS-thread `PanResponder` callback
|
|
8
|
+
* (`useTouchDrag`).
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* applyBounds(120, -100, 100, 0) // 100
|
|
12
|
+
* applyBounds(120, -100, 100, 0.5) // 110
|
|
13
|
+
* applyBounds(50, undefined, 100, 0) // 50
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function applyBounds(
|
|
17
|
+
value: number,
|
|
18
|
+
min: number | undefined,
|
|
19
|
+
max: number | undefined,
|
|
20
|
+
elastic: number,
|
|
21
|
+
): number {
|
|
22
|
+
'worklet'
|
|
23
|
+
if (min !== undefined && value < min) {
|
|
24
|
+
return elastic > 0 ? min + (value - min) * elastic : min
|
|
25
|
+
}
|
|
26
|
+
if (max !== undefined && value > max) {
|
|
27
|
+
return elastic > 0 ? max + (value - max) * elastic : max
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
package/src/touch/index.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
1
|
+
import { useMemo, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
PanResponder,
|
|
4
4
|
type PanResponderGestureState,
|
|
5
5
|
type PanResponderInstance,
|
|
6
6
|
} from 'react-native'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
useSharedValue,
|
|
10
|
-
type SharedValue,
|
|
11
|
-
} from 'react-native-reanimated'
|
|
7
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
8
|
+
import type { useAnimatedStyle } from 'react-native-reanimated'
|
|
12
9
|
import { buildReleaseAnimation } from '../transitions'
|
|
10
|
+
import { useTranslateStyle } from '../values/useTranslateStyle'
|
|
11
|
+
import { applyBounds } from './applyBounds'
|
|
13
12
|
import type { TransitionConfig } from '../types'
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -166,23 +165,17 @@ export function useTouchDrag(
|
|
|
166
165
|
const top = constraints?.top
|
|
167
166
|
const bottom = constraints?.bottom
|
|
168
167
|
const elasticCoef = elastic
|
|
169
|
-
|
|
168
|
+
|
|
169
|
+
// Callbacks are read through a ref at call time, so an inline arrow in
|
|
170
|
+
// `options` does not rebuild the PanResponder every render. PanResponder
|
|
171
|
+
// callbacks run on the JS thread, so a ref read is safe here.
|
|
172
|
+
const callbacksRef = useRef(options)
|
|
173
|
+
callbacksRef.current = options
|
|
170
174
|
|
|
171
175
|
const responder = useMemo(
|
|
172
176
|
() => buildResponder(),
|
|
173
177
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
-
[
|
|
175
|
-
lockX,
|
|
176
|
-
lockY,
|
|
177
|
-
left,
|
|
178
|
-
right,
|
|
179
|
-
top,
|
|
180
|
-
bottom,
|
|
181
|
-
elasticCoef,
|
|
182
|
-
onDragStart,
|
|
183
|
-
onDragEnd,
|
|
184
|
-
onRelease,
|
|
185
|
-
],
|
|
178
|
+
[lockX, lockY, left, right, top, bottom, elasticCoef],
|
|
186
179
|
)
|
|
187
180
|
|
|
188
181
|
// Hoisted out of the inline `useMemo` factory to keep the dep list readable
|
|
@@ -196,6 +189,7 @@ export function useTouchDrag(
|
|
|
196
189
|
// `@rootnative/inertia-gestures` API (px/sec from gesture-handler).
|
|
197
190
|
const vx = g.vx * 1000
|
|
198
191
|
const vy = g.vy * 1000
|
|
192
|
+
const { onRelease, onDragEnd } = callbacksRef.current
|
|
199
193
|
if (onRelease) {
|
|
200
194
|
const result = onRelease({ x, y, velocity: { x: vx, y: vy } })
|
|
201
195
|
if (result) {
|
|
@@ -228,7 +222,7 @@ export function useTouchDrag(
|
|
|
228
222
|
startX.value = dragX.value
|
|
229
223
|
startY.value = dragY.value
|
|
230
224
|
isDragging.value = true
|
|
231
|
-
|
|
225
|
+
callbacksRef.current.onDragStart?.()
|
|
232
226
|
},
|
|
233
227
|
onPanResponderMove: (_e, g) => {
|
|
234
228
|
if (lockX) {
|
|
@@ -253,9 +247,7 @@ export function useTouchDrag(
|
|
|
253
247
|
})
|
|
254
248
|
}
|
|
255
249
|
|
|
256
|
-
const animatedStyle =
|
|
257
|
-
transform: [{ translateX: dragX.value }, { translateY: dragY.value }],
|
|
258
|
-
}))
|
|
250
|
+
const animatedStyle = useTranslateStyle(dragX, dragY)
|
|
259
251
|
|
|
260
252
|
return {
|
|
261
253
|
panHandlers: responder.panHandlers,
|
|
@@ -265,25 +257,3 @@ export function useTouchDrag(
|
|
|
265
257
|
isDragging,
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
|
|
271
|
-
* bound is scaled by `elastic`, giving a rubber-band feel. `min` / `max`
|
|
272
|
-
* may be `undefined` to leave that side unbounded.
|
|
273
|
-
*
|
|
274
|
-
* JS-thread (PanResponder callbacks are JS, not worklets).
|
|
275
|
-
*/
|
|
276
|
-
function applyBounds(
|
|
277
|
-
value: number,
|
|
278
|
-
min: number | undefined,
|
|
279
|
-
max: number | undefined,
|
|
280
|
-
elastic: number,
|
|
281
|
-
): number {
|
|
282
|
-
if (min !== undefined && value < min) {
|
|
283
|
-
return elastic > 0 ? min + (value - min) * elastic : min
|
|
284
|
-
}
|
|
285
|
-
if (max !== undefined && value > max) {
|
|
286
|
-
return elastic > 0 ? max + (value - max) * elastic : max
|
|
287
|
-
}
|
|
288
|
-
return value
|
|
289
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `duration` for a `timing` transition that does not set one, in ms.
|
|
3
|
+
* Shared by the JS-thread resolver (`resolve.ts`) and the UI-thread builder
|
|
4
|
+
* (`runtime.ts`) so the two paths cannot drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_TIMING_DURATION = 250
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default `duration` for a `timing` layout transition (`layout` prop and
|
|
10
|
+
* `layoutId` shared-element legs), in ms. Layout moves cover more distance
|
|
11
|
+
* than a property tween, so the default is longer than
|
|
12
|
+
* `DEFAULT_TIMING_DURATION`.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_LAYOUT_DURATION = 300
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// dep); Reanimated's own re-export is deprecated.
|
|
3
3
|
import { isWorkletFunction } from 'react-native-worklets'
|
|
4
4
|
import { warnNonWorkletOnce } from '../internal/nonWorkletWarning'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type EasingFunction,
|
|
7
|
+
type EasingFunctionFactory,
|
|
8
|
+
type EasingInput,
|
|
9
|
+
} from '../types'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Reanimated 3.9+ validates that easing functions used in nested-transition
|
|
@@ -33,7 +37,7 @@ export function ensureWorkletEasing(
|
|
|
33
37
|
// Reanimated 4 `EasingFunctionFactory` — unwrap via `.factory()` before
|
|
34
38
|
// checking worklet status, so the wrapped fn (not the factory wrapper)
|
|
35
39
|
// ends up in the transition config.
|
|
36
|
-
const fn =
|
|
40
|
+
const fn = unwrapEasingFactory(easing)
|
|
37
41
|
if (isWorkletFunction(fn)) return fn
|
|
38
42
|
warnNonWorkletOnce(
|
|
39
43
|
'timing-easing',
|
|
@@ -46,13 +50,21 @@ export function ensureWorkletEasing(
|
|
|
46
50
|
return wrapped
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Return the easing function behind an `EasingInput`: a Reanimated 4
|
|
55
|
+
* `EasingFunctionFactory` is unwrapped through `.factory()`, a plain function
|
|
56
|
+
* passes through. Worklet, so `buildReleaseAnimation` can call it from a
|
|
57
|
+
* gesture `onEnd` handler on the UI thread.
|
|
58
|
+
*/
|
|
59
|
+
export function unwrapEasingFactory(easing: EasingInput): EasingFunction {
|
|
60
|
+
'worklet'
|
|
61
|
+
if (
|
|
62
|
+
typeof easing === 'object' &&
|
|
63
|
+
easing !== null &&
|
|
64
|
+
'factory' in easing &&
|
|
65
|
+
typeof (easing as { factory: unknown }).factory === 'function'
|
|
66
|
+
) {
|
|
67
|
+
return (easing as EasingFunctionFactory).factory()
|
|
68
|
+
}
|
|
69
|
+
return easing as EasingFunction
|
|
58
70
|
}
|
package/src/transitions/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export {
|
|
2
2
|
applyDelay,
|
|
3
|
+
applyRepeat,
|
|
4
|
+
normalizeRepeat,
|
|
5
|
+
repeatIterationsOf,
|
|
3
6
|
resolveTransition,
|
|
4
7
|
type AnimationCallback,
|
|
5
8
|
type CallbackFactory,
|
|
9
|
+
type NormalizedRepeat,
|
|
6
10
|
} from './resolve'
|
|
11
|
+
export { DEFAULT_LAYOUT_DURATION, DEFAULT_TIMING_DURATION } from './constants'
|
|
7
12
|
export { resolveAnimatableValue } from './resolveSequence'
|
|
8
13
|
export { cubicBezier } from './cubicBezier'
|
|
9
14
|
export { ensureWorkletEasing } from './easing'
|
package/src/transitions/keys.ts
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
withSpring,
|
|
7
7
|
withTiming,
|
|
8
8
|
} from 'react-native-reanimated'
|
|
9
|
+
import { DEFAULT_TIMING_DURATION } from './constants'
|
|
9
10
|
import { ensureWorkletEasing } from './easing'
|
|
10
11
|
import { springToReanimated } from './spring'
|
|
12
|
+
import { warnOnce } from '../internal/warnOnce'
|
|
11
13
|
import {
|
|
12
14
|
type DecayTransition,
|
|
13
15
|
type RepeatConfig,
|
|
@@ -37,8 +39,6 @@ export type CallbackFactory = (
|
|
|
37
39
|
step: number | undefined,
|
|
38
40
|
) => AnimationCallback | undefined
|
|
39
41
|
|
|
40
|
-
const DEFAULT_TIMING_DURATION = 250
|
|
41
|
-
|
|
42
42
|
function buildSpring(
|
|
43
43
|
cfg: SpringTransition,
|
|
44
44
|
toValue: number | string,
|
|
@@ -94,26 +94,101 @@ function buildOne(
|
|
|
94
94
|
return buildSpring(cfg as SpringTransition, toValue, cb)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Normalised repeat: `count` is the total number of iterations
|
|
99
|
+
* (`Number.POSITIVE_INFINITY` for `'infinite'`), `alternate` is the resolved
|
|
100
|
+
* flag, `explicitAlternate` says whether the caller set `alternate` by hand.
|
|
101
|
+
*/
|
|
102
|
+
export interface NormalizedRepeat {
|
|
103
|
+
count: number
|
|
104
|
+
alternate: boolean
|
|
105
|
+
explicitAlternate: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reduce the three public `repeat` shapes to one record, or `undefined` when
|
|
110
|
+
* the animation runs once. A count below `1` is treated as "run once" and
|
|
111
|
+
* warns in dev: Reanimated's `withRepeat` reads `0` and negative counts as
|
|
112
|
+
* endless, so forwarding them would turn `repeat: 0` into an infinite loop.
|
|
113
|
+
*/
|
|
114
|
+
export function normalizeRepeat(
|
|
115
|
+
repeat: RepeatConfig | undefined,
|
|
116
|
+
): NormalizedRepeat | undefined {
|
|
117
|
+
if (repeat === undefined) return undefined
|
|
118
|
+
if (repeat === 'infinite') {
|
|
119
|
+
return {
|
|
120
|
+
count: Number.POSITIVE_INFINITY,
|
|
121
|
+
alternate: true,
|
|
122
|
+
explicitAlternate: false,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const rawCount = typeof repeat === 'number' ? repeat : repeat.count
|
|
126
|
+
const alternate = typeof repeat === 'number' ? true : repeat.alternate
|
|
127
|
+
if (rawCount === 'infinite') {
|
|
128
|
+
return {
|
|
129
|
+
count: Number.POSITIVE_INFINITY,
|
|
130
|
+
alternate: alternate ?? true,
|
|
131
|
+
explicitAlternate: alternate !== undefined,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!(rawCount >= 1)) {
|
|
135
|
+
warnOnce(
|
|
136
|
+
`repeat-count:${String(rawCount)}`,
|
|
137
|
+
`[inertia] repeat count ${String(rawCount)} is below 1 — the ` +
|
|
138
|
+
`animation runs once. Use \`repeat: 2\` or more to repeat, or ` +
|
|
139
|
+
`\`repeat: 'infinite'\`. (Reanimated reads a count of 0 as endless, ` +
|
|
140
|
+
`so it is not forwarded.)`,
|
|
141
|
+
)
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
count: rawCount,
|
|
146
|
+
alternate: alternate ?? true,
|
|
147
|
+
explicitAlternate: alternate !== undefined,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Total number of iterations an animation built from `repeat` runs, including
|
|
153
|
+
* the first pass. `1` when there is no repeat or the count is below 1;
|
|
154
|
+
* `Number.POSITIVE_INFINITY` for `'infinite'`.
|
|
155
|
+
*/
|
|
156
|
+
export function repeatIterationsOf(repeat: RepeatConfig | undefined): number {
|
|
157
|
+
return normalizeRepeat(repeat)?.count ?? 1
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
/**
|
|
98
161
|
* Wrap an animation in `withRepeat` per the unified `repeat` shape:
|
|
99
162
|
* - `number` → finite count, alternating direction
|
|
100
163
|
* - `'infinite'` → endless, alternating direction
|
|
101
164
|
* - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
|
|
165
|
+
*
|
|
166
|
+
* Pass `{ sequence: true }` when `animation` is a `withSequence` result.
|
|
167
|
+
* Reanimated's `reverse` flag only swaps the wrapped animation's `toValue`,
|
|
168
|
+
* which a sequence ignores — it restarts at step 0 on every pass — so the
|
|
169
|
+
* flag is not forwarded for sequences. An explicit `alternate: true` on a
|
|
170
|
+
* sequence warns in dev; write the reverse steps into the sequence instead.
|
|
102
171
|
*/
|
|
103
172
|
export function applyRepeat(
|
|
104
173
|
animation: unknown,
|
|
105
174
|
repeat: RepeatConfig | undefined,
|
|
175
|
+
options?: { sequence?: boolean },
|
|
106
176
|
) {
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
177
|
+
const r = normalizeRepeat(repeat)
|
|
178
|
+
if (r === undefined) return animation
|
|
179
|
+
const count = Number.isFinite(r.count) ? r.count : -1
|
|
180
|
+
if (options?.sequence) {
|
|
181
|
+
if (r.explicitAlternate && r.alternate) {
|
|
182
|
+
warnOnce(
|
|
183
|
+
'repeat-sequence-alternate',
|
|
184
|
+
'[inertia] repeat.alternate has no effect on a sequence — Reanimated ' +
|
|
185
|
+
'restarts a sequence at its first step on every pass. Append the ' +
|
|
186
|
+
'reverse steps to the sequence to alternate.',
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
return withRepeat(animation as never, count, false)
|
|
113
190
|
}
|
|
114
|
-
|
|
115
|
-
const alternate = repeat.alternate ?? true
|
|
116
|
-
return withRepeat(animation as never, count, alternate)
|
|
191
|
+
return withRepeat(animation as never, count, r.alternate)
|
|
117
192
|
}
|
|
118
193
|
|
|
119
194
|
/**
|