@rootnative/impulse 0.0.0-alpha.0 → 0.0.0-alpha.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/CHANGELOG.md +51 -5
- package/README.md +206 -11
- package/dist/{chunk-5BMRKYVY.js → chunk-2UZTAWUQ.js} +20 -2
- package/dist/chunk-BNSFDNLA.js +75 -0
- package/dist/chunk-DXXGWG4Q.js +136 -0
- package/dist/{chunk-PMR25UCT.js → chunk-HGBCIL6X.js} +1 -1
- package/dist/chunk-HI5PHDJY.js +94 -0
- package/dist/{chunk-IG5RXCYR.js → chunk-IH7SQ5X6.js} +11 -15
- package/dist/chunk-MWCIEVTA.js +199 -0
- package/dist/{chunk-F4RHM4ZK.js → chunk-NYDDZD4G.js} +58 -1
- package/dist/chunk-PGOQSKEJ.js +144 -0
- package/dist/{chunk-FR242SUF.js → chunk-TGOGIZDH.js} +13 -7
- package/dist/chunk-VEPUHGPN.js +12 -0
- package/dist/chunk-YZHAQ4XK.js +136 -0
- package/dist/compose/index.d.ts +1 -1
- package/dist/double-tap/index.d.ts +184 -0
- package/dist/double-tap/index.js +5 -0
- package/dist/drag/index.d.ts +18 -13
- package/dist/drag/index.js +3 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +12 -5
- package/dist/long-press/index.d.ts +219 -0
- package/dist/long-press/index.js +4 -0
- package/dist/pan/index.d.ts +221 -0
- package/dist/pan/index.js +4 -0
- package/dist/pinch/index.d.ts +239 -0
- package/dist/pinch/index.js +4 -0
- package/dist/raw/index.d.ts +3 -3
- package/dist/raw/index.js +2 -2
- package/dist/rotate/index.d.ts +263 -0
- package/dist/rotate/index.js +4 -0
- package/dist/swipe/index.d.ts +254 -0
- package/dist/swipe/index.js +4 -0
- package/dist/tap/index.d.ts +34 -29
- package/dist/tap/index.js +4 -3
- package/dist/tapEvent-KSSojt_l.d.ts +28 -0
- package/dist/{types-Ch2HM3aP.d.ts → types-ChGKY28a.d.ts} +27 -1
- package/dist/{useGestureMemo-Ccv8rB0C.d.ts → useGestureMemo-BRW1EKcJ.d.ts} +1 -1
- package/jest-setup.cjs +32 -1
- package/llms.txt +155 -0
- package/package.json +35 -2
- package/src/index.ts +50 -4
- package/src/intents/double-tap/index.ts +6 -0
- package/src/intents/long-press/index.ts +6 -0
- package/src/intents/pan/index.ts +2 -0
- package/src/intents/pinch/index.ts +2 -0
- package/src/intents/rotate/index.ts +6 -0
- package/src/intents/swipe/index.ts +8 -0
- package/src/intents/tapEvent.ts +50 -0
- package/src/intents/useDoubleTap.ts +321 -0
- package/src/intents/useDrag.ts +49 -28
- package/src/intents/useLongPress.ts +391 -0
- package/src/intents/usePan.ts +444 -0
- package/src/intents/usePinch.ts +483 -0
- package/src/intents/useRotate.ts +507 -0
- package/src/intents/useSwipe.ts +585 -0
- package/src/intents/useTap.ts +51 -60
- package/src/internal/intentResult.ts +67 -0
- package/src/internal/phaseCallbacks.ts +51 -0
- package/src/internal/useGestureMemo.ts +32 -4
- package/src/internal/useLatestCallback.ts +2 -2
- package/src/raw/useRawGesture.ts +1 -1
- package/src/relations/index.ts +94 -0
- package/src/types.ts +27 -0
|
@@ -0,0 +1,585 @@
|
|
|
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 { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
10
|
+
import { scheduleOnRN } from 'react-native-worklets'
|
|
11
|
+
import {
|
|
12
|
+
useGestureMemo,
|
|
13
|
+
type GestureMemoOptions,
|
|
14
|
+
} from '../internal/useGestureMemo'
|
|
15
|
+
import { buildIntentResult } from '../internal/intentResult'
|
|
16
|
+
import { useLatestCallback } from '../internal/useLatestCallback'
|
|
17
|
+
import { useStableList } from '../internal/useStableList'
|
|
18
|
+
import { useStableRecord } from '../internal/useStableRecord'
|
|
19
|
+
import {
|
|
20
|
+
type HitSlop,
|
|
21
|
+
type IntentEndInfo,
|
|
22
|
+
type IntentResult,
|
|
23
|
+
type Point,
|
|
24
|
+
} from '../types'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How far the finger must travel before the swipe takes the touch, in points.
|
|
28
|
+
* The same number `useDrag` and `usePan` use.
|
|
29
|
+
*
|
|
30
|
+
* **This number is a design intention, not a measurement.** The device sweep
|
|
31
|
+
* of 2026-09-19 did not test it. See Known gaps in docs/docs/roadmap.md.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_THRESHOLD = 10
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How far the finger must travel for a release to commit, in points.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately far above `threshold`: passing the threshold means the swipe
|
|
39
|
+
* is tracking, and passing this means the user meant it.
|
|
40
|
+
*
|
|
41
|
+
* **Unmeasured**, like every other default here.
|
|
42
|
+
*/
|
|
43
|
+
const DEFAULT_COMMIT_DISTANCE = 80
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How fast the finger must be moving for a release to commit, in points per
|
|
47
|
+
* second — the flick that commits without travelling `commitDistance`.
|
|
48
|
+
*
|
|
49
|
+
* **Unmeasured.**
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_COMMIT_SPEED = 800
|
|
52
|
+
|
|
53
|
+
/** Which way a swipe went. The same vocabulary as the `directions` option. */
|
|
54
|
+
export type SwipeDirection = 'left' | 'right' | 'up' | 'down'
|
|
55
|
+
|
|
56
|
+
/** Every direction, which is what an unset `directions` means. */
|
|
57
|
+
const ALL_DIRECTIONS: readonly SwipeDirection[] = [
|
|
58
|
+
'left',
|
|
59
|
+
'right',
|
|
60
|
+
'up',
|
|
61
|
+
'down',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/** The intent-shaped payload a {@link useSwipe} callback receives. */
|
|
65
|
+
export interface SwipeEvent {
|
|
66
|
+
/**
|
|
67
|
+
* Which way the swipe went, or `null` when the release did not commit.
|
|
68
|
+
*
|
|
69
|
+
* `onSwipe` receives a payload whose direction is never `null` — that is
|
|
70
|
+
* the whole meaning of that callback. `onSwipeEnd` fires on every release
|
|
71
|
+
* of a swipe that activated, so it is the one that has to read this.
|
|
72
|
+
*/
|
|
73
|
+
readonly direction: SwipeDirection | null
|
|
74
|
+
/**
|
|
75
|
+
* How far the finger travelled along the dominant axis, in points, always
|
|
76
|
+
* positive.
|
|
77
|
+
*
|
|
78
|
+
* Compare it against `commitDistance` to see how close a release that did
|
|
79
|
+
* not commit came.
|
|
80
|
+
*/
|
|
81
|
+
readonly distance: number
|
|
82
|
+
/**
|
|
83
|
+
* How fast the finger was moving along the dominant axis, in points per
|
|
84
|
+
* second, always positive. The scalar counterpart of `velocity`, and what
|
|
85
|
+
* `commitSpeed` is compared against.
|
|
86
|
+
*/
|
|
87
|
+
readonly speed: number
|
|
88
|
+
/**
|
|
89
|
+
* How far the finger moved since the swipe activated, signed and per axis.
|
|
90
|
+
*
|
|
91
|
+
* Measured from the activation point, so it does not include the
|
|
92
|
+
* `threshold` travel the finger spent before the swipe existed.
|
|
93
|
+
*/
|
|
94
|
+
readonly translation: Point
|
|
95
|
+
/** Finger speed, in points per second, signed and per axis. */
|
|
96
|
+
readonly velocity: Point
|
|
97
|
+
/** The touch point relative to the window. */
|
|
98
|
+
readonly absolute: Point
|
|
99
|
+
/** How many fingers are down. */
|
|
100
|
+
readonly pointers: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A {@link SwipeEvent} that committed, so its direction is known.
|
|
105
|
+
*
|
|
106
|
+
* `onSwipe` takes this rather than `SwipeEvent`, which is what saves every
|
|
107
|
+
* consumer of that callback a null check for a case it cannot be in.
|
|
108
|
+
*/
|
|
109
|
+
export type CommittedSwipeEvent = SwipeEvent & {
|
|
110
|
+
readonly direction: SwipeDirection
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Options for {@link useSwipe}. */
|
|
114
|
+
export interface UseSwipeOptions extends GestureMemoOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Which directions may commit. Every direction by default.
|
|
117
|
+
*
|
|
118
|
+
* **This also sets the activation criterion**, which is the reason to
|
|
119
|
+
* narrow it even when the extra directions would never fire. A list that is
|
|
120
|
+
* entirely horizontal gives the gesture a directional threshold on the x
|
|
121
|
+
* axis, so a vertical scroll view under it keeps working; an entirely
|
|
122
|
+
* vertical list does the same on y. A mixed list has no axis to lock, so
|
|
123
|
+
* the threshold is radial and the swipe competes with a scroller for every
|
|
124
|
+
* touch.
|
|
125
|
+
*
|
|
126
|
+
* Written inline as an array is fine — the gesture is not rebuilt when the
|
|
127
|
+
* contents are unchanged.
|
|
128
|
+
*/
|
|
129
|
+
directions?: readonly SwipeDirection[]
|
|
130
|
+
/**
|
|
131
|
+
* How far the finger must travel before the swipe activates and starts
|
|
132
|
+
* tracking, in points. Default `10`.
|
|
133
|
+
*
|
|
134
|
+
* This is not the commit test. It is the point at which the swipe takes the
|
|
135
|
+
* touch and `x` and `y` start reporting — see `commitDistance` for what
|
|
136
|
+
* decides that a release counts.
|
|
137
|
+
*/
|
|
138
|
+
threshold?: number
|
|
139
|
+
/**
|
|
140
|
+
* How far the finger must travel for a release to commit, in points.
|
|
141
|
+
* Default `80`.
|
|
142
|
+
*
|
|
143
|
+
* Measured along the dominant axis, from the activation point.
|
|
144
|
+
*/
|
|
145
|
+
commitDistance?: number
|
|
146
|
+
/**
|
|
147
|
+
* How fast the finger must be moving for a release to commit, in points per
|
|
148
|
+
* second. Default `800`.
|
|
149
|
+
*
|
|
150
|
+
* The flick: a release this fast commits even when it never travelled
|
|
151
|
+
* `commitDistance`. Either test is enough on its own.
|
|
152
|
+
*/
|
|
153
|
+
commitSpeed?: number
|
|
154
|
+
/**
|
|
155
|
+
* Movement across the axis that makes the swipe fail, in points. Unset by
|
|
156
|
+
* default, which leaves RNGH's own behaviour in place.
|
|
157
|
+
*
|
|
158
|
+
* Ignored when `directions` has no single axis, which has no cross axis.
|
|
159
|
+
*/
|
|
160
|
+
failOffset?: number
|
|
161
|
+
/**
|
|
162
|
+
* How many fingers must be on the view. Unset by default, which leaves
|
|
163
|
+
* RNGH's range of one to ten in place.
|
|
164
|
+
*/
|
|
165
|
+
pointers?: number
|
|
166
|
+
/**
|
|
167
|
+
* Extra touchable area around the view, in points.
|
|
168
|
+
*
|
|
169
|
+
* Written inline as an object is fine — the gesture is not rebuilt when the
|
|
170
|
+
* contents are unchanged.
|
|
171
|
+
*/
|
|
172
|
+
hitSlop?: HitSlop
|
|
173
|
+
/**
|
|
174
|
+
* Whether the gesture is recognized at all. Default `true`.
|
|
175
|
+
*
|
|
176
|
+
* Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
|
|
177
|
+
* keeps its identity and its relations, so re-enabling it does not
|
|
178
|
+
* re-attach anything.
|
|
179
|
+
*/
|
|
180
|
+
enabled?: boolean
|
|
181
|
+
/**
|
|
182
|
+
* The swipe committed. **Runs on the JS thread** — Impulse owns the
|
|
183
|
+
* `scheduleOnRN` boundary, so this is an ordinary function and may touch
|
|
184
|
+
* React state.
|
|
185
|
+
*
|
|
186
|
+
* This is the callback almost every consumer wants, and it fires only for
|
|
187
|
+
* the thing it is named after: a release that passed `commitDistance` or
|
|
188
|
+
* `commitSpeed` in a direction `directions` allows. It never fires for a
|
|
189
|
+
* cancelled gesture, because a swipe the system took away did not happen.
|
|
190
|
+
*
|
|
191
|
+
* `event.direction` is never `null` here.
|
|
192
|
+
*/
|
|
193
|
+
onSwipe?: (event: CommittedSwipeEvent) => void
|
|
194
|
+
/**
|
|
195
|
+
* The swipe ended, committed or not. **Runs on the JS thread.**
|
|
196
|
+
*
|
|
197
|
+
* Fires on every release of a swipe that activated, which is what a view
|
|
198
|
+
* that followed the finger needs: `onSwipe` says to act, and this says to
|
|
199
|
+
* put the view back. Read `event.direction` for whether it committed —
|
|
200
|
+
* `null` is the release that did not.
|
|
201
|
+
*
|
|
202
|
+
* It fires for both endings, and `cancelled` says which. A cancelled
|
|
203
|
+
* gesture never commits, so its direction is always `null`.
|
|
204
|
+
*/
|
|
205
|
+
onSwipeEnd?: (event: SwipeEvent, info: IntentEndInfo) => void
|
|
206
|
+
/**
|
|
207
|
+
* The finger went down and the gesture is now a candidate. **This is a
|
|
208
|
+
* worklet** — mark it with the `'worklet'` directive, and do not touch
|
|
209
|
+
* React state from it.
|
|
210
|
+
*
|
|
211
|
+
* The swipe has not passed `threshold` yet and may never. Undo whatever it
|
|
212
|
+
* sets in `onFinalize`.
|
|
213
|
+
*/
|
|
214
|
+
onBegin?: (event: SwipeEvent) => void
|
|
215
|
+
/**
|
|
216
|
+
* The finger moved. **This is a worklet**, and it runs on every frame.
|
|
217
|
+
*
|
|
218
|
+
* `direction` is `null` on every one of these: a swipe is decided at
|
|
219
|
+
* release, not while the finger is down. Read `translation` to show the
|
|
220
|
+
* view following, and `distance` to show how close a commit is.
|
|
221
|
+
*/
|
|
222
|
+
onUpdate?: (event: SwipeEvent) => void
|
|
223
|
+
/**
|
|
224
|
+
* The gesture is over, whether it activated or not. **This is a worklet.**
|
|
225
|
+
*
|
|
226
|
+
* `success` is `true` when the swipe activated and ended normally — which
|
|
227
|
+
* is not the same as committing. A release that stopped short is a
|
|
228
|
+
* successful gesture with no direction.
|
|
229
|
+
*/
|
|
230
|
+
onFinalize?: (event: SwipeEvent, success: boolean) => void
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** What {@link useSwipe} returns. */
|
|
234
|
+
export interface UseSwipeResult extends IntentResult<PanGesture> {
|
|
235
|
+
/**
|
|
236
|
+
* How far the finger has moved along the x axis since the swipe activated.
|
|
237
|
+
*
|
|
238
|
+
* **This is movement, not a position**, on the same terms as `usePan`: it
|
|
239
|
+
* is zeroed at the start of every gesture and keeps its final number after
|
|
240
|
+
* the gesture ends, so a release animation has something to animate from.
|
|
241
|
+
*
|
|
242
|
+
* Impulse does not put it back. That is an animation, and Principle 5 keeps
|
|
243
|
+
* animation out of this library — `onSwipeEnd` is where a consumer springs
|
|
244
|
+
* it home or off the screen.
|
|
245
|
+
*/
|
|
246
|
+
readonly x: SharedValue<number>
|
|
247
|
+
/**
|
|
248
|
+
* How far the finger has moved along the y axis since the swipe activated.
|
|
249
|
+
* Same terms as `x`.
|
|
250
|
+
*/
|
|
251
|
+
readonly y: SharedValue<number>
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Decide which direction a release commits to, or `null` for one that does
|
|
256
|
+
* not.
|
|
257
|
+
*
|
|
258
|
+
* The dominant axis is the one the finger travelled furthest along, and only
|
|
259
|
+
* that axis is tested — a release is one swipe, not two. Either test passes
|
|
260
|
+
* on its own: far enough, or fast enough.
|
|
261
|
+
*
|
|
262
|
+
* A dominant axis whose direction is not allowed returns `null` rather than
|
|
263
|
+
* falling back to the other axis. A swipe that went mostly down is not an
|
|
264
|
+
* left swipe, however far sideways it also drifted.
|
|
265
|
+
*/
|
|
266
|
+
function pickDirection(
|
|
267
|
+
translationX: number,
|
|
268
|
+
translationY: number,
|
|
269
|
+
velocityX: number,
|
|
270
|
+
velocityY: number,
|
|
271
|
+
commitDistance: number,
|
|
272
|
+
commitSpeed: number,
|
|
273
|
+
allowLeft: boolean,
|
|
274
|
+
allowRight: boolean,
|
|
275
|
+
allowUp: boolean,
|
|
276
|
+
allowDown: boolean,
|
|
277
|
+
): SwipeDirection | null {
|
|
278
|
+
'worklet'
|
|
279
|
+
const distanceX = Math.abs(translationX)
|
|
280
|
+
const distanceY = Math.abs(translationY)
|
|
281
|
+
if (distanceX >= distanceY) {
|
|
282
|
+
if (distanceX < commitDistance && Math.abs(velocityX) < commitSpeed) {
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
if (translationX < 0) {
|
|
286
|
+
return allowLeft ? 'left' : null
|
|
287
|
+
}
|
|
288
|
+
if (translationX > 0) {
|
|
289
|
+
return allowRight ? 'right' : null
|
|
290
|
+
}
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
if (distanceY < commitDistance && Math.abs(velocityY) < commitSpeed) {
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
if (translationY < 0) {
|
|
297
|
+
return allowUp ? 'up' : null
|
|
298
|
+
}
|
|
299
|
+
if (translationY > 0) {
|
|
300
|
+
return allowDown ? 'down' : null
|
|
301
|
+
}
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Recognize a directional swipe.
|
|
307
|
+
*
|
|
308
|
+
* ```tsx
|
|
309
|
+
* const swipe = useSwipe({
|
|
310
|
+
* directions: ['left'],
|
|
311
|
+
* onSwipe: () => archive(item.id),
|
|
312
|
+
* onSwipeEnd: () => {
|
|
313
|
+
* swipe.x.value = withSpring(0)
|
|
314
|
+
* },
|
|
315
|
+
* })
|
|
316
|
+
*
|
|
317
|
+
* return (
|
|
318
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
319
|
+
* <Animated.View style={style} />
|
|
320
|
+
* </GestureDetector>
|
|
321
|
+
* )
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* A swipe is a pan that is judged at release. The finger moves, `x` and `y`
|
|
325
|
+
* report it so the view can follow, and on release the travel and the speed
|
|
326
|
+
* along the dominant axis decide whether it counted. `onSwipe` fires only for
|
|
327
|
+
* a release that counted, in a direction `directions` allows.
|
|
328
|
+
*
|
|
329
|
+
* **Narrowing `directions` is the coexistence setting, not only a filter.**
|
|
330
|
+
* An all-horizontal list gives the gesture a directional threshold on x, so a
|
|
331
|
+
* vertical scroll view under it keeps working without a relation. A mixed
|
|
332
|
+
* list has no axis to lock, so the threshold is radial and the swipe competes
|
|
333
|
+
* for every touch — declare `deferTo` or `blocks` there.
|
|
334
|
+
*
|
|
335
|
+
* **Two thresholds, and they mean different things.** `threshold` is when the
|
|
336
|
+
* swipe takes the touch and starts reporting. `commitDistance` and
|
|
337
|
+
* `commitSpeed` are what a release is measured against. A swipe that
|
|
338
|
+
* activates and stops short reaches `onSwipeEnd` with a `null` direction, and
|
|
339
|
+
* never reaches `onSwipe`.
|
|
340
|
+
*
|
|
341
|
+
* **No style, and no animation.** This hook returns numbers and stops there.
|
|
342
|
+
* Whether a committed row leaves the screen and an uncommitted one springs
|
|
343
|
+
* back is the consumer's decision, taken in `onSwipeEnd`.
|
|
344
|
+
* `@rootnative/inertia-gestures` has a `useSwipe` that owns both, and needs
|
|
345
|
+
* `@rootnative/inertia` to do it.
|
|
346
|
+
*
|
|
347
|
+
* **Web.** RNGH recognizes pan from pointer events, so a mouse drag commits
|
|
348
|
+
* the same way a touch does, and `speed` is reported in the same units. A
|
|
349
|
+
* trackpad's two-finger swipe is a scroll, not a pan, and never reaches this
|
|
350
|
+
* hook.
|
|
351
|
+
*
|
|
352
|
+
* **Accessibility.** A swipe is invisible to a screen reader and unreachable
|
|
353
|
+
* from a keyboard. Whatever it commits must be reachable another way: a
|
|
354
|
+
* visible button for a row action, a paging control for a carousel, or
|
|
355
|
+
* `accessibilityActions` with `onAccessibilityAction`. A swipe-only action is
|
|
356
|
+
* a bug, not a trade-off.
|
|
357
|
+
*
|
|
358
|
+
* @param options - Directions, activation and commit criteria, callbacks, and
|
|
359
|
+
* the `alongside` / `blocks` / `deferTo` coexistence options every Impulse
|
|
360
|
+
* hook accepts.
|
|
361
|
+
*/
|
|
362
|
+
export function useSwipe(options: UseSwipeOptions = {}): UseSwipeResult {
|
|
363
|
+
const {
|
|
364
|
+
threshold = DEFAULT_THRESHOLD,
|
|
365
|
+
commitDistance = DEFAULT_COMMIT_DISTANCE,
|
|
366
|
+
commitSpeed = DEFAULT_COMMIT_SPEED,
|
|
367
|
+
failOffset,
|
|
368
|
+
pointers,
|
|
369
|
+
enabled,
|
|
370
|
+
onSwipe,
|
|
371
|
+
onSwipeEnd,
|
|
372
|
+
onBegin,
|
|
373
|
+
onUpdate,
|
|
374
|
+
onFinalize,
|
|
375
|
+
} = options
|
|
376
|
+
|
|
377
|
+
const x = useSharedValue(0)
|
|
378
|
+
const y = useSharedValue(0)
|
|
379
|
+
const isActive = useSharedValue(false)
|
|
380
|
+
// RNGH's translation at the moment the swipe activated. Every number this
|
|
381
|
+
// hook reports is measured from here, so the `threshold` travel is not
|
|
382
|
+
// counted towards `commitDistance`.
|
|
383
|
+
const originX = useSharedValue(0)
|
|
384
|
+
const originY = useSharedValue(0)
|
|
385
|
+
|
|
386
|
+
const hitSlop = useStableRecord(options.hitSlop)
|
|
387
|
+
// Written inline at almost every call site, so compared by content rather
|
|
388
|
+
// than by identity — an array literal would otherwise rebuild the gesture
|
|
389
|
+
// on every render.
|
|
390
|
+
const directions = useStableList(options.directions ?? ALL_DIRECTIONS)
|
|
391
|
+
|
|
392
|
+
// Pulled apart into booleans so the commit worklet captures four primitives
|
|
393
|
+
// rather than the array. A captured array is serialized to the UI thread on
|
|
394
|
+
// every rebuild; four booleans are not.
|
|
395
|
+
const allowLeft = directions.includes('left')
|
|
396
|
+
const allowRight = directions.includes('right')
|
|
397
|
+
const allowUp = directions.includes('up')
|
|
398
|
+
const allowDown = directions.includes('down')
|
|
399
|
+
|
|
400
|
+
// The axis the directions live on, or `null` for a mixed list. This is what
|
|
401
|
+
// turns `directions` into an activation criterion: an axis means a
|
|
402
|
+
// directional threshold, which is what lets the swipe share a view with a
|
|
403
|
+
// scroller. A list with no allowed direction at all has no axis either, and
|
|
404
|
+
// falls back to the radial form — the gesture then tracks and never
|
|
405
|
+
// commits, which is what an empty list asks for.
|
|
406
|
+
const hasHorizontal = allowLeft || allowRight
|
|
407
|
+
const hasVertical = allowUp || allowDown
|
|
408
|
+
const axis =
|
|
409
|
+
hasHorizontal && !hasVertical
|
|
410
|
+
? 'x'
|
|
411
|
+
: hasVertical && !hasHorizontal
|
|
412
|
+
? 'y'
|
|
413
|
+
: null
|
|
414
|
+
|
|
415
|
+
// JS-thread callbacks reach the gesture through stable identities, so they
|
|
416
|
+
// are never gesture dependencies.
|
|
417
|
+
const handleSwipe = useLatestCallback(onSwipe)
|
|
418
|
+
const handleSwipeEnd = useLatestCallback(onSwipeEnd)
|
|
419
|
+
// Attaching a handler is not the same as calling it: RNGH decides which
|
|
420
|
+
// thread a gesture's callbacks run on by inspecting the ones it was given.
|
|
421
|
+
const hasSwipe = onSwipe !== undefined
|
|
422
|
+
const hasSwipeEnd = onSwipeEnd !== undefined
|
|
423
|
+
|
|
424
|
+
const built = useGestureMemo(
|
|
425
|
+
'useSwipe',
|
|
426
|
+
() => {
|
|
427
|
+
/**
|
|
428
|
+
* Shape RNGH's flat event into the swipe payload.
|
|
429
|
+
*
|
|
430
|
+
* `direction` is passed in rather than derived here: it is decided once
|
|
431
|
+
* per release, and a payload built for `onUpdate` has no release to
|
|
432
|
+
* judge.
|
|
433
|
+
*/
|
|
434
|
+
const toSwipeEvent = (
|
|
435
|
+
event:
|
|
436
|
+
| GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
|
437
|
+
| GestureUpdateEvent<PanGestureHandlerEventPayload>,
|
|
438
|
+
direction: SwipeDirection | null,
|
|
439
|
+
): SwipeEvent => {
|
|
440
|
+
'worklet'
|
|
441
|
+
const translationX = x.value
|
|
442
|
+
const translationY = y.value
|
|
443
|
+
const horizontal = Math.abs(translationX) >= Math.abs(translationY)
|
|
444
|
+
return {
|
|
445
|
+
direction,
|
|
446
|
+
distance: Math.abs(horizontal ? translationX : translationY),
|
|
447
|
+
speed: Math.abs(horizontal ? event.velocityX : event.velocityY),
|
|
448
|
+
translation: { x: translationX, y: translationY },
|
|
449
|
+
velocity: { x: event.velocityX, y: event.velocityY },
|
|
450
|
+
absolute: { x: event.absoluteX, y: event.absoluteY },
|
|
451
|
+
pointers: event.numberOfPointers,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const pan = Gesture.Pan()
|
|
456
|
+
.onBegin((event) => {
|
|
457
|
+
'worklet'
|
|
458
|
+
onBegin?.(toSwipeEvent(event, null))
|
|
459
|
+
})
|
|
460
|
+
.onStart((event) => {
|
|
461
|
+
'worklet'
|
|
462
|
+
originX.value = event.translationX
|
|
463
|
+
originY.value = event.translationY
|
|
464
|
+
x.value = 0
|
|
465
|
+
y.value = 0
|
|
466
|
+
isActive.value = true
|
|
467
|
+
})
|
|
468
|
+
.onUpdate((event) => {
|
|
469
|
+
'worklet'
|
|
470
|
+
x.value = event.translationX - originX.value
|
|
471
|
+
y.value = event.translationY - originY.value
|
|
472
|
+
onUpdate?.(toSwipeEvent(event, null))
|
|
473
|
+
})
|
|
474
|
+
.onEnd((event, success) => {
|
|
475
|
+
'worklet'
|
|
476
|
+
// A cancelled gesture never commits. The finger did not lift, so
|
|
477
|
+
// the velocity describes the last movement rather than a release,
|
|
478
|
+
// and committing on it would act on a swipe the user never
|
|
479
|
+
// finished.
|
|
480
|
+
const direction = success
|
|
481
|
+
? pickDirection(
|
|
482
|
+
x.value,
|
|
483
|
+
y.value,
|
|
484
|
+
event.velocityX,
|
|
485
|
+
event.velocityY,
|
|
486
|
+
commitDistance,
|
|
487
|
+
commitSpeed,
|
|
488
|
+
allowLeft,
|
|
489
|
+
allowRight,
|
|
490
|
+
allowUp,
|
|
491
|
+
allowDown,
|
|
492
|
+
)
|
|
493
|
+
: null
|
|
494
|
+
if (direction !== null && hasSwipe) {
|
|
495
|
+
scheduleOnRN(
|
|
496
|
+
handleSwipe,
|
|
497
|
+
toSwipeEvent(event, direction) as CommittedSwipeEvent,
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
// Fires on both endings, which is what a view that followed the
|
|
501
|
+
// finger needs: `onSwipe` says to act, this says where to put the
|
|
502
|
+
// view. RNGH reaches `onEnd` only from the ACTIVE state, so a touch
|
|
503
|
+
// that never passed the threshold never gets here.
|
|
504
|
+
if (hasSwipeEnd) {
|
|
505
|
+
scheduleOnRN(handleSwipeEnd, toSwipeEvent(event, direction), {
|
|
506
|
+
cancelled: !success,
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
.onFinalize((event, success) => {
|
|
511
|
+
'worklet'
|
|
512
|
+
isActive.value = false
|
|
513
|
+
onFinalize?.(toSwipeEvent(event, null), success)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// The directional threshold the allowed directions imply. This is the
|
|
517
|
+
// part that lets a horizontal swipe live inside a vertical scroller:
|
|
518
|
+
// vertical movement never reaches the offset, so the swipe never claims
|
|
519
|
+
// the touch.
|
|
520
|
+
if (axis === 'x') {
|
|
521
|
+
pan.activeOffsetX([-threshold, threshold])
|
|
522
|
+
} else if (axis === 'y') {
|
|
523
|
+
pan.activeOffsetY([-threshold, threshold])
|
|
524
|
+
} else {
|
|
525
|
+
pan.minDistance(threshold)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Applied conditionally rather than with a default, so an option the
|
|
529
|
+
// consumer did not set leaves RNGH's own default in place instead of
|
|
530
|
+
// Impulse overwriting it with a guess.
|
|
531
|
+
if (failOffset !== undefined) {
|
|
532
|
+
if (axis === 'x') {
|
|
533
|
+
pan.failOffsetY([-failOffset, failOffset])
|
|
534
|
+
} else if (axis === 'y') {
|
|
535
|
+
pan.failOffsetX([-failOffset, failOffset])
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (pointers !== undefined) {
|
|
539
|
+
pan.minPointers(pointers).maxPointers(pointers)
|
|
540
|
+
}
|
|
541
|
+
if (hitSlop !== undefined) {
|
|
542
|
+
pan.hitSlop(hitSlop)
|
|
543
|
+
}
|
|
544
|
+
if (enabled !== undefined) {
|
|
545
|
+
pan.enabled(enabled)
|
|
546
|
+
}
|
|
547
|
+
return pan
|
|
548
|
+
},
|
|
549
|
+
[
|
|
550
|
+
axis,
|
|
551
|
+
threshold,
|
|
552
|
+
commitDistance,
|
|
553
|
+
commitSpeed,
|
|
554
|
+
failOffset,
|
|
555
|
+
pointers,
|
|
556
|
+
hitSlop,
|
|
557
|
+
enabled,
|
|
558
|
+
allowLeft,
|
|
559
|
+
allowRight,
|
|
560
|
+
allowUp,
|
|
561
|
+
allowDown,
|
|
562
|
+
hasSwipe,
|
|
563
|
+
hasSwipeEnd,
|
|
564
|
+
handleSwipe,
|
|
565
|
+
handleSwipeEnd,
|
|
566
|
+
x,
|
|
567
|
+
y,
|
|
568
|
+
originX,
|
|
569
|
+
originY,
|
|
570
|
+
isActive,
|
|
571
|
+
onBegin,
|
|
572
|
+
onUpdate,
|
|
573
|
+
onFinalize,
|
|
574
|
+
],
|
|
575
|
+
options,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
// Memoised so a consumer can put the whole hook result in a dependency
|
|
579
|
+
// list. The shared values are stable for the life of the hook, so `built`
|
|
580
|
+
// is the only real input.
|
|
581
|
+
return useMemo(
|
|
582
|
+
() => buildIntentResult(built, { x, y, isActive }),
|
|
583
|
+
[built, x, y, isActive],
|
|
584
|
+
)
|
|
585
|
+
}
|