@rootnative/inertia-gestures 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/dist/index.d.mts +118 -11
- package/dist/index.d.ts +118 -11
- package/dist/index.js +79 -4
- package/dist/index.mjs +82 -7
- package/llms.txt +92 -12
- package/package.json +3 -3
- package/src/index.ts +3 -1
- package/src/types.ts +15 -0
- package/src/useDrag.ts +8 -2
- package/src/usePan.ts +6 -1
- package/src/useSwipe.ts +211 -11
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ This package ships in lockstep with `@rootnative/inertia` — version numbers tr
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.9] - 2026-08-22
|
|
10
|
+
|
|
11
|
+
**Lockstep version bump** alongside `@rootnative/inertia@0.0.9` (`useInterpolatedStyle` types its return against the map it was given, so a `style` array no longer needs a cast). No runtime changes in this adapter; the `@rootnative/inertia` peer range moves to `>=0.0.9`.
|
|
12
|
+
|
|
13
|
+
## [0.0.8] - 2026-08-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`useSwipe` snap-back is configurable: `releaseTransition`.** The reset to zero after a release was a hard-coded `withSpring(0)` — it could not join a design-token motion system, and it dropped the release velocity, so a flick that stopped short of the threshold reset as if the finger had been still. The option accepts a spring / timing / no-animation config inline, or a `TransitionName` registered on the nearest `<MotionConfig transitions={...}>`. The release velocity is passed into a spring automatically (unless the config sets `velocity` itself), on both the default spring and a configured one. Decay is excluded — the snap-back always targets zero and decay has no target; a name that resolves to a decay config dev-warns and falls back to the default spring.
|
|
18
|
+
|
|
19
|
+
- **`useSwipe` commit exit: `onCommit` + `onSwipeEnd`.** The hook always sprang back to zero, which is right for swipe-to-delete and wrong for a card deck — a committed card must continue in the swipe direction and leave the screen, and consumers were rebuilding that with a parallel translation layer, an animation-end handler, and a safety timer. `onCommit` is a UI-thread worklet that fires when the gesture commits and returns per-axis release transitions (the same `ReleaseResult` shape as `useDrag`'s `onRelease`) to run **instead of** the snap-back; an omitted axis or a `void` return snaps back as usual. `onSwipeEnd(direction, { finished })` fires on the JS thread when the committed swipe's release animation settles — the commit exit if one ran, the snap-back otherwise — which is the "card is gone, advance the deck" moment `onSwipe` (fired at release) cannot give. It does not fire for a release that did not commit.
|
|
20
|
+
|
|
21
|
+
- **`useSwipe` returns `reset()`.** Snaps both shared values back to zero with no animation, cancelling anything in flight. After a commit exit the values stay at the exit target; call `reset()` when the next card takes over the same mounted component. A keyed remount gets fresh values and doesn't need it.
|
|
22
|
+
|
|
23
|
+
- New exported type: `SnapBackTransition` (the `releaseTransition` shapes). Requires `@rootnative/inertia` >=0.0.8 — the settle callback rides a new third parameter on core's `buildReleaseAnimation`.
|
|
24
|
+
|
|
9
25
|
## [0.0.7] - 2026-08-14
|
|
10
26
|
|
|
11
27
|
**Lockstep version bump** alongside `@rootnative/inertia@0.0.7` (`Motion.FlatList`, a virtualized animated scroller, and `gesture={{ pressed }}` responding to a mouse on web). No runtime changes in this adapter; the `@rootnative/inertia` peer range moves to `>=0.0.7`.
|
|
@@ -55,7 +71,9 @@ Initial alpha publish alongside `@rootnative/inertia@0.0.0-alpha.0`. Optional ad
|
|
|
55
71
|
- `useDrag({ onRelease })` — release worklet returns per-axis Inertia transitions (snap-to-tick spring, decay with bounds, etc.). Velocity stays on the UI thread; no JS round-trip.
|
|
56
72
|
- `useSwipe`, `usePan` hooks composable with any `Motion.*` primitive via `<GestureDetector>`.
|
|
57
73
|
|
|
58
|
-
[unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.
|
|
74
|
+
[unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.9...HEAD
|
|
75
|
+
[0.0.9]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.9
|
|
76
|
+
[0.0.8]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.8
|
|
59
77
|
[0.0.7]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.7
|
|
60
78
|
[0.0.6]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.6
|
|
61
79
|
[0.0.5]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.5
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PanGesture } from 'react-native-gesture-handler';
|
|
2
2
|
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
-
import { SpringTransition, TimingTransition, DecayTransition, NoAnimationTransition } from '@rootnative/inertia';
|
|
3
|
+
import { SpringTransition, TimingTransition, DecayTransition, NoAnimationTransition, TransitionName } from '@rootnative/inertia';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Public types for `@rootnative/inertia-gestures`.
|
|
@@ -48,11 +48,22 @@ type ReleaseTransition = (SpringTransition & {
|
|
|
48
48
|
/**
|
|
49
49
|
* Per-axis release transitions returned by `onRelease`. Omit an axis to leave
|
|
50
50
|
* its SV where it landed (no release animation on that axis).
|
|
51
|
+
*
|
|
52
|
+
* `useSwipe`'s `onCommit` returns the same shape with one difference in the
|
|
53
|
+
* omit semantics: an omitted axis snaps back to zero (the hook's resting
|
|
54
|
+
* state) rather than staying where it landed.
|
|
51
55
|
*/
|
|
52
56
|
interface ReleaseResult {
|
|
53
57
|
x?: ReleaseTransition;
|
|
54
58
|
y?: ReleaseTransition;
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Transition shapes `useSwipe` accepts for its snap-back (the
|
|
62
|
+
* `releaseTransition` option). The target is always zero, so decay — which
|
|
63
|
+
* has no target — is excluded; a registered name that resolves to a decay
|
|
64
|
+
* config dev-warns and falls back to the default spring.
|
|
65
|
+
*/
|
|
66
|
+
type SnapBackTransition = SpringTransition | TimingTransition | NoAnimationTransition;
|
|
56
67
|
/**
|
|
57
68
|
* Configuration for `useDrag`. All fields are optional; the defaults give an
|
|
58
69
|
* unconstrained two-axis drag with no elasticity.
|
|
@@ -104,8 +115,14 @@ interface UseDragResult {
|
|
|
104
115
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
105
116
|
gesture: PanGesture;
|
|
106
117
|
/**
|
|
107
|
-
* Animated style fragment
|
|
108
|
-
*
|
|
118
|
+
* Animated style fragment for the dragged Motion primitive's `style` prop.
|
|
119
|
+
* Stable across renders.
|
|
120
|
+
*
|
|
121
|
+
* This owns the whole `transform` key. Do not stack a second transform style
|
|
122
|
+
* beside it — `transform` is one key in React Native, so the later style
|
|
123
|
+
* replaces this one instead of merging, and the drag silently stops
|
|
124
|
+
* following the finger. Nest another animated view, or build one style from
|
|
125
|
+
* `dragX` / `dragY` with `useInterpolatedStyle`, to add a transform.
|
|
109
126
|
*/
|
|
110
127
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
111
128
|
/** Current x translation in pixels. UI-thread shared value. */
|
|
@@ -155,6 +172,17 @@ interface SwipeOptions {
|
|
|
155
172
|
* to `800`.
|
|
156
173
|
*/
|
|
157
174
|
velocityThreshold?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Transition for the snap-back to zero, given inline or as a registered
|
|
177
|
+
* `TransitionName` from the nearest `<MotionConfig transitions={...}>`.
|
|
178
|
+
* Defaults to the library default spring. The release velocity is passed
|
|
179
|
+
* into a spring automatically (unless the config sets `velocity` itself),
|
|
180
|
+
* so a flick that stops short of the threshold resets with the momentum it
|
|
181
|
+
* had. Decay is excluded — the snap-back always targets zero, and decay has
|
|
182
|
+
* no target; a name that resolves to a decay config dev-warns and falls
|
|
183
|
+
* back to the default spring.
|
|
184
|
+
*/
|
|
185
|
+
releaseTransition?: SnapBackTransition | TransitionName;
|
|
158
186
|
/**
|
|
159
187
|
* Fired on the JS thread when the gesture commits in an allowed direction.
|
|
160
188
|
*/
|
|
@@ -162,14 +190,62 @@ interface SwipeOptions {
|
|
|
162
190
|
distance: number;
|
|
163
191
|
velocity: number;
|
|
164
192
|
}) => void;
|
|
193
|
+
/**
|
|
194
|
+
* UI-thread callback fired when the gesture commits, before any release
|
|
195
|
+
* animation starts. Return per-axis release transitions to run **instead
|
|
196
|
+
* of** the snap-back — the commit-exit path a card deck needs, where the
|
|
197
|
+
* committed card continues in the swipe direction and leaves the screen:
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* onCommit: (direction, info) => {
|
|
201
|
+
* 'worklet'
|
|
202
|
+
* return {
|
|
203
|
+
* x: {
|
|
204
|
+
* type: 'spring',
|
|
205
|
+
* to: direction === 'right' ? 500 : -500,
|
|
206
|
+
* velocity: info.velocity.x,
|
|
207
|
+
* },
|
|
208
|
+
* }
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* An omitted axis (or a `void` return) snaps back to zero as usual. This
|
|
213
|
+
* callback runs as a worklet so the release velocity stays on the UI
|
|
214
|
+
* thread — author it with the `'worklet'` directive at the top of the body.
|
|
215
|
+
*
|
|
216
|
+
* Composes with `onSwipe`: both fire on commit. `onCommit` picks the
|
|
217
|
+
* release animation on the UI thread; `onSwipe` is for JS-thread side
|
|
218
|
+
* effects. Note the shared values stay at the exit target afterwards — call
|
|
219
|
+
* `reset()` when the next card takes over without a remount.
|
|
220
|
+
*/
|
|
221
|
+
onCommit?: (direction: SwipeDirection, info: ReleaseInfo) => ReleaseResult | void;
|
|
222
|
+
/**
|
|
223
|
+
* Fired on the JS thread when a committed swipe's release animation
|
|
224
|
+
* settles — the commit exit from `onCommit` if one ran, the snap-back
|
|
225
|
+
* otherwise. This is the "card is gone, advance the deck" moment that
|
|
226
|
+
* `onSwipe` (which fires at release) cannot give you. `finished` is `false`
|
|
227
|
+
* when the animation was interrupted (for example by a new gesture). Does
|
|
228
|
+
* not fire for a release that did not commit.
|
|
229
|
+
*/
|
|
230
|
+
onSwipeEnd?: (direction: SwipeDirection, info: {
|
|
231
|
+
finished: boolean;
|
|
232
|
+
}) => void;
|
|
165
233
|
}
|
|
166
234
|
interface UseSwipeResult {
|
|
167
235
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
168
236
|
gesture: PanGesture;
|
|
169
237
|
/**
|
|
170
238
|
* Animated style fragment exposing live translation while the gesture is
|
|
171
|
-
* active.
|
|
172
|
-
*
|
|
239
|
+
* active. After release it snaps back to `{ 0, 0 }` via `releaseTransition`
|
|
240
|
+
* (default spring), unless a committed swipe's `onCommit` returned a
|
|
241
|
+
* commit-exit transition for that axis.
|
|
242
|
+
*
|
|
243
|
+
* This owns the whole `transform` style key. `transform` is one key in React
|
|
244
|
+
* Native, so a second style in the same array **replaces** this array rather
|
|
245
|
+
* than merging with it — `style={[swipe.animatedStyle, tiltStyle]}` silently
|
|
246
|
+
* drops the translation and only the tilt runs. To add a transform of your
|
|
247
|
+
* own, nest another animated view, or build one style from `swipeX` /
|
|
248
|
+
* `swipeY` yourself with `useInterpolatedStyle`.
|
|
173
249
|
*/
|
|
174
250
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
175
251
|
/** Live x translation. */
|
|
@@ -178,14 +254,24 @@ interface UseSwipeResult {
|
|
|
178
254
|
swipeY: SharedValue<number>;
|
|
179
255
|
/** True while the user is actively swiping. */
|
|
180
256
|
isActive: SharedValue<boolean>;
|
|
257
|
+
/**
|
|
258
|
+
* Snap both shared values back to zero with no animation, cancelling
|
|
259
|
+
* anything in flight. After a commit exit the values stay at the exit
|
|
260
|
+
* target — call this when the next card takes over the same mounted
|
|
261
|
+
* component. A keyed remount gets fresh values and doesn't need it.
|
|
262
|
+
*/
|
|
263
|
+
reset: () => void;
|
|
181
264
|
}
|
|
182
265
|
/**
|
|
183
266
|
* Directional commit-or-snap-back gesture. Tracks live translation while the
|
|
184
267
|
* user drags and fires `onSwipe(direction)` on release if either the distance
|
|
185
|
-
* or velocity threshold is exceeded in an allowed direction.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
268
|
+
* or velocity threshold is exceeded in an allowed direction.
|
|
269
|
+
*
|
|
270
|
+
* By default the position shared values animate back to zero whether or not
|
|
271
|
+
* the swipe commits — the right shape for swipe-to-delete, where the row
|
|
272
|
+
* returns and the consumer removes it. For a card deck, return a commit-exit
|
|
273
|
+
* transition from `onCommit` so the committed card continues off screen, and
|
|
274
|
+
* advance the deck from `onSwipeEnd` when it settles.
|
|
189
275
|
*
|
|
190
276
|
* Usage:
|
|
191
277
|
* ```tsx
|
|
@@ -199,6 +285,22 @@ interface UseSwipeResult {
|
|
|
199
285
|
* </GestureDetector>
|
|
200
286
|
* )
|
|
201
287
|
* ```
|
|
288
|
+
*
|
|
289
|
+
* `animatedStyle` owns the whole `transform` key, so it does not compose with
|
|
290
|
+
* a second transform style. To add rotation — the usual card-deck shape —
|
|
291
|
+
* build one style from `swipeX` instead of stacking two:
|
|
292
|
+
*
|
|
293
|
+
* ```tsx
|
|
294
|
+
* const cardStyle = useInterpolatedStyle(swipe.swipeX, {
|
|
295
|
+
* translateX: [-200, 0, 200],
|
|
296
|
+
* rotate: ['-12deg', '0deg', '12deg'],
|
|
297
|
+
* })
|
|
298
|
+
* return (
|
|
299
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
300
|
+
* <Motion.View style={cardStyle}>...</Motion.View>
|
|
301
|
+
* </GestureDetector>
|
|
302
|
+
* )
|
|
303
|
+
* ```
|
|
202
304
|
*/
|
|
203
305
|
declare function useSwipe(options?: SwipeOptions): UseSwipeResult;
|
|
204
306
|
|
|
@@ -227,7 +329,12 @@ interface PanOptions {
|
|
|
227
329
|
interface UsePanResult {
|
|
228
330
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
229
331
|
gesture: PanGesture;
|
|
230
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Stable animated `transform` style. This owns the whole `transform` key —
|
|
334
|
+
* a second transform style in the same array replaces it rather than merging
|
|
335
|
+
* with it. Nest another animated view, or build one style from `panX` /
|
|
336
|
+
* `panY` with `useInterpolatedStyle`, to add a transform of your own.
|
|
337
|
+
*/
|
|
231
338
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
232
339
|
/** Live x translation, persistent across gestures. */
|
|
233
340
|
panX: SharedValue<number>;
|
|
@@ -247,4 +354,4 @@ interface UsePanResult {
|
|
|
247
354
|
*/
|
|
248
355
|
declare function usePan(options?: PanOptions): UsePanResult;
|
|
249
356
|
|
|
250
|
-
export { type DragConstraints, type DragOptions, type PanOptions, type ReleaseInfo, type ReleaseResult, type ReleaseTransition, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
|
357
|
+
export { type DragConstraints, type DragOptions, type PanOptions, type ReleaseInfo, type ReleaseResult, type ReleaseTransition, type SnapBackTransition, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PanGesture } from 'react-native-gesture-handler';
|
|
2
2
|
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
-
import { SpringTransition, TimingTransition, DecayTransition, NoAnimationTransition } from '@rootnative/inertia';
|
|
3
|
+
import { SpringTransition, TimingTransition, DecayTransition, NoAnimationTransition, TransitionName } from '@rootnative/inertia';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Public types for `@rootnative/inertia-gestures`.
|
|
@@ -48,11 +48,22 @@ type ReleaseTransition = (SpringTransition & {
|
|
|
48
48
|
/**
|
|
49
49
|
* Per-axis release transitions returned by `onRelease`. Omit an axis to leave
|
|
50
50
|
* its SV where it landed (no release animation on that axis).
|
|
51
|
+
*
|
|
52
|
+
* `useSwipe`'s `onCommit` returns the same shape with one difference in the
|
|
53
|
+
* omit semantics: an omitted axis snaps back to zero (the hook's resting
|
|
54
|
+
* state) rather than staying where it landed.
|
|
51
55
|
*/
|
|
52
56
|
interface ReleaseResult {
|
|
53
57
|
x?: ReleaseTransition;
|
|
54
58
|
y?: ReleaseTransition;
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Transition shapes `useSwipe` accepts for its snap-back (the
|
|
62
|
+
* `releaseTransition` option). The target is always zero, so decay — which
|
|
63
|
+
* has no target — is excluded; a registered name that resolves to a decay
|
|
64
|
+
* config dev-warns and falls back to the default spring.
|
|
65
|
+
*/
|
|
66
|
+
type SnapBackTransition = SpringTransition | TimingTransition | NoAnimationTransition;
|
|
56
67
|
/**
|
|
57
68
|
* Configuration for `useDrag`. All fields are optional; the defaults give an
|
|
58
69
|
* unconstrained two-axis drag with no elasticity.
|
|
@@ -104,8 +115,14 @@ interface UseDragResult {
|
|
|
104
115
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
105
116
|
gesture: PanGesture;
|
|
106
117
|
/**
|
|
107
|
-
* Animated style fragment
|
|
108
|
-
*
|
|
118
|
+
* Animated style fragment for the dragged Motion primitive's `style` prop.
|
|
119
|
+
* Stable across renders.
|
|
120
|
+
*
|
|
121
|
+
* This owns the whole `transform` key. Do not stack a second transform style
|
|
122
|
+
* beside it — `transform` is one key in React Native, so the later style
|
|
123
|
+
* replaces this one instead of merging, and the drag silently stops
|
|
124
|
+
* following the finger. Nest another animated view, or build one style from
|
|
125
|
+
* `dragX` / `dragY` with `useInterpolatedStyle`, to add a transform.
|
|
109
126
|
*/
|
|
110
127
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
111
128
|
/** Current x translation in pixels. UI-thread shared value. */
|
|
@@ -155,6 +172,17 @@ interface SwipeOptions {
|
|
|
155
172
|
* to `800`.
|
|
156
173
|
*/
|
|
157
174
|
velocityThreshold?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Transition for the snap-back to zero, given inline or as a registered
|
|
177
|
+
* `TransitionName` from the nearest `<MotionConfig transitions={...}>`.
|
|
178
|
+
* Defaults to the library default spring. The release velocity is passed
|
|
179
|
+
* into a spring automatically (unless the config sets `velocity` itself),
|
|
180
|
+
* so a flick that stops short of the threshold resets with the momentum it
|
|
181
|
+
* had. Decay is excluded — the snap-back always targets zero, and decay has
|
|
182
|
+
* no target; a name that resolves to a decay config dev-warns and falls
|
|
183
|
+
* back to the default spring.
|
|
184
|
+
*/
|
|
185
|
+
releaseTransition?: SnapBackTransition | TransitionName;
|
|
158
186
|
/**
|
|
159
187
|
* Fired on the JS thread when the gesture commits in an allowed direction.
|
|
160
188
|
*/
|
|
@@ -162,14 +190,62 @@ interface SwipeOptions {
|
|
|
162
190
|
distance: number;
|
|
163
191
|
velocity: number;
|
|
164
192
|
}) => void;
|
|
193
|
+
/**
|
|
194
|
+
* UI-thread callback fired when the gesture commits, before any release
|
|
195
|
+
* animation starts. Return per-axis release transitions to run **instead
|
|
196
|
+
* of** the snap-back — the commit-exit path a card deck needs, where the
|
|
197
|
+
* committed card continues in the swipe direction and leaves the screen:
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* onCommit: (direction, info) => {
|
|
201
|
+
* 'worklet'
|
|
202
|
+
* return {
|
|
203
|
+
* x: {
|
|
204
|
+
* type: 'spring',
|
|
205
|
+
* to: direction === 'right' ? 500 : -500,
|
|
206
|
+
* velocity: info.velocity.x,
|
|
207
|
+
* },
|
|
208
|
+
* }
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* An omitted axis (or a `void` return) snaps back to zero as usual. This
|
|
213
|
+
* callback runs as a worklet so the release velocity stays on the UI
|
|
214
|
+
* thread — author it with the `'worklet'` directive at the top of the body.
|
|
215
|
+
*
|
|
216
|
+
* Composes with `onSwipe`: both fire on commit. `onCommit` picks the
|
|
217
|
+
* release animation on the UI thread; `onSwipe` is for JS-thread side
|
|
218
|
+
* effects. Note the shared values stay at the exit target afterwards — call
|
|
219
|
+
* `reset()` when the next card takes over without a remount.
|
|
220
|
+
*/
|
|
221
|
+
onCommit?: (direction: SwipeDirection, info: ReleaseInfo) => ReleaseResult | void;
|
|
222
|
+
/**
|
|
223
|
+
* Fired on the JS thread when a committed swipe's release animation
|
|
224
|
+
* settles — the commit exit from `onCommit` if one ran, the snap-back
|
|
225
|
+
* otherwise. This is the "card is gone, advance the deck" moment that
|
|
226
|
+
* `onSwipe` (which fires at release) cannot give you. `finished` is `false`
|
|
227
|
+
* when the animation was interrupted (for example by a new gesture). Does
|
|
228
|
+
* not fire for a release that did not commit.
|
|
229
|
+
*/
|
|
230
|
+
onSwipeEnd?: (direction: SwipeDirection, info: {
|
|
231
|
+
finished: boolean;
|
|
232
|
+
}) => void;
|
|
165
233
|
}
|
|
166
234
|
interface UseSwipeResult {
|
|
167
235
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
168
236
|
gesture: PanGesture;
|
|
169
237
|
/**
|
|
170
238
|
* Animated style fragment exposing live translation while the gesture is
|
|
171
|
-
* active.
|
|
172
|
-
*
|
|
239
|
+
* active. After release it snaps back to `{ 0, 0 }` via `releaseTransition`
|
|
240
|
+
* (default spring), unless a committed swipe's `onCommit` returned a
|
|
241
|
+
* commit-exit transition for that axis.
|
|
242
|
+
*
|
|
243
|
+
* This owns the whole `transform` style key. `transform` is one key in React
|
|
244
|
+
* Native, so a second style in the same array **replaces** this array rather
|
|
245
|
+
* than merging with it — `style={[swipe.animatedStyle, tiltStyle]}` silently
|
|
246
|
+
* drops the translation and only the tilt runs. To add a transform of your
|
|
247
|
+
* own, nest another animated view, or build one style from `swipeX` /
|
|
248
|
+
* `swipeY` yourself with `useInterpolatedStyle`.
|
|
173
249
|
*/
|
|
174
250
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
175
251
|
/** Live x translation. */
|
|
@@ -178,14 +254,24 @@ interface UseSwipeResult {
|
|
|
178
254
|
swipeY: SharedValue<number>;
|
|
179
255
|
/** True while the user is actively swiping. */
|
|
180
256
|
isActive: SharedValue<boolean>;
|
|
257
|
+
/**
|
|
258
|
+
* Snap both shared values back to zero with no animation, cancelling
|
|
259
|
+
* anything in flight. After a commit exit the values stay at the exit
|
|
260
|
+
* target — call this when the next card takes over the same mounted
|
|
261
|
+
* component. A keyed remount gets fresh values and doesn't need it.
|
|
262
|
+
*/
|
|
263
|
+
reset: () => void;
|
|
181
264
|
}
|
|
182
265
|
/**
|
|
183
266
|
* Directional commit-or-snap-back gesture. Tracks live translation while the
|
|
184
267
|
* user drags and fires `onSwipe(direction)` on release if either the distance
|
|
185
|
-
* or velocity threshold is exceeded in an allowed direction.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
268
|
+
* or velocity threshold is exceeded in an allowed direction.
|
|
269
|
+
*
|
|
270
|
+
* By default the position shared values animate back to zero whether or not
|
|
271
|
+
* the swipe commits — the right shape for swipe-to-delete, where the row
|
|
272
|
+
* returns and the consumer removes it. For a card deck, return a commit-exit
|
|
273
|
+
* transition from `onCommit` so the committed card continues off screen, and
|
|
274
|
+
* advance the deck from `onSwipeEnd` when it settles.
|
|
189
275
|
*
|
|
190
276
|
* Usage:
|
|
191
277
|
* ```tsx
|
|
@@ -199,6 +285,22 @@ interface UseSwipeResult {
|
|
|
199
285
|
* </GestureDetector>
|
|
200
286
|
* )
|
|
201
287
|
* ```
|
|
288
|
+
*
|
|
289
|
+
* `animatedStyle` owns the whole `transform` key, so it does not compose with
|
|
290
|
+
* a second transform style. To add rotation — the usual card-deck shape —
|
|
291
|
+
* build one style from `swipeX` instead of stacking two:
|
|
292
|
+
*
|
|
293
|
+
* ```tsx
|
|
294
|
+
* const cardStyle = useInterpolatedStyle(swipe.swipeX, {
|
|
295
|
+
* translateX: [-200, 0, 200],
|
|
296
|
+
* rotate: ['-12deg', '0deg', '12deg'],
|
|
297
|
+
* })
|
|
298
|
+
* return (
|
|
299
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
300
|
+
* <Motion.View style={cardStyle}>...</Motion.View>
|
|
301
|
+
* </GestureDetector>
|
|
302
|
+
* )
|
|
303
|
+
* ```
|
|
202
304
|
*/
|
|
203
305
|
declare function useSwipe(options?: SwipeOptions): UseSwipeResult;
|
|
204
306
|
|
|
@@ -227,7 +329,12 @@ interface PanOptions {
|
|
|
227
329
|
interface UsePanResult {
|
|
228
330
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
229
331
|
gesture: PanGesture;
|
|
230
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Stable animated `transform` style. This owns the whole `transform` key —
|
|
334
|
+
* a second transform style in the same array replaces it rather than merging
|
|
335
|
+
* with it. Nest another animated view, or build one style from `panX` /
|
|
336
|
+
* `panY` with `useInterpolatedStyle`, to add a transform of your own.
|
|
337
|
+
*/
|
|
231
338
|
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
232
339
|
/** Live x translation, persistent across gestures. */
|
|
233
340
|
panX: SharedValue<number>;
|
|
@@ -247,4 +354,4 @@ interface UsePanResult {
|
|
|
247
354
|
*/
|
|
248
355
|
declare function usePan(options?: PanOptions): UsePanResult;
|
|
249
356
|
|
|
250
|
-
export { type DragConstraints, type DragOptions, type PanOptions, type ReleaseInfo, type ReleaseResult, type ReleaseTransition, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
|
357
|
+
export { type DragConstraints, type DragOptions, type PanOptions, type ReleaseInfo, type ReleaseResult, type ReleaseTransition, type SnapBackTransition, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
package/dist/index.js
CHANGED
|
@@ -116,12 +116,16 @@ function applyBounds(value, min, max, elastic) {
|
|
|
116
116
|
return value;
|
|
117
117
|
}
|
|
118
118
|
var DEFAULT_DIRECTIONS = ["left", "right", "up", "down"];
|
|
119
|
+
var DEFAULT_SNAP_BACK = { type: "spring" };
|
|
119
120
|
function useSwipe(options = {}) {
|
|
120
121
|
const {
|
|
121
122
|
directions = DEFAULT_DIRECTIONS,
|
|
122
123
|
distanceThreshold = 80,
|
|
123
124
|
velocityThreshold = 800,
|
|
124
|
-
|
|
125
|
+
releaseTransition,
|
|
126
|
+
onSwipe,
|
|
127
|
+
onCommit,
|
|
128
|
+
onSwipeEnd
|
|
125
129
|
} = options;
|
|
126
130
|
const swipeX = reactNativeReanimated.useSharedValue(0);
|
|
127
131
|
const swipeY = reactNativeReanimated.useSharedValue(0);
|
|
@@ -130,6 +134,19 @@ function useSwipe(options = {}) {
|
|
|
130
134
|
const allowRight = directions.includes("right");
|
|
131
135
|
const allowUp = directions.includes("up");
|
|
132
136
|
const allowDown = directions.includes("down");
|
|
137
|
+
const registry = inertia.useNamedTransitions();
|
|
138
|
+
const snapBack = react.useMemo(() => {
|
|
139
|
+
const cfg = inertia.resolveNamedTransition(releaseTransition, registry) ?? DEFAULT_SNAP_BACK;
|
|
140
|
+
if (cfg.type === "decay") {
|
|
141
|
+
if (__DEV__) {
|
|
142
|
+
console.warn(
|
|
143
|
+
"[inertia] useSwipe `releaseTransition` resolved to a decay transition, which has no target and cannot snap back to zero \u2014 falling back to the default spring."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return DEFAULT_SNAP_BACK;
|
|
147
|
+
}
|
|
148
|
+
return cfg;
|
|
149
|
+
}, [releaseTransition, registry]);
|
|
133
150
|
const gesture = react.useMemo(() => {
|
|
134
151
|
const pan = reactNativeGestureHandler.Gesture.Pan().onStart(() => {
|
|
135
152
|
"worklet";
|
|
@@ -159,8 +176,53 @@ function useSwipe(options = {}) {
|
|
|
159
176
|
const velocity = isHoriz ? Math.abs(e.velocityX) : Math.abs(e.velocityY);
|
|
160
177
|
reactNativeReanimated.runOnJS(onSwipe)(direction, { distance, velocity });
|
|
161
178
|
}
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
let exit = void 0;
|
|
180
|
+
if (direction !== null && onCommit) {
|
|
181
|
+
exit = onCommit(direction, {
|
|
182
|
+
x: swipeX.value,
|
|
183
|
+
y: swipeY.value,
|
|
184
|
+
velocity: { x: e.velocityX, y: e.velocityY }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
let settle;
|
|
188
|
+
if (direction !== null && onSwipeEnd) {
|
|
189
|
+
const dir = direction;
|
|
190
|
+
const end = onSwipeEnd;
|
|
191
|
+
settle = (finished) => {
|
|
192
|
+
reactNativeReanimated.runOnJS(end)(dir, { finished: finished === true });
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const isHorizontalCommit = direction === "left" || direction === "right";
|
|
196
|
+
const settleX = isHorizontalCommit ? settle : void 0;
|
|
197
|
+
const settleY = isHorizontalCommit ? void 0 : settle;
|
|
198
|
+
const exitX = exit ? exit.x : void 0;
|
|
199
|
+
const exitY = exit ? exit.y : void 0;
|
|
200
|
+
if (exitX) {
|
|
201
|
+
swipeX.value = inertia.buildReleaseAnimation(
|
|
202
|
+
exitX,
|
|
203
|
+
"to" in exitX ? exitX.to : swipeX.value,
|
|
204
|
+
settleX
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
swipeX.value = inertia.buildReleaseAnimation(
|
|
208
|
+
withReleaseVelocity(snapBack, e.velocityX),
|
|
209
|
+
0,
|
|
210
|
+
settleX
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (exitY) {
|
|
214
|
+
swipeY.value = inertia.buildReleaseAnimation(
|
|
215
|
+
exitY,
|
|
216
|
+
"to" in exitY ? exitY.to : swipeY.value,
|
|
217
|
+
settleY
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
swipeY.value = inertia.buildReleaseAnimation(
|
|
221
|
+
withReleaseVelocity(snapBack, e.velocityY),
|
|
222
|
+
0,
|
|
223
|
+
settleY
|
|
224
|
+
);
|
|
225
|
+
}
|
|
164
226
|
}).onFinalize(() => {
|
|
165
227
|
"worklet";
|
|
166
228
|
isActive.value = false;
|
|
@@ -173,7 +235,10 @@ function useSwipe(options = {}) {
|
|
|
173
235
|
allowRight,
|
|
174
236
|
allowUp,
|
|
175
237
|
allowDown,
|
|
238
|
+
snapBack,
|
|
176
239
|
onSwipe,
|
|
240
|
+
onCommit,
|
|
241
|
+
onSwipeEnd,
|
|
177
242
|
swipeX,
|
|
178
243
|
swipeY,
|
|
179
244
|
isActive
|
|
@@ -181,7 +246,17 @@ function useSwipe(options = {}) {
|
|
|
181
246
|
const animatedStyle = reactNativeReanimated.useAnimatedStyle(() => ({
|
|
182
247
|
transform: [{ translateX: swipeX.value }, { translateY: swipeY.value }]
|
|
183
248
|
}));
|
|
184
|
-
|
|
249
|
+
const reset = react.useCallback(() => {
|
|
250
|
+
swipeX.value = 0;
|
|
251
|
+
swipeY.value = 0;
|
|
252
|
+
}, [swipeX, swipeY]);
|
|
253
|
+
return { gesture, animatedStyle, swipeX, swipeY, isActive, reset };
|
|
254
|
+
}
|
|
255
|
+
function withReleaseVelocity(cfg, velocity) {
|
|
256
|
+
"worklet";
|
|
257
|
+
if (cfg.type !== "spring" && cfg.type !== void 0) return cfg;
|
|
258
|
+
if (cfg.velocity !== void 0) return cfg;
|
|
259
|
+
return { ...cfg, velocity };
|
|
185
260
|
}
|
|
186
261
|
function pickDirection(tx, ty, vx, vy, distanceThreshold, velocityThreshold, allowLeft, allowRight, allowUp, allowDown) {
|
|
187
262
|
"worklet";
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
2
|
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
-
import { useSharedValue, runOnJS, useAnimatedStyle,
|
|
4
|
-
import { buildReleaseAnimation } from '@rootnative/inertia';
|
|
3
|
+
import { useSharedValue, runOnJS, useAnimatedStyle, withDecay } from 'react-native-reanimated';
|
|
4
|
+
import { buildReleaseAnimation, useNamedTransitions, resolveNamedTransition } from '@rootnative/inertia';
|
|
5
5
|
|
|
6
6
|
// src/useDrag.ts
|
|
7
7
|
function useDrag(options = {}) {
|
|
@@ -114,12 +114,16 @@ function applyBounds(value, min, max, elastic) {
|
|
|
114
114
|
return value;
|
|
115
115
|
}
|
|
116
116
|
var DEFAULT_DIRECTIONS = ["left", "right", "up", "down"];
|
|
117
|
+
var DEFAULT_SNAP_BACK = { type: "spring" };
|
|
117
118
|
function useSwipe(options = {}) {
|
|
118
119
|
const {
|
|
119
120
|
directions = DEFAULT_DIRECTIONS,
|
|
120
121
|
distanceThreshold = 80,
|
|
121
122
|
velocityThreshold = 800,
|
|
122
|
-
|
|
123
|
+
releaseTransition,
|
|
124
|
+
onSwipe,
|
|
125
|
+
onCommit,
|
|
126
|
+
onSwipeEnd
|
|
123
127
|
} = options;
|
|
124
128
|
const swipeX = useSharedValue(0);
|
|
125
129
|
const swipeY = useSharedValue(0);
|
|
@@ -128,6 +132,19 @@ function useSwipe(options = {}) {
|
|
|
128
132
|
const allowRight = directions.includes("right");
|
|
129
133
|
const allowUp = directions.includes("up");
|
|
130
134
|
const allowDown = directions.includes("down");
|
|
135
|
+
const registry = useNamedTransitions();
|
|
136
|
+
const snapBack = useMemo(() => {
|
|
137
|
+
const cfg = resolveNamedTransition(releaseTransition, registry) ?? DEFAULT_SNAP_BACK;
|
|
138
|
+
if (cfg.type === "decay") {
|
|
139
|
+
if (__DEV__) {
|
|
140
|
+
console.warn(
|
|
141
|
+
"[inertia] useSwipe `releaseTransition` resolved to a decay transition, which has no target and cannot snap back to zero \u2014 falling back to the default spring."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return DEFAULT_SNAP_BACK;
|
|
145
|
+
}
|
|
146
|
+
return cfg;
|
|
147
|
+
}, [releaseTransition, registry]);
|
|
131
148
|
const gesture = useMemo(() => {
|
|
132
149
|
const pan = Gesture.Pan().onStart(() => {
|
|
133
150
|
"worklet";
|
|
@@ -157,8 +174,53 @@ function useSwipe(options = {}) {
|
|
|
157
174
|
const velocity = isHoriz ? Math.abs(e.velocityX) : Math.abs(e.velocityY);
|
|
158
175
|
runOnJS(onSwipe)(direction, { distance, velocity });
|
|
159
176
|
}
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
let exit = void 0;
|
|
178
|
+
if (direction !== null && onCommit) {
|
|
179
|
+
exit = onCommit(direction, {
|
|
180
|
+
x: swipeX.value,
|
|
181
|
+
y: swipeY.value,
|
|
182
|
+
velocity: { x: e.velocityX, y: e.velocityY }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
let settle;
|
|
186
|
+
if (direction !== null && onSwipeEnd) {
|
|
187
|
+
const dir = direction;
|
|
188
|
+
const end = onSwipeEnd;
|
|
189
|
+
settle = (finished) => {
|
|
190
|
+
runOnJS(end)(dir, { finished: finished === true });
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const isHorizontalCommit = direction === "left" || direction === "right";
|
|
194
|
+
const settleX = isHorizontalCommit ? settle : void 0;
|
|
195
|
+
const settleY = isHorizontalCommit ? void 0 : settle;
|
|
196
|
+
const exitX = exit ? exit.x : void 0;
|
|
197
|
+
const exitY = exit ? exit.y : void 0;
|
|
198
|
+
if (exitX) {
|
|
199
|
+
swipeX.value = buildReleaseAnimation(
|
|
200
|
+
exitX,
|
|
201
|
+
"to" in exitX ? exitX.to : swipeX.value,
|
|
202
|
+
settleX
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
swipeX.value = buildReleaseAnimation(
|
|
206
|
+
withReleaseVelocity(snapBack, e.velocityX),
|
|
207
|
+
0,
|
|
208
|
+
settleX
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (exitY) {
|
|
212
|
+
swipeY.value = buildReleaseAnimation(
|
|
213
|
+
exitY,
|
|
214
|
+
"to" in exitY ? exitY.to : swipeY.value,
|
|
215
|
+
settleY
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
swipeY.value = buildReleaseAnimation(
|
|
219
|
+
withReleaseVelocity(snapBack, e.velocityY),
|
|
220
|
+
0,
|
|
221
|
+
settleY
|
|
222
|
+
);
|
|
223
|
+
}
|
|
162
224
|
}).onFinalize(() => {
|
|
163
225
|
"worklet";
|
|
164
226
|
isActive.value = false;
|
|
@@ -171,7 +233,10 @@ function useSwipe(options = {}) {
|
|
|
171
233
|
allowRight,
|
|
172
234
|
allowUp,
|
|
173
235
|
allowDown,
|
|
236
|
+
snapBack,
|
|
174
237
|
onSwipe,
|
|
238
|
+
onCommit,
|
|
239
|
+
onSwipeEnd,
|
|
175
240
|
swipeX,
|
|
176
241
|
swipeY,
|
|
177
242
|
isActive
|
|
@@ -179,7 +244,17 @@ function useSwipe(options = {}) {
|
|
|
179
244
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
180
245
|
transform: [{ translateX: swipeX.value }, { translateY: swipeY.value }]
|
|
181
246
|
}));
|
|
182
|
-
|
|
247
|
+
const reset = useCallback(() => {
|
|
248
|
+
swipeX.value = 0;
|
|
249
|
+
swipeY.value = 0;
|
|
250
|
+
}, [swipeX, swipeY]);
|
|
251
|
+
return { gesture, animatedStyle, swipeX, swipeY, isActive, reset };
|
|
252
|
+
}
|
|
253
|
+
function withReleaseVelocity(cfg, velocity) {
|
|
254
|
+
"worklet";
|
|
255
|
+
if (cfg.type !== "spring" && cfg.type !== void 0) return cfg;
|
|
256
|
+
if (cfg.velocity !== void 0) return cfg;
|
|
257
|
+
return { ...cfg, velocity };
|
|
183
258
|
}
|
|
184
259
|
function pickDirection(tx, ty, vx, vy, distanceThreshold, velocityThreshold, allowLeft, allowRight, allowUp, allowDown) {
|
|
185
260
|
"worklet";
|
package/llms.txt
CHANGED
|
@@ -100,7 +100,7 @@ Internally this routes through `buildReleaseAnimation` exported from `@rootnativ
|
|
|
100
100
|
|
|
101
101
|
## `useSwipe`
|
|
102
102
|
|
|
103
|
-
Directional commit-or-snap-back gesture. Tracks live translation while the user drags; on release, fires `onSwipe` if either the distance or velocity threshold is met along an allowed axis.
|
|
103
|
+
Directional commit-or-snap-back gesture. Tracks live translation while the user drags; on release, fires `onSwipe` if either the distance or velocity threshold is met along an allowed axis. By default the position animates back to zero whether or not it commits — the right shape for swipe-to-delete, where the row returns and the consumer removes it. For a card deck, return a commit-exit transition from `onCommit` so the committed card continues off screen instead (see below).
|
|
104
104
|
|
|
105
105
|
```tsx
|
|
106
106
|
const swipe = useSwipe({
|
|
@@ -121,15 +121,95 @@ return (
|
|
|
121
121
|
)
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
| Option | Type
|
|
125
|
-
| ------------------- |
|
|
126
|
-
| `directions` | `Array<'left' \| 'right' \| 'up' \| 'down'>`
|
|
127
|
-
| `distanceThreshold` | `number` (px)
|
|
128
|
-
| `velocityThreshold` | `number` (px/sec)
|
|
129
|
-
| `
|
|
124
|
+
| Option | Type | Default | Notes |
|
|
125
|
+
| ------------------- | ------------------------------------------------------ | -------------- | ---------------------------------------------------------------------- |
|
|
126
|
+
| `directions` | `Array<'left' \| 'right' \| 'up' \| 'down'>` | all four | Only commits along listed directions. Off-axis swipes do nothing. |
|
|
127
|
+
| `distanceThreshold` | `number` (px) | `80` | Distance past which a release commits. |
|
|
128
|
+
| `velocityThreshold` | `number` (px/sec) | `800` | Flick velocity that commits even before distance threshold. |
|
|
129
|
+
| `releaseTransition` | `SnapBackTransition \| TransitionName` | default spring | Transition for the snap-back to zero — see below. |
|
|
130
|
+
| `onSwipe` | `(direction, { distance, velocity }) => void` | none | JS-thread callback at release. Direction is one of the allowed values. |
|
|
131
|
+
| `onCommit` | `(direction, info) => ReleaseResult \| void` (worklet) | none | UI-thread commit-exit transition — see below. |
|
|
132
|
+
| `onSwipeEnd` | `(direction, { finished }) => void` | none | JS-thread callback when a committed swipe's release animation settles. |
|
|
130
133
|
|
|
131
134
|
The dominant axis (whichever of \|tx\|, \|ty\| is larger at release) decides which direction is checked.
|
|
132
135
|
|
|
136
|
+
Returns `{ gesture, animatedStyle, swipeX, swipeY, isActive, reset }`. `reset()` snaps both shared values back to zero with no animation — useful after a commit exit when the next card takes over the same mounted component (a keyed remount gets fresh values and doesn't need it).
|
|
137
|
+
|
|
138
|
+
### Snap-back transition (`releaseTransition`)
|
|
139
|
+
|
|
140
|
+
The snap-back to zero defaults to the library default spring. Pass `releaseTransition` to use your own — inline, or as a `TransitionName` registered on the nearest [`<MotionConfig transitions={...}>`](./motion-config#named-transitions), which is how the reset joins a design-token motion system:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const swipe = useSwipe({
|
|
144
|
+
directions: ['left', 'right'],
|
|
145
|
+
releaseTransition: 'spatial-default', // or { type: 'spring', tension: 220 }
|
|
146
|
+
onSwipe: (direction) => archive(direction),
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The release velocity is passed into a spring automatically (unless the config sets `velocity` itself), so a flick that stops short of the threshold resets with the momentum it had instead of resetting as if the finger had been still.
|
|
151
|
+
|
|
152
|
+
`SnapBackTransition` is spring / timing / no-animation — decay is excluded, because the snap-back always targets zero and decay has no target. A registered name that resolves to a decay config dev-warns and falls back to the default spring.
|
|
153
|
+
|
|
154
|
+
### Commit exit (`onCommit` + `onSwipeEnd`)
|
|
155
|
+
|
|
156
|
+
The snap-back is right for swipe-to-delete, but a card deck needs the opposite: a **committed** card must continue in the swipe direction and leave the screen. Return per-axis release transitions from `onCommit` to run instead of the snap-back, and advance the deck from `onSwipeEnd` when the exit settles:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const swipe = useSwipe({
|
|
160
|
+
directions: ['left', 'right'],
|
|
161
|
+
onCommit: (direction, info) => {
|
|
162
|
+
'worklet'
|
|
163
|
+
return {
|
|
164
|
+
x: {
|
|
165
|
+
type: 'spring',
|
|
166
|
+
to: direction === 'right' ? 500 : -500,
|
|
167
|
+
velocity: info.velocity.x, // continue the finger's momentum
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
onSwipeEnd: (direction, { finished }) => {
|
|
172
|
+
if (finished) advanceDeck(direction)
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`onCommit` runs as a **worklet** on the UI thread (author it with the `'worklet'` directive), fires only when the gesture commits, and receives the same `info` shape as `useDrag`'s `onRelease` — final translation plus release velocity. The return shape is the same `ReleaseResult` too, with one difference: an omitted axis (or a `void` return) snaps back to zero as usual, because zero is this hook's resting state.
|
|
178
|
+
|
|
179
|
+
`onSwipeEnd` fires on the JS thread when the committed swipe's release animation settles — the commit exit if one ran, the snap-back otherwise. That is the "card is gone, the next one takes over" moment that `onSwipe` (which fires at release) cannot give you. `finished` is `false` when the animation was interrupted, for example by a new gesture. It does not fire for a release that did not commit.
|
|
180
|
+
|
|
181
|
+
Composition: `onSwipe` (JS side effects at release) → `onCommit` (UI-thread exit decision) → `onSwipeEnd` (JS side effects at settle). Use whichever subset the interaction needs.
|
|
182
|
+
|
|
183
|
+
### `animatedStyle` owns the whole `transform` key
|
|
184
|
+
|
|
185
|
+
`animatedStyle` returns a complete `transform` array. In React Native `transform` is a **single** style key, so a second style carrying its own `transform` **replaces** this one rather than merging with it. Stacking a rotation beside it — the first thing almost every card deck does — silently drops the translation, and the card rotates without following the finger:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// Broken: tiltStyle's transform replaces the swipe translation.
|
|
189
|
+
<Motion.View style={[swipe.animatedStyle, tiltStyle]} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Pairing `animatedStyle` with a static style is safe (`[styles.card, swipe.animatedStyle]` above) because a static style declares no `transform`. The conflict only appears when the second style is itself a transform.
|
|
193
|
+
|
|
194
|
+
Build one style from `swipeX` instead of stacking two:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
const cardStyle = useInterpolatedStyle(swipe.swipeX, {
|
|
198
|
+
translateX: [-200, 0, 200],
|
|
199
|
+
rotate: ['-12deg', '0deg', '12deg'],
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<GestureDetector gesture={swipe.gesture}>
|
|
204
|
+
<Motion.View style={[styles.card, cardStyle]}>{children}</Motion.View>
|
|
205
|
+
</GestureDetector>
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Nesting a second `Motion.View` also works — one view per transform source — when the transforms come from genuinely independent drivers.
|
|
210
|
+
|
|
211
|
+
The same applies to `useDrag` and `usePan`: each owns its `transform` key too.
|
|
212
|
+
|
|
133
213
|
## `usePan`
|
|
134
214
|
|
|
135
215
|
Camera-style pan with momentum on release. Position **persists across separate gestures** — the next pan starts from the current offset, not zero — and on release the position continues gliding via Reanimated's `withDecay`.
|
|
@@ -159,11 +239,11 @@ For rubber-banded constraints, prefer `useDrag({ elastic })` — `usePan` hard-c
|
|
|
159
239
|
|
|
160
240
|
## When to pick which
|
|
161
241
|
|
|
162
|
-
| Gesture | Snap back on release?
|
|
163
|
-
| ---------- |
|
|
164
|
-
| `useDrag` | No — stays where left
|
|
165
|
-
| `useSwipe` | Yes —
|
|
166
|
-
| `usePan` | No — stays where left
|
|
242
|
+
| Gesture | Snap back on release? | Momentum on release? | Typical UX |
|
|
243
|
+
| ---------- | --------------------------------------- | ---------------------------- | -------------------------------------------------- |
|
|
244
|
+
| `useDrag` | No — stays where left | No | Move-to-position, sliders, sortable handles. |
|
|
245
|
+
| `useSwipe` | Yes — unless `onCommit` returns an exit | No (springs back) | Swipe-to-delete rows, card stacks, dismiss sheets. |
|
|
246
|
+
| `usePan` | No — stays where left | Yes — coasts via `withDecay` | Maps, zoomable canvases, large-image navigation. |
|
|
167
247
|
|
|
168
248
|
## Scope: touch + mouse only
|
|
169
249
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rootnative/inertia-gestures",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Gesture-handler-driven drag / pan / swipe adapters for @rootnative/inertia.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RootNative",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"!**/*.test.*"
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@rootnative/inertia": ">=0.0.
|
|
53
|
+
"@rootnative/inertia": ">=0.0.9",
|
|
54
54
|
"react": ">=19.0.0",
|
|
55
55
|
"react-native": ">=0.81.0",
|
|
56
56
|
"react-native-gesture-handler": ">=2.0.0",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"react-test-renderer": "19.1.0",
|
|
70
70
|
"tsup": "^8.3.5",
|
|
71
71
|
"typescript": "^5.7.3",
|
|
72
|
-
"@rootnative/inertia": "0.0.
|
|
72
|
+
"@rootnative/inertia": "0.0.9"
|
|
73
73
|
},
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - `useDrag` — one- or two-axis drag with optional constraints and
|
|
7
7
|
* rubber-band elasticity.
|
|
8
8
|
* - `useSwipe` — directional commit-or-snap-back gesture (distance + velocity
|
|
9
|
-
* thresholds)
|
|
9
|
+
* thresholds), with a configurable snap-back and an optional commit-exit
|
|
10
|
+
* path for card decks.
|
|
10
11
|
* - `usePan` — camera-style pan with momentum on release.
|
|
11
12
|
*/
|
|
12
13
|
export { useDrag } from './useDrag'
|
|
@@ -21,4 +22,5 @@ export type {
|
|
|
21
22
|
ReleaseInfo,
|
|
22
23
|
ReleaseResult,
|
|
23
24
|
ReleaseTransition,
|
|
25
|
+
SnapBackTransition,
|
|
24
26
|
} from './types'
|
package/src/types.ts
CHANGED
|
@@ -49,12 +49,27 @@ export type ReleaseTransition =
|
|
|
49
49
|
/**
|
|
50
50
|
* Per-axis release transitions returned by `onRelease`. Omit an axis to leave
|
|
51
51
|
* its SV where it landed (no release animation on that axis).
|
|
52
|
+
*
|
|
53
|
+
* `useSwipe`'s `onCommit` returns the same shape with one difference in the
|
|
54
|
+
* omit semantics: an omitted axis snaps back to zero (the hook's resting
|
|
55
|
+
* state) rather than staying where it landed.
|
|
52
56
|
*/
|
|
53
57
|
export interface ReleaseResult {
|
|
54
58
|
x?: ReleaseTransition
|
|
55
59
|
y?: ReleaseTransition
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Transition shapes `useSwipe` accepts for its snap-back (the
|
|
64
|
+
* `releaseTransition` option). The target is always zero, so decay — which
|
|
65
|
+
* has no target — is excluded; a registered name that resolves to a decay
|
|
66
|
+
* config dev-warns and falls back to the default spring.
|
|
67
|
+
*/
|
|
68
|
+
export type SnapBackTransition =
|
|
69
|
+
| SpringTransition
|
|
70
|
+
| TimingTransition
|
|
71
|
+
| NoAnimationTransition
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Configuration for `useDrag`. All fields are optional; the defaults give an
|
|
60
75
|
* unconstrained two-axis drag with no elasticity.
|
package/src/useDrag.ts
CHANGED
|
@@ -13,8 +13,14 @@ export interface UseDragResult {
|
|
|
13
13
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
14
14
|
gesture: PanGesture
|
|
15
15
|
/**
|
|
16
|
-
* Animated style fragment
|
|
17
|
-
*
|
|
16
|
+
* Animated style fragment for the dragged Motion primitive's `style` prop.
|
|
17
|
+
* Stable across renders.
|
|
18
|
+
*
|
|
19
|
+
* This owns the whole `transform` key. Do not stack a second transform style
|
|
20
|
+
* beside it — `transform` is one key in React Native, so the later style
|
|
21
|
+
* replaces this one instead of merging, and the drag silently stops
|
|
22
|
+
* following the finger. Nest another animated view, or build one style from
|
|
23
|
+
* `dragX` / `dragY` with `useInterpolatedStyle`, to add a transform.
|
|
18
24
|
*/
|
|
19
25
|
animatedStyle: ReturnType<typeof useAnimatedStyle>
|
|
20
26
|
/** Current x translation in pixels. UI-thread shared value. */
|
package/src/usePan.ts
CHANGED
|
@@ -34,7 +34,12 @@ export interface PanOptions {
|
|
|
34
34
|
export interface UsePanResult {
|
|
35
35
|
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
36
36
|
gesture: PanGesture
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Stable animated `transform` style. This owns the whole `transform` key —
|
|
39
|
+
* a second transform style in the same array replaces it rather than merging
|
|
40
|
+
* with it. Nest another animated view, or build one style from `panX` /
|
|
41
|
+
* `panY` with `useInterpolatedStyle`, to add a transform of your own.
|
|
42
|
+
*/
|
|
38
43
|
animatedStyle: ReturnType<typeof useAnimatedStyle>
|
|
39
44
|
/** Live x translation, persistent across gestures. */
|
|
40
45
|
panX: SharedValue<number>
|
package/src/useSwipe.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
2
|
import { Gesture, type PanGesture } from 'react-native-gesture-handler'
|
|
3
3
|
import {
|
|
4
4
|
runOnJS,
|
|
5
5
|
useAnimatedStyle,
|
|
6
6
|
useSharedValue,
|
|
7
|
-
withSpring,
|
|
8
7
|
type SharedValue,
|
|
9
8
|
} from 'react-native-reanimated'
|
|
9
|
+
import {
|
|
10
|
+
buildReleaseAnimation,
|
|
11
|
+
resolveNamedTransition,
|
|
12
|
+
useNamedTransitions,
|
|
13
|
+
type TransitionConfig,
|
|
14
|
+
type TransitionName,
|
|
15
|
+
} from '@rootnative/inertia'
|
|
16
|
+
import type { ReleaseInfo, ReleaseResult, SnapBackTransition } from './types'
|
|
17
|
+
|
|
18
|
+
declare const __DEV__: boolean
|
|
10
19
|
|
|
11
20
|
export type SwipeDirection = 'left' | 'right' | 'up' | 'down'
|
|
12
21
|
|
|
@@ -28,6 +37,17 @@ export interface SwipeOptions {
|
|
|
28
37
|
* to `800`.
|
|
29
38
|
*/
|
|
30
39
|
velocityThreshold?: number
|
|
40
|
+
/**
|
|
41
|
+
* Transition for the snap-back to zero, given inline or as a registered
|
|
42
|
+
* `TransitionName` from the nearest `<MotionConfig transitions={...}>`.
|
|
43
|
+
* Defaults to the library default spring. The release velocity is passed
|
|
44
|
+
* into a spring automatically (unless the config sets `velocity` itself),
|
|
45
|
+
* so a flick that stops short of the threshold resets with the momentum it
|
|
46
|
+
* had. Decay is excluded — the snap-back always targets zero, and decay has
|
|
47
|
+
* no target; a name that resolves to a decay config dev-warns and falls
|
|
48
|
+
* back to the default spring.
|
|
49
|
+
*/
|
|
50
|
+
releaseTransition?: SnapBackTransition | TransitionName
|
|
31
51
|
/**
|
|
32
52
|
* Fired on the JS thread when the gesture commits in an allowed direction.
|
|
33
53
|
*/
|
|
@@ -35,6 +55,47 @@ export interface SwipeOptions {
|
|
|
35
55
|
direction: SwipeDirection,
|
|
36
56
|
info: { distance: number; velocity: number },
|
|
37
57
|
) => void
|
|
58
|
+
/**
|
|
59
|
+
* UI-thread callback fired when the gesture commits, before any release
|
|
60
|
+
* animation starts. Return per-axis release transitions to run **instead
|
|
61
|
+
* of** the snap-back — the commit-exit path a card deck needs, where the
|
|
62
|
+
* committed card continues in the swipe direction and leaves the screen:
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* onCommit: (direction, info) => {
|
|
66
|
+
* 'worklet'
|
|
67
|
+
* return {
|
|
68
|
+
* x: {
|
|
69
|
+
* type: 'spring',
|
|
70
|
+
* to: direction === 'right' ? 500 : -500,
|
|
71
|
+
* velocity: info.velocity.x,
|
|
72
|
+
* },
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* An omitted axis (or a `void` return) snaps back to zero as usual. This
|
|
78
|
+
* callback runs as a worklet so the release velocity stays on the UI
|
|
79
|
+
* thread — author it with the `'worklet'` directive at the top of the body.
|
|
80
|
+
*
|
|
81
|
+
* Composes with `onSwipe`: both fire on commit. `onCommit` picks the
|
|
82
|
+
* release animation on the UI thread; `onSwipe` is for JS-thread side
|
|
83
|
+
* effects. Note the shared values stay at the exit target afterwards — call
|
|
84
|
+
* `reset()` when the next card takes over without a remount.
|
|
85
|
+
*/
|
|
86
|
+
onCommit?: (
|
|
87
|
+
direction: SwipeDirection,
|
|
88
|
+
info: ReleaseInfo,
|
|
89
|
+
) => ReleaseResult | void
|
|
90
|
+
/**
|
|
91
|
+
* Fired on the JS thread when a committed swipe's release animation
|
|
92
|
+
* settles — the commit exit from `onCommit` if one ran, the snap-back
|
|
93
|
+
* otherwise. This is the "card is gone, advance the deck" moment that
|
|
94
|
+
* `onSwipe` (which fires at release) cannot give you. `finished` is `false`
|
|
95
|
+
* when the animation was interrupted (for example by a new gesture). Does
|
|
96
|
+
* not fire for a release that did not commit.
|
|
97
|
+
*/
|
|
98
|
+
onSwipeEnd?: (direction: SwipeDirection, info: { finished: boolean }) => void
|
|
38
99
|
}
|
|
39
100
|
|
|
40
101
|
export interface UseSwipeResult {
|
|
@@ -42,8 +103,16 @@ export interface UseSwipeResult {
|
|
|
42
103
|
gesture: PanGesture
|
|
43
104
|
/**
|
|
44
105
|
* Animated style fragment exposing live translation while the gesture is
|
|
45
|
-
* active.
|
|
46
|
-
*
|
|
106
|
+
* active. After release it snaps back to `{ 0, 0 }` via `releaseTransition`
|
|
107
|
+
* (default spring), unless a committed swipe's `onCommit` returned a
|
|
108
|
+
* commit-exit transition for that axis.
|
|
109
|
+
*
|
|
110
|
+
* This owns the whole `transform` style key. `transform` is one key in React
|
|
111
|
+
* Native, so a second style in the same array **replaces** this array rather
|
|
112
|
+
* than merging with it — `style={[swipe.animatedStyle, tiltStyle]}` silently
|
|
113
|
+
* drops the translation and only the tilt runs. To add a transform of your
|
|
114
|
+
* own, nest another animated view, or build one style from `swipeX` /
|
|
115
|
+
* `swipeY` yourself with `useInterpolatedStyle`.
|
|
47
116
|
*/
|
|
48
117
|
animatedStyle: ReturnType<typeof useAnimatedStyle>
|
|
49
118
|
/** Live x translation. */
|
|
@@ -52,17 +121,29 @@ export interface UseSwipeResult {
|
|
|
52
121
|
swipeY: SharedValue<number>
|
|
53
122
|
/** True while the user is actively swiping. */
|
|
54
123
|
isActive: SharedValue<boolean>
|
|
124
|
+
/**
|
|
125
|
+
* Snap both shared values back to zero with no animation, cancelling
|
|
126
|
+
* anything in flight. After a commit exit the values stay at the exit
|
|
127
|
+
* target — call this when the next card takes over the same mounted
|
|
128
|
+
* component. A keyed remount gets fresh values and doesn't need it.
|
|
129
|
+
*/
|
|
130
|
+
reset: () => void
|
|
55
131
|
}
|
|
56
132
|
|
|
57
133
|
const DEFAULT_DIRECTIONS: SwipeDirection[] = ['left', 'right', 'up', 'down']
|
|
58
134
|
|
|
135
|
+
const DEFAULT_SNAP_BACK: TransitionConfig = { type: 'spring' }
|
|
136
|
+
|
|
59
137
|
/**
|
|
60
138
|
* Directional commit-or-snap-back gesture. Tracks live translation while the
|
|
61
139
|
* user drags and fires `onSwipe(direction)` on release if either the distance
|
|
62
|
-
* or velocity threshold is exceeded in an allowed direction.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
140
|
+
* or velocity threshold is exceeded in an allowed direction.
|
|
141
|
+
*
|
|
142
|
+
* By default the position shared values animate back to zero whether or not
|
|
143
|
+
* the swipe commits — the right shape for swipe-to-delete, where the row
|
|
144
|
+
* returns and the consumer removes it. For a card deck, return a commit-exit
|
|
145
|
+
* transition from `onCommit` so the committed card continues off screen, and
|
|
146
|
+
* advance the deck from `onSwipeEnd` when it settles.
|
|
66
147
|
*
|
|
67
148
|
* Usage:
|
|
68
149
|
* ```tsx
|
|
@@ -76,13 +157,32 @@ const DEFAULT_DIRECTIONS: SwipeDirection[] = ['left', 'right', 'up', 'down']
|
|
|
76
157
|
* </GestureDetector>
|
|
77
158
|
* )
|
|
78
159
|
* ```
|
|
160
|
+
*
|
|
161
|
+
* `animatedStyle` owns the whole `transform` key, so it does not compose with
|
|
162
|
+
* a second transform style. To add rotation — the usual card-deck shape —
|
|
163
|
+
* build one style from `swipeX` instead of stacking two:
|
|
164
|
+
*
|
|
165
|
+
* ```tsx
|
|
166
|
+
* const cardStyle = useInterpolatedStyle(swipe.swipeX, {
|
|
167
|
+
* translateX: [-200, 0, 200],
|
|
168
|
+
* rotate: ['-12deg', '0deg', '12deg'],
|
|
169
|
+
* })
|
|
170
|
+
* return (
|
|
171
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
172
|
+
* <Motion.View style={cardStyle}>...</Motion.View>
|
|
173
|
+
* </GestureDetector>
|
|
174
|
+
* )
|
|
175
|
+
* ```
|
|
79
176
|
*/
|
|
80
177
|
export function useSwipe(options: SwipeOptions = {}): UseSwipeResult {
|
|
81
178
|
const {
|
|
82
179
|
directions = DEFAULT_DIRECTIONS,
|
|
83
180
|
distanceThreshold = 80,
|
|
84
181
|
velocityThreshold = 800,
|
|
182
|
+
releaseTransition,
|
|
85
183
|
onSwipe,
|
|
184
|
+
onCommit,
|
|
185
|
+
onSwipeEnd,
|
|
86
186
|
} = options
|
|
87
187
|
|
|
88
188
|
const swipeX = useSharedValue(0)
|
|
@@ -94,6 +194,25 @@ export function useSwipe(options: SwipeOptions = {}): UseSwipeResult {
|
|
|
94
194
|
const allowUp = directions.includes('up')
|
|
95
195
|
const allowDown = directions.includes('down')
|
|
96
196
|
|
|
197
|
+
// Resolve a named snap-back on the JS thread, at the nearest provider —
|
|
198
|
+
// the worklet captures the resolved plain config.
|
|
199
|
+
const registry = useNamedTransitions()
|
|
200
|
+
const snapBack = useMemo(() => {
|
|
201
|
+
const cfg =
|
|
202
|
+
resolveNamedTransition(releaseTransition, registry) ?? DEFAULT_SNAP_BACK
|
|
203
|
+
if (cfg.type === 'decay') {
|
|
204
|
+
if (__DEV__) {
|
|
205
|
+
console.warn(
|
|
206
|
+
'[inertia] useSwipe `releaseTransition` resolved to a decay ' +
|
|
207
|
+
'transition, which has no target and cannot snap back to zero — ' +
|
|
208
|
+
'falling back to the default spring.',
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
return DEFAULT_SNAP_BACK
|
|
212
|
+
}
|
|
213
|
+
return cfg
|
|
214
|
+
}, [releaseTransition, registry])
|
|
215
|
+
|
|
97
216
|
const gesture = useMemo(() => {
|
|
98
217
|
const pan = Gesture.Pan()
|
|
99
218
|
.onStart(() => {
|
|
@@ -130,8 +249,63 @@ export function useSwipe(options: SwipeOptions = {}): UseSwipeResult {
|
|
|
130
249
|
: Math.abs(e.velocityY)
|
|
131
250
|
runOnJS(onSwipe)(direction, { distance, velocity })
|
|
132
251
|
}
|
|
133
|
-
|
|
134
|
-
|
|
252
|
+
|
|
253
|
+
// A commit may return per-axis exit transitions to run instead of
|
|
254
|
+
// the snap-back; any axis it leaves out snaps back as usual.
|
|
255
|
+
let exit: ReleaseResult | void = undefined
|
|
256
|
+
if (direction !== null && onCommit) {
|
|
257
|
+
exit = onCommit(direction, {
|
|
258
|
+
x: swipeX.value,
|
|
259
|
+
y: swipeY.value,
|
|
260
|
+
velocity: { x: e.velocityX, y: e.velocityY },
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// `onSwipeEnd` rides the settle callback of the swipe axis — the one
|
|
265
|
+
// animation whose end means "the card has arrived".
|
|
266
|
+
let settle: ((finished?: boolean) => void) | undefined
|
|
267
|
+
if (direction !== null && onSwipeEnd) {
|
|
268
|
+
const dir = direction
|
|
269
|
+
const end = onSwipeEnd
|
|
270
|
+
settle = (finished?: boolean) => {
|
|
271
|
+
runOnJS(end)(dir, { finished: finished === true })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const isHorizontalCommit = direction === 'left' || direction === 'right'
|
|
275
|
+
const settleX = isHorizontalCommit ? settle : undefined
|
|
276
|
+
const settleY = isHorizontalCommit ? undefined : settle
|
|
277
|
+
|
|
278
|
+
const exitX = exit ? exit.x : undefined
|
|
279
|
+
const exitY = exit ? exit.y : undefined
|
|
280
|
+
// Decay ignores its target (it has no `to`); the other types animate
|
|
281
|
+
// to `to`. The snap-back injects the release velocity so a spring
|
|
282
|
+
// reset continues the finger's momentum.
|
|
283
|
+
if (exitX) {
|
|
284
|
+
swipeX.value = buildReleaseAnimation(
|
|
285
|
+
exitX,
|
|
286
|
+
'to' in exitX ? exitX.to : swipeX.value,
|
|
287
|
+
settleX,
|
|
288
|
+
) as unknown as number
|
|
289
|
+
} else {
|
|
290
|
+
swipeX.value = buildReleaseAnimation(
|
|
291
|
+
withReleaseVelocity(snapBack, e.velocityX),
|
|
292
|
+
0,
|
|
293
|
+
settleX,
|
|
294
|
+
) as unknown as number
|
|
295
|
+
}
|
|
296
|
+
if (exitY) {
|
|
297
|
+
swipeY.value = buildReleaseAnimation(
|
|
298
|
+
exitY,
|
|
299
|
+
'to' in exitY ? exitY.to : swipeY.value,
|
|
300
|
+
settleY,
|
|
301
|
+
) as unknown as number
|
|
302
|
+
} else {
|
|
303
|
+
swipeY.value = buildReleaseAnimation(
|
|
304
|
+
withReleaseVelocity(snapBack, e.velocityY),
|
|
305
|
+
0,
|
|
306
|
+
settleY,
|
|
307
|
+
) as unknown as number
|
|
308
|
+
}
|
|
135
309
|
})
|
|
136
310
|
.onFinalize(() => {
|
|
137
311
|
'worklet'
|
|
@@ -145,7 +319,10 @@ export function useSwipe(options: SwipeOptions = {}): UseSwipeResult {
|
|
|
145
319
|
allowRight,
|
|
146
320
|
allowUp,
|
|
147
321
|
allowDown,
|
|
322
|
+
snapBack,
|
|
148
323
|
onSwipe,
|
|
324
|
+
onCommit,
|
|
325
|
+
onSwipeEnd,
|
|
149
326
|
swipeX,
|
|
150
327
|
swipeY,
|
|
151
328
|
isActive,
|
|
@@ -155,7 +332,30 @@ export function useSwipe(options: SwipeOptions = {}): UseSwipeResult {
|
|
|
155
332
|
transform: [{ translateX: swipeX.value }, { translateY: swipeY.value }],
|
|
156
333
|
}))
|
|
157
334
|
|
|
158
|
-
|
|
335
|
+
const reset = useCallback(() => {
|
|
336
|
+
swipeX.value = 0
|
|
337
|
+
swipeY.value = 0
|
|
338
|
+
}, [swipeX, swipeY])
|
|
339
|
+
|
|
340
|
+
return { gesture, animatedStyle, swipeX, swipeY, isActive, reset }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Inject the release velocity into a spring snap-back so the reset continues
|
|
345
|
+
* the finger's momentum. Leaves every other transition type — and a spring
|
|
346
|
+
* whose config already sets `velocity` — untouched.
|
|
347
|
+
*
|
|
348
|
+
* Worklet — runs on the UI thread inside the pan handler.
|
|
349
|
+
*/
|
|
350
|
+
function withReleaseVelocity(
|
|
351
|
+
cfg: TransitionConfig,
|
|
352
|
+
velocity: number,
|
|
353
|
+
): TransitionConfig {
|
|
354
|
+
'worklet'
|
|
355
|
+
// An omitted `type` means spring — the library default everywhere.
|
|
356
|
+
if (cfg.type !== 'spring' && cfg.type !== undefined) return cfg
|
|
357
|
+
if (cfg.velocity !== undefined) return cfg
|
|
358
|
+
return { ...cfg, velocity }
|
|
159
359
|
}
|
|
160
360
|
|
|
161
361
|
/**
|