@rootnative/impulse 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/chunk-5BMRKYVY.js +39 -0
- package/dist/chunk-F4RHM4ZK.js +77 -0
- package/dist/chunk-FR242SUF.js +174 -0
- package/dist/chunk-IG5RXCYR.js +74 -0
- package/dist/chunk-LM645QQT.js +37 -0
- package/dist/chunk-PMR25UCT.js +8 -0
- package/dist/chunk-ZX7WNICB.js +39 -0
- package/dist/compose/index.d.ts +80 -0
- package/dist/compose/index.js +2 -0
- package/dist/drag/index.d.ts +277 -0
- package/dist/drag/index.js +4 -0
- package/dist/gesture-handler/index.d.ts +1 -0
- package/dist/gesture-handler/index.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/raw/index.d.ts +63 -0
- package/dist/raw/index.js +3 -0
- package/dist/tap/index.d.ts +150 -0
- package/dist/tap/index.js +4 -0
- package/dist/types-Ch2HM3aP.d.ts +142 -0
- package/dist/useGestureMemo-Ccv8rB0C.d.ts +38 -0
- package/jest-preset.cjs +60 -0
- package/jest-setup.cjs +56 -0
- package/package.json +115 -0
- package/src/compose/index.ts +7 -0
- package/src/compose/useGestures.ts +136 -0
- package/src/gesture-handler/index.ts +18 -0
- package/src/index.ts +53 -0
- package/src/intents/drag/index.ts +8 -0
- package/src/intents/tap/index.ts +2 -0
- package/src/intents/useDrag.ts +563 -0
- package/src/intents/useTap.ts +285 -0
- package/src/internal/useGestureMemo.ts +110 -0
- package/src/internal/useLatestCallback.ts +64 -0
- package/src/internal/useStableList.ts +62 -0
- package/src/internal/useStableRecord.ts +72 -0
- package/src/internal/warnOnce.ts +60 -0
- package/src/raw/index.ts +2 -0
- package/src/raw/useRawGesture.ts +71 -0
- package/src/relations/index.ts +120 -0
- package/src/types.ts +187 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Gesture,
|
|
4
|
+
type GestureStateChangeEvent,
|
|
5
|
+
type GestureUpdateEvent,
|
|
6
|
+
type PanGesture,
|
|
7
|
+
type PanGestureHandlerEventPayload,
|
|
8
|
+
} from 'react-native-gesture-handler'
|
|
9
|
+
import {
|
|
10
|
+
runOnJS,
|
|
11
|
+
useSharedValue,
|
|
12
|
+
type SharedValue,
|
|
13
|
+
} from 'react-native-reanimated'
|
|
14
|
+
import {
|
|
15
|
+
useGestureMemo,
|
|
16
|
+
type GestureMemoOptions,
|
|
17
|
+
} from '../internal/useGestureMemo'
|
|
18
|
+
import { useLatestCallback } from '../internal/useLatestCallback'
|
|
19
|
+
import { useStableRecord } from '../internal/useStableRecord'
|
|
20
|
+
import { type HitSlop, type IntentResult, type Point } from '../types'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How far the finger must travel before the drag takes the touch, in points.
|
|
24
|
+
*
|
|
25
|
+
* Impulse's number, not RNGH's: a bare `Gesture.Pan()` activates almost
|
|
26
|
+
* immediately, which is what makes a drag inside a scroll view steal the
|
|
27
|
+
* scroll. A threshold is the single setting that decides whether those two
|
|
28
|
+
* can share a view, so it is set here rather than left to the consumer to
|
|
29
|
+
* discover.
|
|
30
|
+
*
|
|
31
|
+
* **This number is a design intention, not a measurement.** No hardware pass
|
|
32
|
+
* has happened. See Known gaps in CLAUDE.md.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_THRESHOLD = 10
|
|
35
|
+
|
|
36
|
+
/** Which way the drag is allowed to move. */
|
|
37
|
+
export type DragAxis = 'x' | 'y' | 'both'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How far the drag may travel, in the same coordinates as `x` and `y`.
|
|
41
|
+
*
|
|
42
|
+
* Every edge is optional, and an omitted edge is unbounded. The names are the
|
|
43
|
+
* edges of the travel, not of the view: `left` is the smallest `x`, `bottom`
|
|
44
|
+
* is the largest `y`.
|
|
45
|
+
*/
|
|
46
|
+
export interface DragBounds {
|
|
47
|
+
/** Smallest `x`. */
|
|
48
|
+
left?: number
|
|
49
|
+
/** Largest `x`. */
|
|
50
|
+
right?: number
|
|
51
|
+
/** Smallest `y`. */
|
|
52
|
+
top?: number
|
|
53
|
+
/** Largest `y`. */
|
|
54
|
+
bottom?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The intent-shaped payload a {@link useDrag} callback receives. */
|
|
58
|
+
export interface DragEvent {
|
|
59
|
+
/**
|
|
60
|
+
* Where the drag is now — the same numbers as `x` and `y`, after `bounds`
|
|
61
|
+
* and `elastic` were applied.
|
|
62
|
+
*
|
|
63
|
+
* This accumulates across gestures. A second drag continues from where the
|
|
64
|
+
* first one stopped, which is why `bounds` can be written against a layout
|
|
65
|
+
* rather than against one gesture's travel.
|
|
66
|
+
*/
|
|
67
|
+
readonly position: Point
|
|
68
|
+
/**
|
|
69
|
+
* How far the finger moved since this gesture activated, raw.
|
|
70
|
+
*
|
|
71
|
+
* Untouched by `bounds` and `elastic`, and reset to zero at the start of
|
|
72
|
+
* every gesture. Read `position` for where the thing being dragged actually
|
|
73
|
+
* sits.
|
|
74
|
+
*/
|
|
75
|
+
readonly translation: Point
|
|
76
|
+
/** Finger speed, in points per second. The input a release spring needs. */
|
|
77
|
+
readonly velocity: Point
|
|
78
|
+
/** The touch point relative to the window. */
|
|
79
|
+
readonly absolute: Point
|
|
80
|
+
/**
|
|
81
|
+
* The nearest point inside `bounds`. Equal to `position` whenever the drag
|
|
82
|
+
* is in bounds, which with the default `elastic` of `0` is always.
|
|
83
|
+
*
|
|
84
|
+
* With `elastic` set, the finger can pull the value past an edge and
|
|
85
|
+
* Impulse leaves it there on release — moving it back is an animation, and
|
|
86
|
+
* Impulse owns no animation vocabulary. This field is the destination that
|
|
87
|
+
* animation needs, so the consumer does not have to re-derive the clamp
|
|
88
|
+
* from bounds it already handed over.
|
|
89
|
+
*/
|
|
90
|
+
readonly settled: Point
|
|
91
|
+
/** How many fingers are down. */
|
|
92
|
+
readonly pointers: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Options for {@link useDrag}. */
|
|
96
|
+
export interface UseDragOptions extends GestureMemoOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Which way the drag may move. Default `'both'`.
|
|
99
|
+
*
|
|
100
|
+
* The locked axis's shared value never changes — `axis: 'x'` leaves `y` at
|
|
101
|
+
* its initial value for the life of the hook. The axis also decides the
|
|
102
|
+
* activation criterion, which is the part that matters inside a scroll
|
|
103
|
+
* view: see `threshold`.
|
|
104
|
+
*/
|
|
105
|
+
axis?: DragAxis
|
|
106
|
+
/**
|
|
107
|
+
* How far the finger must travel before the drag activates, in points.
|
|
108
|
+
* Default `10`.
|
|
109
|
+
*
|
|
110
|
+
* On a single axis this is a directional threshold, so a drag with
|
|
111
|
+
* `axis: 'x'` ignores vertical movement entirely and a vertical scroll view
|
|
112
|
+
* under it keeps working. On `'both'` it is a radial distance.
|
|
113
|
+
*
|
|
114
|
+
* Lower it for a drag that must feel immediate and owns its view. Raise it
|
|
115
|
+
* when the drag shares the view with something that should usually win.
|
|
116
|
+
*/
|
|
117
|
+
threshold?: number
|
|
118
|
+
/**
|
|
119
|
+
* Movement across the axis that makes the drag fail, in points. Unset by
|
|
120
|
+
* default, which leaves RNGH's own behaviour in place.
|
|
121
|
+
*
|
|
122
|
+
* `threshold` decides when the drag wins; this decides when it gives up.
|
|
123
|
+
* Set it when the cross-axis gesture must win a diagonal — a horizontal
|
|
124
|
+
* row action inside a vertical list, where a mostly-vertical drag should
|
|
125
|
+
* scroll rather than half-open the row. Ignored when `axis` is `'both'`,
|
|
126
|
+
* which has no cross axis.
|
|
127
|
+
*/
|
|
128
|
+
failOffset?: number
|
|
129
|
+
/**
|
|
130
|
+
* How far the drag may travel. Unbounded by default.
|
|
131
|
+
*
|
|
132
|
+
* Written inline as an object is fine — the gesture is not rebuilt when the
|
|
133
|
+
* contents are unchanged.
|
|
134
|
+
*/
|
|
135
|
+
bounds?: DragBounds
|
|
136
|
+
/**
|
|
137
|
+
* How much of the finger's movement survives past a bound, `0` to `1`.
|
|
138
|
+
* Default `0`.
|
|
139
|
+
*
|
|
140
|
+
* `0` clamps hard at the edge. `1` ignores the bound while the finger is
|
|
141
|
+
* down. `0.3` or so gives the rubber-band pull an over-scroll has.
|
|
142
|
+
*
|
|
143
|
+
* **Impulse does not put the value back on release.** That is an animation,
|
|
144
|
+
* and Principle 5 keeps animation out of this library. `onDragEnd` carries
|
|
145
|
+
* `settled` — the point to animate to — and `@rootnative/inertia` or a
|
|
146
|
+
* plain `withSpring` does the moving.
|
|
147
|
+
*/
|
|
148
|
+
elastic?: number
|
|
149
|
+
/**
|
|
150
|
+
* Where `x` and `y` start. Default `{ x: 0, y: 0 }`.
|
|
151
|
+
*
|
|
152
|
+
* Read once, when the hook mounts. Changing it later does nothing, because
|
|
153
|
+
* the shared values are the drag's state from then on — write
|
|
154
|
+
* `drag.x.value` to move it instead.
|
|
155
|
+
*/
|
|
156
|
+
initial?: Point
|
|
157
|
+
/**
|
|
158
|
+
* How many fingers must be on the view. Unset by default, which leaves
|
|
159
|
+
* RNGH's range of one to ten in place.
|
|
160
|
+
*
|
|
161
|
+
* Setting it fixes the count exactly, so `pointers: 2` is a two-finger drag
|
|
162
|
+
* that neither starts with one finger nor survives a third.
|
|
163
|
+
*/
|
|
164
|
+
pointers?: number
|
|
165
|
+
/**
|
|
166
|
+
* Extra touchable area around the view, in points.
|
|
167
|
+
*
|
|
168
|
+
* Written inline as an object is fine — the gesture is not rebuilt when the
|
|
169
|
+
* contents are unchanged.
|
|
170
|
+
*/
|
|
171
|
+
hitSlop?: HitSlop
|
|
172
|
+
/**
|
|
173
|
+
* Whether the gesture is recognized at all. Default `true`.
|
|
174
|
+
*
|
|
175
|
+
* Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
|
|
176
|
+
* keeps its identity and its relations, so re-enabling it does not
|
|
177
|
+
* re-attach anything.
|
|
178
|
+
*/
|
|
179
|
+
enabled?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* The drag passed `threshold` and now owns the touch. **Runs on the JS
|
|
182
|
+
* thread** — Impulse owns the `runOnJS` boundary, so this is an ordinary
|
|
183
|
+
* function and may touch React state.
|
|
184
|
+
*
|
|
185
|
+
* This is the first moment the drag has definitely won. `onBegin` fires
|
|
186
|
+
* earlier and promises nothing.
|
|
187
|
+
*/
|
|
188
|
+
onDragStart?: (event: DragEvent) => void
|
|
189
|
+
/**
|
|
190
|
+
* The finger lifted and the drag is over. **Runs on the JS thread.**
|
|
191
|
+
*
|
|
192
|
+
* Fires only for a drag that activated and then released. A drag the system
|
|
193
|
+
* took away — a competing gesture won, or the app went to the background —
|
|
194
|
+
* reaches `onFinalize` with `success: false` and never gets here, because
|
|
195
|
+
* there was no release and so no velocity worth seeding a spring with.
|
|
196
|
+
*
|
|
197
|
+
* Read `velocity` to seed that release animation, and `settled` for where
|
|
198
|
+
* an elastic overshoot should return to.
|
|
199
|
+
*/
|
|
200
|
+
onDragEnd?: (event: DragEvent) => void
|
|
201
|
+
/**
|
|
202
|
+
* The finger went down and the gesture is now a candidate. **This is a
|
|
203
|
+
* worklet** — mark it with the `'worklet'` directive, and do not touch
|
|
204
|
+
* React state from it.
|
|
205
|
+
*
|
|
206
|
+
* Being a candidate is not the same as winning: the drag has not passed
|
|
207
|
+
* `threshold` yet and may never. Use it to show a grabbed state, and undo
|
|
208
|
+
* that state in `onFinalize`.
|
|
209
|
+
*/
|
|
210
|
+
onBegin?: (event: DragEvent) => void
|
|
211
|
+
/**
|
|
212
|
+
* The drag moved. **This is a worklet**, and it runs on every frame the
|
|
213
|
+
* finger moves.
|
|
214
|
+
*
|
|
215
|
+
* There is no JS-thread counterpart on purpose. A per-frame `runOnJS` is a
|
|
216
|
+
* scheduling cost paid sixty times a second for a value that is already on
|
|
217
|
+
* the thread that needs it — read `x` and `y` from a `useAnimatedStyle`
|
|
218
|
+
* instead, and let this callback handle what the style cannot.
|
|
219
|
+
*/
|
|
220
|
+
onUpdate?: (event: DragEvent) => void
|
|
221
|
+
/**
|
|
222
|
+
* The gesture is over, whether it activated or not. **This is a worklet.**
|
|
223
|
+
*
|
|
224
|
+
* `success` is `true` when the drag activated and ended normally. This is
|
|
225
|
+
* the right place to clear anything `onBegin` set, because it runs on both
|
|
226
|
+
* paths.
|
|
227
|
+
*/
|
|
228
|
+
onFinalize?: (event: DragEvent, success: boolean) => void
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** What {@link useDrag} returns. */
|
|
232
|
+
export interface UseDragResult extends IntentResult<PanGesture> {
|
|
233
|
+
/**
|
|
234
|
+
* Where the drag is along the x axis, after `bounds` and `elastic`.
|
|
235
|
+
*
|
|
236
|
+
* Writable: assigning `drag.x.value` moves the drag, and the next gesture
|
|
237
|
+
* continues from the new number rather than snapping back. That is how a
|
|
238
|
+
* release animation hands control back — animate this value, and the drag
|
|
239
|
+
* picks up wherever the animation left it.
|
|
240
|
+
*
|
|
241
|
+
* Frozen at its initial value when `axis` is `'y'`.
|
|
242
|
+
*/
|
|
243
|
+
readonly x: SharedValue<number>
|
|
244
|
+
/**
|
|
245
|
+
* Where the drag is along the y axis, after `bounds` and `elastic`.
|
|
246
|
+
*
|
|
247
|
+
* Writable, on the same terms as `x`. Frozen at its initial value when
|
|
248
|
+
* `axis` is `'x'`.
|
|
249
|
+
*/
|
|
250
|
+
readonly y: SharedValue<number>
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hold a number inside a range, letting `elastic` of the excess through.
|
|
255
|
+
*
|
|
256
|
+
* A worklet, because the drag's whole value path runs on the UI thread.
|
|
257
|
+
* `elastic` of `0` is a hard clamp and `1` ignores the bound, so the two
|
|
258
|
+
* extremes are the two behaviours a consumer would otherwise write by hand.
|
|
259
|
+
*/
|
|
260
|
+
function resist(
|
|
261
|
+
value: number,
|
|
262
|
+
min: number | undefined,
|
|
263
|
+
max: number | undefined,
|
|
264
|
+
elastic: number,
|
|
265
|
+
): number {
|
|
266
|
+
'worklet'
|
|
267
|
+
if (min !== undefined && value < min) {
|
|
268
|
+
return min + (value - min) * elastic
|
|
269
|
+
}
|
|
270
|
+
if (max !== undefined && value > max) {
|
|
271
|
+
return max + (value - max) * elastic
|
|
272
|
+
}
|
|
273
|
+
return value
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The nearest number inside the range. What {@link resist} would have
|
|
278
|
+
* returned with `elastic` at `0`.
|
|
279
|
+
*/
|
|
280
|
+
function clamp(
|
|
281
|
+
value: number,
|
|
282
|
+
min: number | undefined,
|
|
283
|
+
max: number | undefined,
|
|
284
|
+
): number {
|
|
285
|
+
'worklet'
|
|
286
|
+
if (min !== undefined && value < min) {
|
|
287
|
+
return min
|
|
288
|
+
}
|
|
289
|
+
if (max !== undefined && value > max) {
|
|
290
|
+
return max
|
|
291
|
+
}
|
|
292
|
+
return value
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Recognize a drag, and stream where it is.
|
|
297
|
+
*
|
|
298
|
+
* ```tsx
|
|
299
|
+
* const drag = useDrag({ axis: 'x', bounds: { left: -120, right: 0 } })
|
|
300
|
+
* const style = useAnimatedStyle(() => ({
|
|
301
|
+
* transform: [{ translateX: drag.x.value }],
|
|
302
|
+
* }))
|
|
303
|
+
*
|
|
304
|
+
* return (
|
|
305
|
+
* <GestureDetector gesture={drag.gesture}>
|
|
306
|
+
* <Animated.View style={style} />
|
|
307
|
+
* </GestureDetector>
|
|
308
|
+
* )
|
|
309
|
+
* ```
|
|
310
|
+
*
|
|
311
|
+
* `x` and `y` are shared values, so the view follows the finger on the UI
|
|
312
|
+
* thread with no re-render. They accumulate across gestures: a second drag
|
|
313
|
+
* continues from where the first stopped.
|
|
314
|
+
*
|
|
315
|
+
* **No style, and no animation.** This hook returns numbers and stops there.
|
|
316
|
+
* Building the `transform` is the consumer's call, and so is any spring back.
|
|
317
|
+
* `@rootnative/inertia-gestures` has a `useDrag` that does both, and needs
|
|
318
|
+
* `@rootnative/inertia` to do it — reach for that one when a `Motion.View`
|
|
319
|
+
* should follow a finger and spring home, and for this one when the values
|
|
320
|
+
* are what you want.
|
|
321
|
+
*
|
|
322
|
+
* **Activation criteria.** `threshold` defaults to 10 points. With
|
|
323
|
+
* `axis: 'x'` or `'y'` it is directional, so a horizontal drag inside a
|
|
324
|
+
* vertical `ScrollView` leaves the scroll alone until the finger commits
|
|
325
|
+
* sideways; with `'both'` it is a radial distance. Set `failOffset` as well
|
|
326
|
+
* when a mostly-diagonal move should go to the other gesture rather than to
|
|
327
|
+
* this one. The default has not been measured on hardware yet.
|
|
328
|
+
*
|
|
329
|
+
* **Coexistence.** A threshold decides who moves first; it does not decide
|
|
330
|
+
* who wins a contested touch. For a drag inside a scroll view, say which one
|
|
331
|
+
* the touch belongs to as well — `deferTo: scrollRef` for a drag that is the
|
|
332
|
+
* fallback, `blocks: listRef` for one that is the foreground affordance.
|
|
333
|
+
*
|
|
334
|
+
* **Web.** RNGH recognizes pan from pointer events, so a mouse drag behaves
|
|
335
|
+
* the same as a touch drag and `velocity` is reported in the same units. A
|
|
336
|
+
* trackpad's momentum scroll is not a pan and never reaches this hook.
|
|
337
|
+
* `pointers` above 1 is unreliable on web.
|
|
338
|
+
*
|
|
339
|
+
* **Accessibility.** A drag is invisible to a screen reader and unreachable
|
|
340
|
+
* from a keyboard, and this hook does not fix that. Whatever the drag
|
|
341
|
+
* adjusts must be reachable another way: a pair of buttons for a slider, a
|
|
342
|
+
* visible action for a swipeable row, or `accessibilityActions` with
|
|
343
|
+
* `onAccessibilityAction` — `increment` and `decrement` for a value,
|
|
344
|
+
* `magicTap` or a named action for a dismissal. A drag-only affordance is a
|
|
345
|
+
* bug, not a trade-off.
|
|
346
|
+
*
|
|
347
|
+
* @param options - Activation criteria, bounds, callbacks, and the
|
|
348
|
+
* `alongside` / `blocks` / `deferTo` coexistence options every Impulse hook
|
|
349
|
+
* accepts.
|
|
350
|
+
*/
|
|
351
|
+
export function useDrag(options: UseDragOptions = {}): UseDragResult {
|
|
352
|
+
const {
|
|
353
|
+
axis = 'both',
|
|
354
|
+
threshold = DEFAULT_THRESHOLD,
|
|
355
|
+
failOffset,
|
|
356
|
+
elastic = 0,
|
|
357
|
+
pointers,
|
|
358
|
+
enabled,
|
|
359
|
+
onDragStart,
|
|
360
|
+
onDragEnd,
|
|
361
|
+
onBegin,
|
|
362
|
+
onUpdate,
|
|
363
|
+
onFinalize,
|
|
364
|
+
} = options
|
|
365
|
+
|
|
366
|
+
// Read once. `useSharedValue` keeps its first argument and ignores every
|
|
367
|
+
// later one, and that is the behaviour the option documents: after mount
|
|
368
|
+
// the shared values are the drag's state, and the consumer moves it by
|
|
369
|
+
// writing them.
|
|
370
|
+
const x = useSharedValue(options.initial?.x ?? 0)
|
|
371
|
+
const y = useSharedValue(options.initial?.y ?? 0)
|
|
372
|
+
const isActive = useSharedValue(false)
|
|
373
|
+
// Where `x` and `y` were when this gesture activated, so the position can
|
|
374
|
+
// be rebuilt from the start plus RNGH's per-gesture translation.
|
|
375
|
+
const startX = useSharedValue(0)
|
|
376
|
+
const startY = useSharedValue(0)
|
|
377
|
+
|
|
378
|
+
// The two options a consumer writes as object literals, so the two that
|
|
379
|
+
// would rebuild the gesture every render if taken as-is.
|
|
380
|
+
const bounds = useStableRecord(options.bounds)
|
|
381
|
+
const hitSlop = useStableRecord(options.hitSlop)
|
|
382
|
+
|
|
383
|
+
// Pulled out of `bounds` so the worklets below capture four primitives
|
|
384
|
+
// rather than the record. A captured object is serialized to the UI thread
|
|
385
|
+
// on every rebuild; four numbers are not.
|
|
386
|
+
const left = bounds?.left
|
|
387
|
+
const right = bounds?.right
|
|
388
|
+
const top = bounds?.top
|
|
389
|
+
const bottom = bounds?.bottom
|
|
390
|
+
|
|
391
|
+
const movesX = axis !== 'y'
|
|
392
|
+
const movesY = axis !== 'x'
|
|
393
|
+
|
|
394
|
+
// JS-thread callbacks reach the gesture through stable identities, so they
|
|
395
|
+
// are never gesture dependencies. The worklet callbacks stay direct
|
|
396
|
+
// dependencies, because a worklet is captured as written.
|
|
397
|
+
const handleDragStart = useLatestCallback(onDragStart)
|
|
398
|
+
const handleDragEnd = useLatestCallback(onDragEnd)
|
|
399
|
+
// Attaching a handler is not the same as calling it: RNGH decides which
|
|
400
|
+
// thread a gesture's callbacks run on by inspecting the ones it was given,
|
|
401
|
+
// so the gesture does have to change when a handler appears or disappears.
|
|
402
|
+
// Booleans, so they change only when that is actually true.
|
|
403
|
+
const hasDragStart = onDragStart !== undefined
|
|
404
|
+
const hasDragEnd = onDragEnd !== undefined
|
|
405
|
+
|
|
406
|
+
const built = useGestureMemo(
|
|
407
|
+
'useDrag',
|
|
408
|
+
() => {
|
|
409
|
+
/**
|
|
410
|
+
* Shape RNGH's flat event into the drag payload.
|
|
411
|
+
*
|
|
412
|
+
* Built inside the gesture rather than at module scope because it
|
|
413
|
+
* closes over the bounds — which is also why it is rebuilt only when
|
|
414
|
+
* they change. Called from the callbacks, and only when one is present:
|
|
415
|
+
* a payload nobody reads is six objects allocated on the UI thread for
|
|
416
|
+
* every frame of every drag.
|
|
417
|
+
*/
|
|
418
|
+
const toDragEvent = (
|
|
419
|
+
event:
|
|
420
|
+
| GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
|
421
|
+
| GestureUpdateEvent<PanGestureHandlerEventPayload>,
|
|
422
|
+
): DragEvent => {
|
|
423
|
+
'worklet'
|
|
424
|
+
const position = { x: x.value, y: y.value }
|
|
425
|
+
return {
|
|
426
|
+
position,
|
|
427
|
+
translation: { x: event.translationX, y: event.translationY },
|
|
428
|
+
velocity: { x: event.velocityX, y: event.velocityY },
|
|
429
|
+
absolute: { x: event.absoluteX, y: event.absoluteY },
|
|
430
|
+
settled: {
|
|
431
|
+
x: clamp(position.x, left, right),
|
|
432
|
+
y: clamp(position.y, top, bottom),
|
|
433
|
+
},
|
|
434
|
+
pointers: event.numberOfPointers,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const pan = Gesture.Pan()
|
|
439
|
+
.onBegin((event) => {
|
|
440
|
+
'worklet'
|
|
441
|
+
onBegin?.(toDragEvent(event))
|
|
442
|
+
})
|
|
443
|
+
.onStart((event) => {
|
|
444
|
+
'worklet'
|
|
445
|
+
// The finger has already travelled `threshold` by the time RNGH
|
|
446
|
+
// calls this, and that distance is in `translation` from here on.
|
|
447
|
+
// Subtracting it now means the first `onUpdate` reproduces the
|
|
448
|
+
// current position instead of adding the threshold to it — the
|
|
449
|
+
// difference between a drag that picks the view up where it is and
|
|
450
|
+
// one that jumps ten points before it moves.
|
|
451
|
+
startX.value = x.value - event.translationX
|
|
452
|
+
startY.value = y.value - event.translationY
|
|
453
|
+
isActive.value = true
|
|
454
|
+
if (hasDragStart) {
|
|
455
|
+
runOnJS(handleDragStart)(toDragEvent(event))
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
.onUpdate((event) => {
|
|
459
|
+
'worklet'
|
|
460
|
+
if (movesX) {
|
|
461
|
+
x.value = resist(
|
|
462
|
+
startX.value + event.translationX,
|
|
463
|
+
left,
|
|
464
|
+
right,
|
|
465
|
+
elastic,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
if (movesY) {
|
|
469
|
+
y.value = resist(
|
|
470
|
+
startY.value + event.translationY,
|
|
471
|
+
top,
|
|
472
|
+
bottom,
|
|
473
|
+
elastic,
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
onUpdate?.(toDragEvent(event))
|
|
477
|
+
})
|
|
478
|
+
.onEnd((event, success) => {
|
|
479
|
+
'worklet'
|
|
480
|
+
// Guarded on `success`, because RNGH calls this for a cancelled
|
|
481
|
+
// drag too. A cancelled drag had its touch taken away — by a
|
|
482
|
+
// competing gesture winning, or the app going to the background —
|
|
483
|
+
// and there was no release, so there is no velocity worth seeding a
|
|
484
|
+
// spring with. That path reaches `onFinalize` instead.
|
|
485
|
+
if (success && hasDragEnd) {
|
|
486
|
+
runOnJS(handleDragEnd)(toDragEvent(event))
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
.onFinalize((event, success) => {
|
|
490
|
+
'worklet'
|
|
491
|
+
isActive.value = false
|
|
492
|
+
onFinalize?.(toDragEvent(event), success)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// A directional threshold on a single axis, a radial one on both. The
|
|
496
|
+
// directional form is what lets a horizontal drag and a vertical
|
|
497
|
+
// scroller share a view: vertical movement never reaches the offset, so
|
|
498
|
+
// the drag never claims the touch.
|
|
499
|
+
if (axis === 'x') {
|
|
500
|
+
pan.activeOffsetX([-threshold, threshold])
|
|
501
|
+
} else if (axis === 'y') {
|
|
502
|
+
pan.activeOffsetY([-threshold, threshold])
|
|
503
|
+
} else {
|
|
504
|
+
pan.minDistance(threshold)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Applied conditionally rather than with a default, so an option the
|
|
508
|
+
// consumer did not set leaves RNGH's own default in place instead of
|
|
509
|
+
// Impulse overwriting it with a guess.
|
|
510
|
+
if (failOffset !== undefined) {
|
|
511
|
+
if (axis === 'x') {
|
|
512
|
+
pan.failOffsetY([-failOffset, failOffset])
|
|
513
|
+
} else if (axis === 'y') {
|
|
514
|
+
pan.failOffsetX([-failOffset, failOffset])
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (pointers !== undefined) {
|
|
518
|
+
pan.minPointers(pointers).maxPointers(pointers)
|
|
519
|
+
}
|
|
520
|
+
if (hitSlop !== undefined) {
|
|
521
|
+
pan.hitSlop(hitSlop)
|
|
522
|
+
}
|
|
523
|
+
if (enabled !== undefined) {
|
|
524
|
+
pan.enabled(enabled)
|
|
525
|
+
}
|
|
526
|
+
return pan
|
|
527
|
+
},
|
|
528
|
+
[
|
|
529
|
+
axis,
|
|
530
|
+
threshold,
|
|
531
|
+
failOffset,
|
|
532
|
+
left,
|
|
533
|
+
right,
|
|
534
|
+
top,
|
|
535
|
+
bottom,
|
|
536
|
+
elastic,
|
|
537
|
+
pointers,
|
|
538
|
+
hitSlop,
|
|
539
|
+
enabled,
|
|
540
|
+
movesX,
|
|
541
|
+
movesY,
|
|
542
|
+
hasDragStart,
|
|
543
|
+
hasDragEnd,
|
|
544
|
+
handleDragStart,
|
|
545
|
+
handleDragEnd,
|
|
546
|
+
x,
|
|
547
|
+
y,
|
|
548
|
+
startX,
|
|
549
|
+
startY,
|
|
550
|
+
isActive,
|
|
551
|
+
onBegin,
|
|
552
|
+
onUpdate,
|
|
553
|
+
onFinalize,
|
|
554
|
+
],
|
|
555
|
+
options,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
// Memoised for the same reason `useGestureMemo` memoises its own result: a
|
|
559
|
+
// consumer may put the whole hook result in a dependency list, and a fresh
|
|
560
|
+
// object every render would make that dependency useless. The shared values
|
|
561
|
+
// are stable for the life of the hook, so `built` is the only real input.
|
|
562
|
+
return useMemo(() => ({ ...built, x, y, isActive }), [built, x, y, isActive])
|
|
563
|
+
}
|