@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,483 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Gesture,
|
|
4
|
+
type GestureStateChangeEvent,
|
|
5
|
+
type GestureUpdateEvent,
|
|
6
|
+
type PinchGesture,
|
|
7
|
+
type PinchGestureHandlerEventPayload,
|
|
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 { useStableRecord } from '../internal/useStableRecord'
|
|
18
|
+
import {
|
|
19
|
+
type HitSlop,
|
|
20
|
+
type IntentEndInfo,
|
|
21
|
+
type IntentResult,
|
|
22
|
+
type Point,
|
|
23
|
+
} from '../types'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The scale a pinch starts from when the consumer sets no `initial`.
|
|
27
|
+
*
|
|
28
|
+
* `1` is the only defensible value: a scale is a multiplier on the view's own
|
|
29
|
+
* size, so the identity is the thing that has not been zoomed.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_INITIAL_SCALE = 1
|
|
32
|
+
|
|
33
|
+
/** The intent-shaped payload a {@link usePinch} callback receives. */
|
|
34
|
+
export interface PinchEvent {
|
|
35
|
+
/**
|
|
36
|
+
* How much the thing is scaled now — the same number as `scale`, after
|
|
37
|
+
* `min`, `max` and `elastic` were applied.
|
|
38
|
+
*
|
|
39
|
+
* This accumulates across gestures. A second pinch continues from where the
|
|
40
|
+
* first one stopped, which is why `min` and `max` can be written as the
|
|
41
|
+
* zoom range of the whole viewer rather than of one gesture.
|
|
42
|
+
*/
|
|
43
|
+
readonly scale: number
|
|
44
|
+
/**
|
|
45
|
+
* How much the fingers scaled during **this gesture alone**, raw.
|
|
46
|
+
*
|
|
47
|
+
* Untouched by `min`, `max` and `elastic`, and reset to `1` at the start of
|
|
48
|
+
* every gesture. This is RNGH's own `scale`. Read `scale` for how big the
|
|
49
|
+
* thing being pinched actually is.
|
|
50
|
+
*/
|
|
51
|
+
readonly gestureScale: number
|
|
52
|
+
/**
|
|
53
|
+
* The midpoint between the fingers, relative to the view.
|
|
54
|
+
*
|
|
55
|
+
* This is the point the zoom must happen about. A pinch scaled about the
|
|
56
|
+
* view's centre instead slides the content out from under the fingers, and
|
|
57
|
+
* that is the defect this field exists to prevent.
|
|
58
|
+
*/
|
|
59
|
+
readonly focal: Point
|
|
60
|
+
/** How fast the scale is changing, in scale units per second. */
|
|
61
|
+
readonly velocity: number
|
|
62
|
+
/**
|
|
63
|
+
* The nearest scale inside `min` and `max`. Equal to `scale` whenever the
|
|
64
|
+
* pinch is in range, which with the default `elastic` of `0` is always.
|
|
65
|
+
*
|
|
66
|
+
* With `elastic` set, the fingers can pull the scale past an end and
|
|
67
|
+
* Impulse leaves it there on release — moving it back is an animation, and
|
|
68
|
+
* Impulse owns no animation vocabulary. This field is the destination that
|
|
69
|
+
* animation needs, so the consumer does not have to re-derive the clamp
|
|
70
|
+
* from a range it already handed over.
|
|
71
|
+
*/
|
|
72
|
+
readonly settled: number
|
|
73
|
+
/** How many fingers are down. */
|
|
74
|
+
readonly pointers: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Options for {@link usePinch}. */
|
|
78
|
+
export interface UsePinchOptions extends GestureMemoOptions {
|
|
79
|
+
/**
|
|
80
|
+
* The scale before any pinch. Default `1`.
|
|
81
|
+
*
|
|
82
|
+
* Read once, at mount. `useSharedValue` keeps its first argument and
|
|
83
|
+
* ignores every later one, and that is the behaviour this option
|
|
84
|
+
* documents: after mount `scale` is the pinch's state, and the consumer
|
|
85
|
+
* moves it by writing it.
|
|
86
|
+
*/
|
|
87
|
+
initial?: number
|
|
88
|
+
/**
|
|
89
|
+
* The smallest scale. Unset by default, which lets the fingers shrink the
|
|
90
|
+
* thing without limit.
|
|
91
|
+
*
|
|
92
|
+
* Set it to `1` for a viewer that may zoom in but never out.
|
|
93
|
+
*/
|
|
94
|
+
min?: number
|
|
95
|
+
/**
|
|
96
|
+
* The largest scale. Unset by default, which lets the fingers grow the
|
|
97
|
+
* thing without limit.
|
|
98
|
+
*/
|
|
99
|
+
max?: number
|
|
100
|
+
/**
|
|
101
|
+
* How much of the travel past `min` or `max` reaches `scale`, from `0` to
|
|
102
|
+
* `1`. Default `0`.
|
|
103
|
+
*
|
|
104
|
+
* `0` stops dead at the end. `1` ignores the end while the fingers are
|
|
105
|
+
* down. Anything between is resistance — the pinch keeps moving and moves
|
|
106
|
+
* less than the fingers do.
|
|
107
|
+
*
|
|
108
|
+
* Impulse does not bring the scale back. The end callback carries
|
|
109
|
+
* `settled` — the scale to animate to — and `@rootnative/inertia` or a
|
|
110
|
+
* `withSpring` of your own does the rest.
|
|
111
|
+
*/
|
|
112
|
+
elastic?: number
|
|
113
|
+
/**
|
|
114
|
+
* Extra touchable area around the view, in points.
|
|
115
|
+
*
|
|
116
|
+
* Written inline as an object is fine — the gesture is not rebuilt when the
|
|
117
|
+
* contents are unchanged.
|
|
118
|
+
*/
|
|
119
|
+
hitSlop?: HitSlop
|
|
120
|
+
/**
|
|
121
|
+
* Whether the gesture is recognized at all. Default `true`.
|
|
122
|
+
*
|
|
123
|
+
* Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
|
|
124
|
+
* keeps its identity and its relations, so re-enabling it does not
|
|
125
|
+
* re-attach anything.
|
|
126
|
+
*/
|
|
127
|
+
enabled?: boolean
|
|
128
|
+
/**
|
|
129
|
+
* The pinch now owns the touch. **Runs on the JS thread** — Impulse owns
|
|
130
|
+
* the `scheduleOnRN` boundary, so this is an ordinary function and may
|
|
131
|
+
* touch React state.
|
|
132
|
+
*
|
|
133
|
+
* This is the first moment the pinch has definitely won. `onBegin` fires
|
|
134
|
+
* earlier and promises nothing.
|
|
135
|
+
*/
|
|
136
|
+
onPinchStart?: (event: PinchEvent) => void
|
|
137
|
+
/**
|
|
138
|
+
* The pinch is over. **Runs on the JS thread.**
|
|
139
|
+
*
|
|
140
|
+
* Fires only for a pinch that activated, so a touch that never became a
|
|
141
|
+
* pinch never reaches here on either path.
|
|
142
|
+
*
|
|
143
|
+
* It fires for both endings, and `cancelled` says which. `false` is the
|
|
144
|
+
* fingers lifting, and `velocity` then describes the release. `true` is the
|
|
145
|
+
* system taking the pinch away — a competing gesture won, or the app went
|
|
146
|
+
* to the background. **There was no release on that path, so the velocity
|
|
147
|
+
* describes the last movement rather than a throw.**
|
|
148
|
+
*
|
|
149
|
+
* Read `settled` on both paths for where an elastic overshoot belongs.
|
|
150
|
+
*/
|
|
151
|
+
onPinchEnd?: (event: PinchEvent, info: IntentEndInfo) => void
|
|
152
|
+
/**
|
|
153
|
+
* A finger went down and the gesture is now a candidate. **This is a
|
|
154
|
+
* worklet** — mark it with the `'worklet'` directive, and do not touch
|
|
155
|
+
* React state from it.
|
|
156
|
+
*
|
|
157
|
+
* Being a candidate is not the same as winning: one finger is not a pinch,
|
|
158
|
+
* and a second one may never arrive. Undo whatever it sets in
|
|
159
|
+
* `onFinalize`.
|
|
160
|
+
*/
|
|
161
|
+
onBegin?: (event: PinchEvent) => void
|
|
162
|
+
/**
|
|
163
|
+
* The scale changed. **This is a worklet**, and it runs on every frame the
|
|
164
|
+
* fingers move.
|
|
165
|
+
*
|
|
166
|
+
* `scale` is already written by the time this runs, so a `useAnimatedStyle`
|
|
167
|
+
* reading it needs nothing from here. Use this for the work that scaling
|
|
168
|
+
* alone does not do — holding the focal point still, say.
|
|
169
|
+
*
|
|
170
|
+
* There is no JS-thread counterpart on purpose. A per-frame `scheduleOnRN`
|
|
171
|
+
* is a scheduling cost paid sixty times a second for a value that is
|
|
172
|
+
* already on the thread that needs it.
|
|
173
|
+
*/
|
|
174
|
+
onUpdate?: (event: PinchEvent) => void
|
|
175
|
+
/**
|
|
176
|
+
* The gesture is over, whether it activated or not. **This is a worklet.**
|
|
177
|
+
*
|
|
178
|
+
* `success` is `true` when the pinch activated and ended normally. This is
|
|
179
|
+
* the right place to clear anything `onBegin` set, because it runs on both
|
|
180
|
+
* paths.
|
|
181
|
+
*/
|
|
182
|
+
onFinalize?: (event: PinchEvent, success: boolean) => void
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** What {@link usePinch} returns. */
|
|
186
|
+
export interface UsePinchResult extends IntentResult<PinchGesture> {
|
|
187
|
+
/**
|
|
188
|
+
* How much the thing is scaled, after `min`, `max` and `elastic`.
|
|
189
|
+
*
|
|
190
|
+
* **This is a position, not a movement.** It accumulates across gestures,
|
|
191
|
+
* so a second pinch continues from where the first stopped. `usePan` is the
|
|
192
|
+
* hook whose value zeroes at every gesture.
|
|
193
|
+
*
|
|
194
|
+
* Writing it is allowed and is how a release animation, or a reset button,
|
|
195
|
+
* hands control back. The next pinch continues from whatever it holds.
|
|
196
|
+
*/
|
|
197
|
+
readonly scale: SharedValue<number>
|
|
198
|
+
/**
|
|
199
|
+
* The midpoint between the fingers, relative to the view.
|
|
200
|
+
*
|
|
201
|
+
* It keeps the last gesture's focal point after the fingers lift, so a
|
|
202
|
+
* release animation scales about the same place the pinch did rather than
|
|
203
|
+
* snapping to the origin.
|
|
204
|
+
*/
|
|
205
|
+
readonly focal: SharedValue<Point>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Hold a scale inside a range, letting `elastic` of the excess through.
|
|
210
|
+
*
|
|
211
|
+
* A worklet, because the pinch's whole value path runs on the UI thread. The
|
|
212
|
+
* resistance is computed on the scale itself rather than on its logarithm:
|
|
213
|
+
* the two differ only past an end, where the number is already a deliberate
|
|
214
|
+
* overshoot rather than a measurement, and the linear form is the one
|
|
215
|
+
* `useDrag` uses for the same option name.
|
|
216
|
+
*/
|
|
217
|
+
function resist(
|
|
218
|
+
value: number,
|
|
219
|
+
min: number | undefined,
|
|
220
|
+
max: number | undefined,
|
|
221
|
+
elastic: number,
|
|
222
|
+
): number {
|
|
223
|
+
'worklet'
|
|
224
|
+
if (min !== undefined && value < min) {
|
|
225
|
+
return min + (value - min) * elastic
|
|
226
|
+
}
|
|
227
|
+
if (max !== undefined && value > max) {
|
|
228
|
+
return max + (value - max) * elastic
|
|
229
|
+
}
|
|
230
|
+
return value
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The nearest scale inside the range. What {@link resist} would have returned
|
|
235
|
+
* with `elastic` at `0`.
|
|
236
|
+
*/
|
|
237
|
+
function clamp(
|
|
238
|
+
value: number,
|
|
239
|
+
min: number | undefined,
|
|
240
|
+
max: number | undefined,
|
|
241
|
+
): number {
|
|
242
|
+
'worklet'
|
|
243
|
+
if (min !== undefined && value < min) {
|
|
244
|
+
return min
|
|
245
|
+
}
|
|
246
|
+
if (max !== undefined && value > max) {
|
|
247
|
+
return max
|
|
248
|
+
}
|
|
249
|
+
return value
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Recognize a two-finger pinch, and own the scale it produces.
|
|
254
|
+
*
|
|
255
|
+
* ```tsx
|
|
256
|
+
* const pinch = usePinch({ min: 1, max: 4 })
|
|
257
|
+
*
|
|
258
|
+
* const style = useAnimatedStyle(() => ({
|
|
259
|
+
* transform: [{ scale: pinch.scale.value }],
|
|
260
|
+
* }))
|
|
261
|
+
*
|
|
262
|
+
* return (
|
|
263
|
+
* <GestureDetector gesture={pinch.gesture}>
|
|
264
|
+
* <Animated.Image style={style} source={source} />
|
|
265
|
+
* </GestureDetector>
|
|
266
|
+
* )
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* **The scale accumulates; RNGH's does not.** A bare `Gesture.Pinch()`
|
|
270
|
+
* reports a factor that restarts at `1` on every gesture, so a viewer built
|
|
271
|
+
* on it snaps back to its original size the moment the fingers lift again.
|
|
272
|
+
* Carrying the scale across gestures is the multiplication and the stored
|
|
273
|
+
* start that every consumer otherwise writes, and it is why `min` and `max`
|
|
274
|
+
* can be the range of the viewer rather than of one gesture.
|
|
275
|
+
*
|
|
276
|
+
* **`focal` is the field a zoom viewer cannot skip.** Scaling about the
|
|
277
|
+
* view's centre slides the content out from under the fingers. The focal
|
|
278
|
+
* point is where the zoom has to happen, and it is reported relative to the
|
|
279
|
+
* view so it can go straight into a `translate` / `scale` / `translate`
|
|
280
|
+
* transform.
|
|
281
|
+
*
|
|
282
|
+
* **Activation criteria.** There are none to set. RNGH's pinch takes the
|
|
283
|
+
* touch as soon as a second finger moves, and it exposes no threshold, so
|
|
284
|
+
* Impulse has none to pass on. This is the one continuous intent whose
|
|
285
|
+
* coexistence is decided entirely by relations — see below.
|
|
286
|
+
*
|
|
287
|
+
* **Coexistence.** A pinch almost always shares its view with a pan, and it
|
|
288
|
+
* has no threshold to separate them with. Say it: `alongside` on both, so the
|
|
289
|
+
* two recognize at once and the same two fingers can move and scale the
|
|
290
|
+
* thing. `useGestures` with `mode: 'simultaneous'` is the same statement for
|
|
291
|
+
* gestures this screen owns.
|
|
292
|
+
*
|
|
293
|
+
* **Web.** RNGH recognizes pinch from pointer events, so it needs two
|
|
294
|
+
* pointers — a touchscreen or a device that reports them. A trackpad's pinch
|
|
295
|
+
* arrives as a `wheel` event with `ctrlKey`, which is not a pointer pair and
|
|
296
|
+
* never reaches this hook, so a desktop browser with a trackpad alone cannot
|
|
297
|
+
* zoom. Give it a control that sets `scale` directly, which the
|
|
298
|
+
* accessibility fallback needs anyway. The browser's own page zoom is
|
|
299
|
+
* unaffected either way.
|
|
300
|
+
*
|
|
301
|
+
* **Accessibility.** A pinch is invisible to a screen reader and unreachable
|
|
302
|
+
* from a keyboard, and this hook does not fix that. Whatever the pinch scales
|
|
303
|
+
* must be reachable another way: zoom-in and zoom-out buttons that write
|
|
304
|
+
* `scale`, a control that resets it, or `accessibilityActions` with
|
|
305
|
+
* `onAccessibilityAction`. A pinch-only zoom is a bug, not a trade-off.
|
|
306
|
+
*
|
|
307
|
+
* @param options - The scale range, callbacks, and the `alongside` /
|
|
308
|
+
* `blocks` / `deferTo` coexistence options every Impulse hook accepts.
|
|
309
|
+
*/
|
|
310
|
+
export function usePinch(options: UsePinchOptions = {}): UsePinchResult {
|
|
311
|
+
const {
|
|
312
|
+
initial = DEFAULT_INITIAL_SCALE,
|
|
313
|
+
min,
|
|
314
|
+
max,
|
|
315
|
+
elastic = 0,
|
|
316
|
+
enabled,
|
|
317
|
+
onPinchStart,
|
|
318
|
+
onPinchEnd,
|
|
319
|
+
onBegin,
|
|
320
|
+
onUpdate,
|
|
321
|
+
onFinalize,
|
|
322
|
+
} = options
|
|
323
|
+
|
|
324
|
+
const scale = useSharedValue(initial)
|
|
325
|
+
const focal = useSharedValue<Point>({ x: 0, y: 0 })
|
|
326
|
+
const isActive = useSharedValue(false)
|
|
327
|
+
// What `scale` would be at a gesture scale of 1, so the position can be
|
|
328
|
+
// rebuilt as `start * event.scale` on every frame.
|
|
329
|
+
const startScale = useSharedValue(initial)
|
|
330
|
+
|
|
331
|
+
const hitSlop = useStableRecord(options.hitSlop)
|
|
332
|
+
|
|
333
|
+
// JS-thread callbacks reach the gesture through stable identities, so they
|
|
334
|
+
// are never gesture dependencies. The worklet callbacks stay direct
|
|
335
|
+
// dependencies, because a worklet is captured as written.
|
|
336
|
+
const handlePinchStart = useLatestCallback(onPinchStart)
|
|
337
|
+
const handlePinchEnd = useLatestCallback(onPinchEnd)
|
|
338
|
+
// Attaching a handler is not the same as calling it: RNGH decides which
|
|
339
|
+
// thread a gesture's callbacks run on by inspecting the ones it was given,
|
|
340
|
+
// so the gesture does have to change when a handler appears or disappears.
|
|
341
|
+
const hasPinchStart = onPinchStart !== undefined
|
|
342
|
+
const hasPinchEnd = onPinchEnd !== undefined
|
|
343
|
+
|
|
344
|
+
const built = useGestureMemo(
|
|
345
|
+
'usePinch',
|
|
346
|
+
() => {
|
|
347
|
+
/**
|
|
348
|
+
* Shape RNGH's flat event into the pinch payload.
|
|
349
|
+
*
|
|
350
|
+
* Built inside the gesture rather than at module scope because it
|
|
351
|
+
* closes over the range — which is also why it is rebuilt only when
|
|
352
|
+
* that changes. Called from the callbacks, and only when one is
|
|
353
|
+
* present: a payload nobody reads is three objects allocated on the UI
|
|
354
|
+
* thread for every frame of every pinch.
|
|
355
|
+
*/
|
|
356
|
+
const toPinchEvent = (
|
|
357
|
+
event:
|
|
358
|
+
| GestureStateChangeEvent<PinchGestureHandlerEventPayload>
|
|
359
|
+
| GestureUpdateEvent<PinchGestureHandlerEventPayload>,
|
|
360
|
+
): PinchEvent => {
|
|
361
|
+
'worklet'
|
|
362
|
+
return {
|
|
363
|
+
scale: scale.value,
|
|
364
|
+
gestureScale: event.scale,
|
|
365
|
+
focal: focal.value,
|
|
366
|
+
velocity: event.velocity,
|
|
367
|
+
settled: clamp(scale.value, min, max),
|
|
368
|
+
pointers: event.numberOfPointers,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Record the focal point.
|
|
374
|
+
*
|
|
375
|
+
* A fresh object rather than two writes into the existing one: a
|
|
376
|
+
* shared value notifies on assignment, so mutating the object in place
|
|
377
|
+
* would leave a `useAnimatedStyle` reading a point that never changed.
|
|
378
|
+
*
|
|
379
|
+
* Called from `onStart` as well as `onUpdate`, so `onPinchStart`
|
|
380
|
+
* reports where the fingers are rather than where the previous gesture
|
|
381
|
+
* left them. Deliberately not called from `onEnd` or `onFinalize`: the
|
|
382
|
+
* fingers are lifting there and the point RNGH reports is no longer the
|
|
383
|
+
* one the zoom happened about.
|
|
384
|
+
*/
|
|
385
|
+
const trackFocal = (
|
|
386
|
+
event:
|
|
387
|
+
| GestureStateChangeEvent<PinchGestureHandlerEventPayload>
|
|
388
|
+
| GestureUpdateEvent<PinchGestureHandlerEventPayload>,
|
|
389
|
+
) => {
|
|
390
|
+
'worklet'
|
|
391
|
+
focal.value = { x: event.focalX, y: event.focalY }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const pinch = Gesture.Pinch()
|
|
395
|
+
.onBegin((event) => {
|
|
396
|
+
'worklet'
|
|
397
|
+
onBegin?.(toPinchEvent(event))
|
|
398
|
+
})
|
|
399
|
+
.onStart((event) => {
|
|
400
|
+
'worklet'
|
|
401
|
+
// Divide rather than assign. RNGH reports `1` here in the ordinary
|
|
402
|
+
// case, but it activates on movement, so the fingers may already
|
|
403
|
+
// have travelled. Dividing means the first `onUpdate` reproduces
|
|
404
|
+
// the current scale instead of multiplying it by that head start —
|
|
405
|
+
// the difference between a pinch that picks the image up at its
|
|
406
|
+
// size and one that jumps before it grows.
|
|
407
|
+
startScale.value =
|
|
408
|
+
event.scale === 0 ? scale.value : scale.value / event.scale
|
|
409
|
+
trackFocal(event)
|
|
410
|
+
isActive.value = true
|
|
411
|
+
if (hasPinchStart) {
|
|
412
|
+
scheduleOnRN(handlePinchStart, toPinchEvent(event))
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
.onUpdate((event) => {
|
|
416
|
+
'worklet'
|
|
417
|
+
trackFocal(event)
|
|
418
|
+
scale.value = resist(
|
|
419
|
+
startScale.value * event.scale,
|
|
420
|
+
min,
|
|
421
|
+
max,
|
|
422
|
+
elastic,
|
|
423
|
+
)
|
|
424
|
+
onUpdate?.(toPinchEvent(event))
|
|
425
|
+
})
|
|
426
|
+
.onEnd((event, success) => {
|
|
427
|
+
'worklet'
|
|
428
|
+
// Not guarded on `success`: RNGH calls this for a cancelled pinch
|
|
429
|
+
// too, and `cancelled` is what carries that to the JS thread. RNGH
|
|
430
|
+
// reaches `onEnd` only from the ACTIVE state, so a touch that never
|
|
431
|
+
// became a pinch goes to `onFinalize` and never gets here.
|
|
432
|
+
if (hasPinchEnd) {
|
|
433
|
+
scheduleOnRN(handlePinchEnd, toPinchEvent(event), {
|
|
434
|
+
cancelled: !success,
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
.onFinalize((event, success) => {
|
|
439
|
+
'worklet'
|
|
440
|
+
isActive.value = false
|
|
441
|
+
onFinalize?.(toPinchEvent(event), success)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// Applied conditionally rather than with a default, so an option the
|
|
445
|
+
// consumer did not set leaves RNGH's own default in place instead of
|
|
446
|
+
// Impulse overwriting it with a guess.
|
|
447
|
+
if (hitSlop !== undefined) {
|
|
448
|
+
pinch.hitSlop(hitSlop)
|
|
449
|
+
}
|
|
450
|
+
if (enabled !== undefined) {
|
|
451
|
+
pinch.enabled(enabled)
|
|
452
|
+
}
|
|
453
|
+
return pinch
|
|
454
|
+
},
|
|
455
|
+
[
|
|
456
|
+
min,
|
|
457
|
+
max,
|
|
458
|
+
elastic,
|
|
459
|
+
hitSlop,
|
|
460
|
+
enabled,
|
|
461
|
+
hasPinchStart,
|
|
462
|
+
hasPinchEnd,
|
|
463
|
+
handlePinchStart,
|
|
464
|
+
handlePinchEnd,
|
|
465
|
+
scale,
|
|
466
|
+
focal,
|
|
467
|
+
startScale,
|
|
468
|
+
isActive,
|
|
469
|
+
onBegin,
|
|
470
|
+
onUpdate,
|
|
471
|
+
onFinalize,
|
|
472
|
+
],
|
|
473
|
+
options,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
// Memoised so a consumer can put the whole hook result in a dependency
|
|
477
|
+
// list. The shared values are stable for the life of the hook, so `built`
|
|
478
|
+
// is the only real input.
|
|
479
|
+
return useMemo(
|
|
480
|
+
() => buildIntentResult(built, { scale, focal, isActive }),
|
|
481
|
+
[built, scale, focal, isActive],
|
|
482
|
+
)
|
|
483
|
+
}
|