@onlynative/inertia 0.0.1-alpha.7 → 0.0.1-alpha.9
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/README.md +1 -1
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +388 -1542
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -1545
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +244 -1462
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +247 -1465
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +244 -1462
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +247 -1465
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +244 -1462
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +247 -1465
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +244 -1462
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +247 -1465
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +244 -1462
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +247 -1465
- package/dist/motion/View.mjs.map +1 -1
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/{types-NmNeJjo1.d.mts → types-cU43dEmH.d.mts} +64 -17
- package/dist/{types-NmNeJjo1.d.ts → types-cU43dEmH.d.ts} +64 -17
- package/dist/useGesture-B7A_1DVg.d.ts +84 -0
- package/dist/useGesture-cimMrzC1.d.mts +84 -0
- package/jest-setup.js +4 -0
- package/llms.txt +12 -3
- package/package.json +22 -2
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/index.ts +7 -0
- package/src/layout/index.ts +15 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/createMotionComponent.tsx +123 -37
- package/src/motion/installCheck.ts +7 -11
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/types.ts +79 -20
- package/src/values/index.ts +11 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useShadow.ts +116 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MutableRefObject,
|
|
3
|
+
type Ref,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { type LayoutChangeEvent } from 'react-native'
|
|
10
|
+
import {
|
|
11
|
+
type SharedValue,
|
|
12
|
+
useSharedValue,
|
|
13
|
+
withSequence,
|
|
14
|
+
withSpring,
|
|
15
|
+
withTiming,
|
|
16
|
+
} from 'react-native-reanimated'
|
|
17
|
+
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
18
|
+
import { type SpringTransition, type TransitionConfig } from '../types'
|
|
19
|
+
import {
|
|
20
|
+
consumeLayout,
|
|
21
|
+
registerLayout,
|
|
22
|
+
releaseLayout,
|
|
23
|
+
type SharedRect,
|
|
24
|
+
} from './sharedRegistry'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shared values produced by `useSharedLayout`. The worklet inside
|
|
28
|
+
* `createMotionComponent` appends `translateX/Y` and `scaleX/Y` transforms
|
|
29
|
+
* built from these so a shared-layout source rect maps onto the new
|
|
30
|
+
* element's transform stack without conflicting with the user's `animate`
|
|
31
|
+
* transforms — multiple transform entries of the same key compose
|
|
32
|
+
* additively (for translates) and multiplicatively (for scales), which is
|
|
33
|
+
* exactly the FLIP semantic.
|
|
34
|
+
*
|
|
35
|
+
* At rest the values are `(0, 0, 1, 1)` — the identity transform — so when
|
|
36
|
+
* no shared-layout transition is active the worklet's contribution is a
|
|
37
|
+
* no-op.
|
|
38
|
+
*/
|
|
39
|
+
export interface SharedLayoutValues {
|
|
40
|
+
dx: SharedValue<number>
|
|
41
|
+
dy: SharedValue<number>
|
|
42
|
+
sx: SharedValue<number>
|
|
43
|
+
sy: SharedValue<number>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What the host component needs to wire into its rendered tree. */
|
|
47
|
+
export interface SharedLayoutBindings {
|
|
48
|
+
flip: SharedLayoutValues
|
|
49
|
+
/**
|
|
50
|
+
* Composite ref the consumer attaches to the rendered animated
|
|
51
|
+
* component. Forwards the underlying ref to the user-supplied `ref`
|
|
52
|
+
* (when present); kept as a stable callback so a `<Motion.*>` with no
|
|
53
|
+
* `layoutId` doesn't pay anything extra.
|
|
54
|
+
*/
|
|
55
|
+
setRef: (node: unknown) => void
|
|
56
|
+
/** onLayout handler the consumer must attach to the animated component. */
|
|
57
|
+
onLayout: (event: LayoutChangeEvent) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook backing `<Motion.* layoutId="..." />`.
|
|
62
|
+
*
|
|
63
|
+
* Responsibilities, in order:
|
|
64
|
+
* 1. Allocate FLIP shared values (identity at rest).
|
|
65
|
+
* 2. Track the latest layout rect via the `onLayout` event, and push it
|
|
66
|
+
* into the registry under `layoutId` so this primitive can serve as
|
|
67
|
+
* the source for a future transition.
|
|
68
|
+
* 3. On unmount, hand the last measured rect to `releaseLayout` so the
|
|
69
|
+
* next mount with the same id can consume it.
|
|
70
|
+
* 4. On the first layout commit, consume any pending source rect and
|
|
71
|
+
* drive the FLIP shared values: snap them to the (delta, scale) that
|
|
72
|
+
* visually places the new element at the source position, then
|
|
73
|
+
* animate back to identity. `withSequence(snap, animate)` keeps the
|
|
74
|
+
* animation starting from the snapped delta rather than from zero.
|
|
75
|
+
*
|
|
76
|
+
* Coordinate space note: rects are in parent-relative coordinates (what
|
|
77
|
+
* `onLayout` reports). For the common cross-screen navigator pattern —
|
|
78
|
+
* both screens share an outer content container — parent-relative deltas
|
|
79
|
+
* match what the user perceives. Nested-parent setups where the source
|
|
80
|
+
* and target screens sit under containers at different screen offsets
|
|
81
|
+
* will be off by that offset; v1 documents this and leaves a precise
|
|
82
|
+
* window-coords path for v2.
|
|
83
|
+
*
|
|
84
|
+
* When `layoutId` is `undefined`, every callback is a no-op and the FLIP
|
|
85
|
+
* shared values stay at identity — the host's worklet then skips the
|
|
86
|
+
* transform contribution entirely.
|
|
87
|
+
*/
|
|
88
|
+
export function useSharedLayout(options: {
|
|
89
|
+
layoutId: string | undefined
|
|
90
|
+
userRef: Ref<unknown> | undefined
|
|
91
|
+
transition: TransitionConfig | undefined
|
|
92
|
+
shouldReduceMotion: boolean
|
|
93
|
+
userOnLayout: ((event: LayoutChangeEvent) => void) | undefined
|
|
94
|
+
}): SharedLayoutBindings {
|
|
95
|
+
const { layoutId, userRef, transition, shouldReduceMotion, userOnLayout } =
|
|
96
|
+
options
|
|
97
|
+
|
|
98
|
+
const dx = useSharedValue(0)
|
|
99
|
+
const dy = useSharedValue(0)
|
|
100
|
+
const sx = useSharedValue(1)
|
|
101
|
+
const sy = useSharedValue(1)
|
|
102
|
+
|
|
103
|
+
// Most-recent rect for this primitive. Updated on every layout commit;
|
|
104
|
+
// read on unmount to populate the registry as the FLIP source for the
|
|
105
|
+
// next mount with the same id.
|
|
106
|
+
const lastRectRef = useRef<SharedRect | null>(null)
|
|
107
|
+
|
|
108
|
+
// First-layout latch — only the first measurement after a fresh mount
|
|
109
|
+
// can consume a source rect. Subsequent layouts (resizes, prop changes)
|
|
110
|
+
// refresh the registry but never re-trigger a FLIP from an old source.
|
|
111
|
+
const consumedRef = useRef(false)
|
|
112
|
+
|
|
113
|
+
const transitionRef = useRef(transition)
|
|
114
|
+
transitionRef.current = transition
|
|
115
|
+
const reducedMotionRef = useRef(shouldReduceMotion)
|
|
116
|
+
reducedMotionRef.current = shouldReduceMotion
|
|
117
|
+
|
|
118
|
+
const setRef = useCallback(
|
|
119
|
+
(node: unknown) => {
|
|
120
|
+
if (typeof userRef === 'function') userRef(node)
|
|
121
|
+
else if (userRef) (userRef as MutableRefObject<unknown>).current = node
|
|
122
|
+
},
|
|
123
|
+
[userRef],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const onLayout = useCallback(
|
|
127
|
+
(event: LayoutChangeEvent) => {
|
|
128
|
+
userOnLayout?.(event)
|
|
129
|
+
if (!layoutId) return
|
|
130
|
+
|
|
131
|
+
const { x, y, width, height } = event.nativeEvent.layout
|
|
132
|
+
const rect: SharedRect = { x, y, width, height }
|
|
133
|
+
lastRectRef.current = rect
|
|
134
|
+
|
|
135
|
+
// First-layout-only: read the registry BEFORE writing our own rect
|
|
136
|
+
// so a previously-released source rect can be consumed cleanly
|
|
137
|
+
// without being overwritten by the current rect first.
|
|
138
|
+
let source: SharedRect | undefined
|
|
139
|
+
if (!consumedRef.current) {
|
|
140
|
+
consumedRef.current = true
|
|
141
|
+
source = consumeLayout(layoutId)
|
|
142
|
+
}
|
|
143
|
+
registerLayout(layoutId, rect)
|
|
144
|
+
|
|
145
|
+
if (source) {
|
|
146
|
+
applyFlip({
|
|
147
|
+
source,
|
|
148
|
+
target: rect,
|
|
149
|
+
dx,
|
|
150
|
+
dy,
|
|
151
|
+
sx,
|
|
152
|
+
sy,
|
|
153
|
+
transition: transitionRef.current,
|
|
154
|
+
shouldReduceMotion: reducedMotionRef.current,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
// dx/dy/sx/sy are stable refs from useSharedValue, but eslint's
|
|
159
|
+
// exhaustive-deps would flag them — including them is harmless and
|
|
160
|
+
// silences the warning.
|
|
161
|
+
[layoutId, userOnLayout, dx, dy, sx, sy],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Reset the first-layout latch when the id changes — a new id is logically
|
|
165
|
+
// a new shared-element identity and should be allowed to consume a source.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
consumedRef.current = false
|
|
168
|
+
}, [layoutId])
|
|
169
|
+
|
|
170
|
+
// On unmount, hand the latest rect to the registry under this id so the
|
|
171
|
+
// next mount can consume it as a FLIP source.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
return () => {
|
|
174
|
+
if (!layoutId) return
|
|
175
|
+
const rect = lastRectRef.current
|
|
176
|
+
if (!rect) return
|
|
177
|
+
releaseLayout(layoutId, rect)
|
|
178
|
+
}
|
|
179
|
+
}, [layoutId])
|
|
180
|
+
|
|
181
|
+
return useMemo<SharedLayoutBindings>(
|
|
182
|
+
() => ({
|
|
183
|
+
flip: { dx, dy, sx, sy },
|
|
184
|
+
setRef,
|
|
185
|
+
onLayout,
|
|
186
|
+
}),
|
|
187
|
+
[dx, dy, sx, sy, setRef, onLayout],
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Snap the FLIP shared values so the new element visually overlays its
|
|
193
|
+
* source rect, then animate back to identity. The `withSequence(snap,
|
|
194
|
+
* animate)` shape is what makes the spring start from the snapped delta —
|
|
195
|
+
* a plain `withSpring(0)` from a zero base would animate from-zero, not
|
|
196
|
+
* from-source.
|
|
197
|
+
*/
|
|
198
|
+
function applyFlip(args: {
|
|
199
|
+
source: SharedRect
|
|
200
|
+
target: SharedRect
|
|
201
|
+
dx: SharedValue<number>
|
|
202
|
+
dy: SharedValue<number>
|
|
203
|
+
sx: SharedValue<number>
|
|
204
|
+
sy: SharedValue<number>
|
|
205
|
+
transition: TransitionConfig | undefined
|
|
206
|
+
shouldReduceMotion: boolean
|
|
207
|
+
}): void {
|
|
208
|
+
const { source, target, dx, dy, sx, sy, transition, shouldReduceMotion } =
|
|
209
|
+
args
|
|
210
|
+
|
|
211
|
+
// Compute the delta that would visually place the new element at the
|
|
212
|
+
// source rect. The transform origin matters: RN scales around the
|
|
213
|
+
// element's center, so the translation needs to account for the
|
|
214
|
+
// center-of-source vs center-of-target offset, not the top-left offset.
|
|
215
|
+
const sourceCenterX = source.x + source.width / 2
|
|
216
|
+
const sourceCenterY = source.y + source.height / 2
|
|
217
|
+
const targetCenterX = target.x + target.width / 2
|
|
218
|
+
const targetCenterY = target.y + target.height / 2
|
|
219
|
+
const deltaX = sourceCenterX - targetCenterX
|
|
220
|
+
const deltaY = sourceCenterY - targetCenterY
|
|
221
|
+
// Guard against zero-sized targets (degenerate layout) — keep scale at 1
|
|
222
|
+
// so the element at least renders even if the FLIP isn't a perfect match.
|
|
223
|
+
const scaleX = target.width > 0 ? source.width / target.width : 1
|
|
224
|
+
const scaleY = target.height > 0 ? source.height / target.height : 1
|
|
225
|
+
|
|
226
|
+
if (shouldReduceMotion) {
|
|
227
|
+
// Reduced-motion: skip the visual transition entirely. The element
|
|
228
|
+
// appears at its natural position; the source rect is discarded.
|
|
229
|
+
dx.value = 0
|
|
230
|
+
dy.value = 0
|
|
231
|
+
sx.value = 1
|
|
232
|
+
sy.value = 1
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (transition?.type === 'no-animation') {
|
|
237
|
+
dx.value = 0
|
|
238
|
+
dy.value = 0
|
|
239
|
+
sx.value = 1
|
|
240
|
+
sy.value = 1
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (transition?.type === 'timing') {
|
|
245
|
+
const duration = transition.duration ?? 300
|
|
246
|
+
dx.value = withSequence(
|
|
247
|
+
withTiming(deltaX, { duration: 0 }),
|
|
248
|
+
withTiming(0, { duration }),
|
|
249
|
+
)
|
|
250
|
+
dy.value = withSequence(
|
|
251
|
+
withTiming(deltaY, { duration: 0 }),
|
|
252
|
+
withTiming(0, { duration }),
|
|
253
|
+
)
|
|
254
|
+
sx.value = withSequence(
|
|
255
|
+
withTiming(scaleX, { duration: 0 }),
|
|
256
|
+
withTiming(1, { duration }),
|
|
257
|
+
)
|
|
258
|
+
sy.value = withSequence(
|
|
259
|
+
withTiming(scaleY, { duration: 0 }),
|
|
260
|
+
withTiming(1, { duration }),
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Spring path (default, and the fallback for `'decay'` which doesn't
|
|
266
|
+
// have a meaningful target value for a FLIP transition).
|
|
267
|
+
const springCfg: SpringTransition =
|
|
268
|
+
transition?.type === 'spring'
|
|
269
|
+
? { ...DEFAULT_SPRING, ...transition }
|
|
270
|
+
: { type: 'spring', ...DEFAULT_SPRING }
|
|
271
|
+
const springParams = springToReanimated(springCfg)
|
|
272
|
+
|
|
273
|
+
dx.value = withSequence(
|
|
274
|
+
withTiming(deltaX, { duration: 0 }),
|
|
275
|
+
withSpring(0, springParams),
|
|
276
|
+
)
|
|
277
|
+
dy.value = withSequence(
|
|
278
|
+
withTiming(deltaY, { duration: 0 }),
|
|
279
|
+
withSpring(0, springParams),
|
|
280
|
+
)
|
|
281
|
+
sx.value = withSequence(
|
|
282
|
+
withTiming(scaleX, { duration: 0 }),
|
|
283
|
+
withSpring(1, springParams),
|
|
284
|
+
)
|
|
285
|
+
sy.value = withSequence(
|
|
286
|
+
withTiming(scaleY, { duration: 0 }),
|
|
287
|
+
withSpring(1, springParams),
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -13,9 +13,14 @@ import Animated, {
|
|
|
13
13
|
useSharedValue,
|
|
14
14
|
type SharedValue,
|
|
15
15
|
} from 'react-native-reanimated'
|
|
16
|
+
import { type LayoutChangeEvent } from 'react-native'
|
|
16
17
|
import { useShouldReduceMotion } from '../config'
|
|
17
18
|
import { isFocusVisible } from '../gestures'
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
resolveLayoutTransition,
|
|
21
|
+
type LayoutProp,
|
|
22
|
+
useSharedLayout,
|
|
23
|
+
} from '../layout'
|
|
19
24
|
import { usePresence } from '../presence'
|
|
20
25
|
import {
|
|
21
26
|
isTopLevelTransition,
|
|
@@ -223,10 +228,31 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
223
228
|
controller,
|
|
224
229
|
gesture,
|
|
225
230
|
layout,
|
|
231
|
+
layoutId,
|
|
226
232
|
onAnimationEnd,
|
|
227
233
|
style,
|
|
234
|
+
onLayout: userOnLayout,
|
|
228
235
|
...rest
|
|
229
|
-
} = props as Props & {
|
|
236
|
+
} = props as Props & {
|
|
237
|
+
style?: unknown
|
|
238
|
+
layout?: LayoutProp
|
|
239
|
+
layoutId?: string
|
|
240
|
+
onLayout?: (event: LayoutChangeEvent) => void
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Function-form `style={(state) => ...}` is the Pressable render-prop API.
|
|
244
|
+
// Inertia drives press/focus state through `gesture.*` and merges its own
|
|
245
|
+
// animated style; a function passed here lands inside a style array where
|
|
246
|
+
// the underlying component never invokes it, so the resulting styles are
|
|
247
|
+
// silently dropped. Throw loudly in dev rather than ship the footgun.
|
|
248
|
+
if (__DEV__ && typeof style === 'function') {
|
|
249
|
+
throw new Error(
|
|
250
|
+
'[inertia] `style` must be a style object or array of style objects, ' +
|
|
251
|
+
'not a function. The function-form `style={(state) => ...}` Pressable ' +
|
|
252
|
+
'API is not supported — use `gesture.pressed` (or `gesture.focused`, ' +
|
|
253
|
+
'etc.) to drive state-dependent styling instead.',
|
|
254
|
+
)
|
|
255
|
+
}
|
|
230
256
|
|
|
231
257
|
// <Presence> contract: when an ancestor flips `isPresent` to false the
|
|
232
258
|
// child stays rendered until `safeToRemove` is called, giving the exit
|
|
@@ -274,43 +300,73 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
274
300
|
const [focusVisible, setFocusVisible] = useState(false)
|
|
275
301
|
const [hovered, setHovered] = useState(false)
|
|
276
302
|
|
|
277
|
-
// The set of keys this instance animates is
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
303
|
+
// The set of keys this instance animates is a *monotonically growing*
|
|
304
|
+
// union, recomputed every render and expanded when a render introduces a
|
|
305
|
+
// key not seen before. It never shrinks. Two requirements meet here:
|
|
306
|
+
//
|
|
307
|
+
// 1. Variants and gesture sub-states contribute the union across *all*
|
|
308
|
+
// their branches up front — a key touched by any variant must be
|
|
309
|
+
// active so the worklet picks it up when the controller transitions
|
|
310
|
+
// to a branch the base `animate` never mentions.
|
|
311
|
+
// 2. A literal `animate` object is reactive: a parent that changes
|
|
312
|
+
// `animate={{ opacity: 1 }}` to `animate={{ opacity: 1, scale: 2 }}`
|
|
313
|
+
// after mount must get `scale` animating. Freezing the set at first
|
|
314
|
+
// render silently dropped the new key (its SV updated, but the
|
|
315
|
+
// worklet — which iterates this set — never read it).
|
|
316
|
+
//
|
|
317
|
+
// Growing-only keeps the worklet stable: the `activeKeysRef.current` array
|
|
318
|
+
// identity only changes on the renders that actually add a key, so the
|
|
319
|
+
// `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
|
|
320
|
+
// expansion without churning frame-to-frame.
|
|
321
|
+
const touched = new Set<AnimatableKey>()
|
|
322
|
+
collectTouchedKeys(touched, animateRecord)
|
|
323
|
+
if (initialRecord) collectTouchedKeys(touched, initialRecord)
|
|
324
|
+
if (variants) {
|
|
325
|
+
for (const variant of Object.values(variants) as object[]) {
|
|
326
|
+
if (!variant) continue
|
|
327
|
+
collectTouchedKeys(touched, variant as Record<string, unknown>)
|
|
293
328
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
329
|
+
}
|
|
330
|
+
if (gesture) {
|
|
331
|
+
for (const subState of [
|
|
332
|
+
gesture.pressed,
|
|
333
|
+
gesture.focused,
|
|
334
|
+
gesture.focusVisible,
|
|
335
|
+
gesture.hovered,
|
|
336
|
+
] as Array<object | undefined>) {
|
|
337
|
+
if (!subState) continue
|
|
338
|
+
collectTouchedKeys(touched, subState as Record<string, unknown>)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (exitRecord) collectTouchedKeys(touched, exitRecord)
|
|
342
|
+
|
|
343
|
+
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
344
|
+
const hasTransformRef = useRef<boolean>(false)
|
|
345
|
+
const hasShadowOffsetRef = useRef<boolean>(false)
|
|
346
|
+
// Expand the active set only when this render touched a key we haven't
|
|
347
|
+
// recorded yet. When nothing new appears we keep the existing array
|
|
348
|
+
// identity so the worklet's captured ref doesn't see a fresh value.
|
|
349
|
+
const prevActive = activeKeysRef.current
|
|
350
|
+
let grew = prevActive === null
|
|
351
|
+
if (!grew && prevActive) {
|
|
352
|
+
for (const k of touched) {
|
|
353
|
+
if (!prevActive.includes(k)) {
|
|
354
|
+
grew = true
|
|
355
|
+
break
|
|
303
356
|
}
|
|
304
357
|
}
|
|
305
|
-
if (exitRecord) collectTouchedKeys(touched, exitRecord)
|
|
306
|
-
activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
|
|
307
358
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
activeKeysRef.current.some((k) =>
|
|
313
|
-
|
|
359
|
+
if (grew) {
|
|
360
|
+
const merged = new Set<AnimatableKey>(prevActive ?? [])
|
|
361
|
+
for (const k of touched) merged.add(k)
|
|
362
|
+
activeKeysRef.current = ALL_KEYS.filter((k) => merged.has(k))
|
|
363
|
+
hasTransformRef.current = activeKeysRef.current.some((k) =>
|
|
364
|
+
TRANSFORM_KEY_SET.has(k),
|
|
365
|
+
)
|
|
366
|
+
hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
|
|
367
|
+
SHADOW_OFFSET_KEY_SET.has(k),
|
|
368
|
+
)
|
|
369
|
+
}
|
|
314
370
|
|
|
315
371
|
const sharedValues = useAnimatableSharedValues((key) => {
|
|
316
372
|
// Shadow offset synthetics seed from the corresponding axis on the
|
|
@@ -531,6 +587,23 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
531
587
|
shouldReduceMotion,
|
|
532
588
|
)
|
|
533
589
|
|
|
590
|
+
// Shared-element transition wiring. `useSharedLayout` allocates FLIP
|
|
591
|
+
// shared values (identity at rest), measures via the merged `onLayout`,
|
|
592
|
+
// and on first-mount snaps the FLIP transform to a source rect popped
|
|
593
|
+
// from the registry. The worklet below appends those entries to the
|
|
594
|
+
// transform array so they compose with the user's animate transforms —
|
|
595
|
+
// multiple `translateX` entries sum, multiple `scaleX` entries multiply,
|
|
596
|
+
// which is exactly the FLIP semantic.
|
|
597
|
+
const sharedLayout = useSharedLayout({
|
|
598
|
+
layoutId,
|
|
599
|
+
userRef: ref,
|
|
600
|
+
transition: isTopLevelTransition(transition) ? transition : undefined,
|
|
601
|
+
shouldReduceMotion,
|
|
602
|
+
userOnLayout,
|
|
603
|
+
})
|
|
604
|
+
const flip = sharedLayout.flip
|
|
605
|
+
const hasLayoutId = layoutId !== undefined
|
|
606
|
+
|
|
534
607
|
const animatedStyle = useAnimatedStyle(() => {
|
|
535
608
|
const activeKeys = activeKeysRef.current!
|
|
536
609
|
const hasTransform = hasTransformRef.current
|
|
@@ -611,7 +684,19 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
611
684
|
out[key] = v
|
|
612
685
|
}
|
|
613
686
|
}
|
|
614
|
-
|
|
687
|
+
// Shared-element FLIP transforms append after the user's transform
|
|
688
|
+
// entries so they compose multiplicatively in the same `transform`
|
|
689
|
+
// array — separate style entries with `transform` keys would
|
|
690
|
+
// last-write-wins, which is what we explicitly avoid here. At rest
|
|
691
|
+
// (dx, dy, sx, sy) = (0, 0, 1, 1) so the contribution is a no-op
|
|
692
|
+
// when no shared-element transition is active.
|
|
693
|
+
if (hasLayoutId) {
|
|
694
|
+
transform.push({ translateX: flip.dx.value })
|
|
695
|
+
transform.push({ translateY: flip.dy.value })
|
|
696
|
+
transform.push({ scaleX: flip.sx.value })
|
|
697
|
+
transform.push({ scaleY: flip.sy.value })
|
|
698
|
+
}
|
|
699
|
+
if (hasTransform || hasLayoutId) out.transform = transform
|
|
615
700
|
if (hasShadowOffset) {
|
|
616
701
|
out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
|
|
617
702
|
}
|
|
@@ -654,9 +739,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
654
739
|
|
|
655
740
|
return (
|
|
656
741
|
<AnimatedComponent
|
|
657
|
-
ref={
|
|
742
|
+
ref={sharedLayout.setRef as never}
|
|
658
743
|
{...(rest as object)}
|
|
659
744
|
{...gestureHandlers}
|
|
745
|
+
onLayout={sharedLayout.onLayout}
|
|
660
746
|
layout={layoutTransition}
|
|
661
747
|
style={mergedStyle}
|
|
662
748
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { reanimatedVersion } from 'react-native-reanimated'
|
|
2
|
+
|
|
1
3
|
declare const __DEV__: boolean
|
|
2
4
|
declare const process: { env?: Record<string, string | undefined> }
|
|
3
|
-
declare const require: (path: string) => unknown
|
|
4
5
|
|
|
5
6
|
let alreadyChecked = false
|
|
6
7
|
|
|
@@ -30,16 +31,11 @@ export function ensureReanimatedInstalled(): void {
|
|
|
30
31
|
}
|
|
31
32
|
alreadyChecked = true
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
version = pkg.version
|
|
39
|
-
} catch {
|
|
40
|
-
// package.json subpath blocked by `exports` field — skip the version
|
|
41
|
-
// probe rather than emit a misleading error.
|
|
42
|
-
}
|
|
34
|
+
// Read the version off Reanimated's own runtime export rather than reaching
|
|
35
|
+
// into its `package.json`. A `require('.../package.json')` here would make
|
|
36
|
+
// esbuild emit a `__require` shim that throws on web bundlers (Expo web), and
|
|
37
|
+
// Reanimated's `exports` field may block the subpath anyway.
|
|
38
|
+
const version: string | undefined = reanimatedVersion
|
|
43
39
|
|
|
44
40
|
if (version) {
|
|
45
41
|
const major = parseInt(version.split('.')[0] ?? '0', 10)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanResponder-backed drag hook. Lives in core because `PanResponder` is
|
|
3
|
+
* built into React Native — no extra peer dependency. Use this when you
|
|
4
|
+
* need keyboard a11y alongside drag, or when you don't want to take
|
|
5
|
+
* `react-native-gesture-handler` as a dependency.
|
|
6
|
+
*
|
|
7
|
+
* For pointer-only drag in a project that already uses gesture-handler,
|
|
8
|
+
* prefer `useDrag` from `@onlynative/inertia-gestures` — its UI-thread
|
|
9
|
+
* release path is more precise.
|
|
10
|
+
*/
|
|
11
|
+
export { useTouchDrag } from './useTouchDrag'
|
|
12
|
+
export type {
|
|
13
|
+
TouchReleaseInfo,
|
|
14
|
+
TouchReleaseResult,
|
|
15
|
+
TouchReleaseTransition,
|
|
16
|
+
UseTouchDragOptions,
|
|
17
|
+
UseTouchDragResult,
|
|
18
|
+
} from './useTouchDrag'
|