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