| 1 | diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 2 | index 4ab38424d1f5fc8b2686dd4a0a8058c454a9874d..0c5e0eaa995af50e5f8c02909dc6825121cc61af 100644 |
| 3 | --- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 4 | +++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 5 | @@ -31,11 +31,13 @@ const Swipeable = props => { |
| 6 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, |
| 7 | dragOffsetFromLeft = DEFAULT_DRAG_OFFSET, |
| 8 | dragOffsetFromRight = -DEFAULT_DRAG_OFFSET, |
| 9 | + failOffsetY, |
| 10 | friction = DEFAULT_FRICTION, |
| 11 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, |
| 12 | onSwipeableOpenStartDrag, |
| 13 | onSwipeableCloseStartDrag, |
| 14 | onSwipeableWillOpen, |
| 15 | + onSwipeableRelease, |
| 16 | onSwipeableWillClose, |
| 17 | onSwipeableOpen, |
| 18 | onSwipeableClose, |
| 19 | @@ -276,8 +278,12 @@ const Swipeable = props => { |
| 20 | toValue = -rightWidth.value; |
| 21 | } |
| 22 | } |
| 23 | + // Let a UI-thread full-swipe commit take over before the return spring. |
| 24 | + if (onSwipeableRelease?.(appliedTranslation)) { |
| 25 | + return; |
| 26 | + } |
| 27 | animateRow(toValue, velocityX / friction); |
| 28 | - }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag]); |
| 29 | + }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag, onSwipeableRelease, appliedTranslation]); |
| 30 | const close = useCallback(() => { |
| 31 | 'worklet'; |
| 32 | |
| 33 | @@ -302,6 +308,7 @@ const Swipeable = props => { |
| 34 | enabled: enabled ?? true, |
| 35 | enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture, |
| 36 | activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft], |
| 37 | + failOffsetY, |
| 38 | simultaneousWith, |
| 39 | requireToFail, |
| 40 | block, |
| 41 | diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 42 | index e97b6dbbced1bcbd359cf35293b7f10cbf13b0e7..de07f26423bee66ecd7d0f9bb33233bcb3215751 100644 |
| 43 | --- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 44 | +++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 45 | @@ -59,6 +59,12 @@ export type SwipeableProps = { |
| 46 | * Called when action panel is closed. |
| 47 | */ |
| 48 | onSwipeableClose?: (direction: SwipeDirection.LEFT | SwipeDirection.RIGHT) => void; |
| 49 | + /** |
| 50 | + * UI-thread worklet called on release before the built-in spring starts. |
| 51 | + * Return true to consume the release and take ownership of translation. |
| 52 | + * The caller must reset or remove the row after its custom animation. |
| 53 | + */ |
| 54 | + onSwipeableRelease?: (translation: SharedValue<number>) => boolean; |
| 55 | /** |
| 56 | * Called when action panel starts animating on open (either right or left). |
| 57 | */ |
| 58 | @@ -150,6 +156,13 @@ export type SwipeableProps = { |
| 59 | * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10. |
| 60 | */ |
| 61 | dragOffsetFromRight?: number; |
| 62 | + /** |
| 63 | + * Range along the Y axis (in points) that, once exceeded before the swipe |
| 64 | + * activates, makes the pan fail. Lets vertically-dominant pans (list |
| 65 | + * scrolling, especially trackpad two-finger pans) skip the swipe entirely. |
| 66 | + * Passed straight to the underlying Pan gesture's `failOffsetY`. |
| 67 | + */ |
| 68 | + failOffsetY?: number | [start: number, end: number]; |
| 69 | }>; |
| 70 | export interface SwipeableMethods { |
| 71 | close: () => void; |
| 72 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 73 | index 4881db1dc7575adc6610569035a8990cee414f3e..8431b738a9ffd09df48e1106b2bd5b62251d536c 100644 |
| 74 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 75 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 76 | @@ -61,11 +61,13 @@ const Swipeable = (props: SwipeableProps) => { |
| 77 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, |
| 78 | dragOffsetFromLeft = DEFAULT_DRAG_OFFSET, |
| 79 | dragOffsetFromRight = -DEFAULT_DRAG_OFFSET, |
| 80 | + failOffsetY, |
| 81 | friction = DEFAULT_FRICTION, |
| 82 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, |
| 83 | onSwipeableOpenStartDrag, |
| 84 | onSwipeableCloseStartDrag, |
| 85 | onSwipeableWillOpen, |
| 86 | + onSwipeableRelease, |
| 87 | onSwipeableWillClose, |
| 88 | onSwipeableOpen, |
| 89 | onSwipeableClose, |
| 90 | @@ -491,6 +493,11 @@ const Swipeable = (props: SwipeableProps) => { |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | + // Let a UI-thread full-swipe commit take over before the return spring. |
| 95 | + if (onSwipeableRelease?.(appliedTranslation)) { |
| 96 | + return; |
| 97 | + } |
| 98 | + |
| 99 | animateRow(toValue, velocityX / friction); |
| 100 | }, |
| 101 | [ |
| 102 | @@ -502,6 +509,8 @@ const Swipeable = (props: SwipeableProps) => { |
| 103 | rightWidth, |
| 104 | rowState, |
| 105 | userDrag, |
| 106 | + onSwipeableRelease, |
| 107 | + appliedTranslation, |
| 108 | ] |
| 109 | ); |
| 110 | |
| 111 | @@ -530,6 +539,7 @@ const Swipeable = (props: SwipeableProps) => { |
| 112 | enabled: enabled ?? true, |
| 113 | enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture, |
| 114 | activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft], |
| 115 | + failOffsetY, |
| 116 | simultaneousWith, |
| 117 | requireToFail, |
| 118 | block, |
| 119 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 120 | index 1e1246f709feb04c01cf3221472cc1219a8fffac..f526d4fe2d2738becbe8d29c59d1105afa11ae60 100644 |
| 121 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 122 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 123 | @@ -77,6 +77,13 @@ export type SwipeableProps = { |
| 124 | direction: SwipeDirection.LEFT | SwipeDirection.RIGHT |
| 125 | ) => void; |
| 126 | |
| 127 | + /** |
| 128 | + * UI-thread worklet called on release before the built-in spring starts. |
| 129 | + * Return true to consume the release and take ownership of translation. |
| 130 | + * The caller must reset or remove the row after its custom animation. |
| 131 | + */ |
| 132 | + onSwipeableRelease?: (translation: SharedValue<number>) => boolean; |
| 133 | + |
| 134 | /** |
| 135 | * Called when action panel starts animating on open (either right or left). |
| 136 | */ |
| 137 | @@ -196,6 +203,14 @@ export type SwipeableProps = { |
| 138 | * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10. |
| 139 | */ |
| 140 | dragOffsetFromRight?: number; |
| 141 | + |
| 142 | + /** |
| 143 | + * Range along the Y axis (in points) that, once exceeded before the swipe |
| 144 | + * activates, makes the pan fail. Lets vertically-dominant pans (list |
| 145 | + * scrolling, especially trackpad two-finger pans) skip the swipe entirely. |
| 146 | + * Passed straight to the underlying Pan gesture's `failOffsetY`. |
| 147 | + */ |
| 148 | + failOffsetY?: number | [start: number, end: number]; |
| 149 | }>; |
| 150 | |
| 151 | export interface SwipeableMethods { |
| 152 | diff --git a/apple/Handlers/RNPanHandler.m b/apple/Handlers/RNPanHandler.m |
| 153 | --- a/apple/Handlers/RNPanHandler.m |
| 154 | +++ b/apple/Handlers/RNPanHandler.m |
| 155 | @@ -31,6 +31,7 @@ |
| 156 | @property (nonatomic) CGFloat failOffsetYStart; |
| 157 | @property (nonatomic) CGFloat failOffsetYEnd; |
| 158 | @property (nonatomic) CGFloat activateAfterLongPress; |
| 159 | +@property (nonatomic) BOOL handlingTouchStart; |
| 160 | |
| 161 | #if !TARGET_OS_OSX && !TARGET_OS_TV |
| 162 | @property (atomic, readonly, strong) RNGHStylusData *stylusData; |
| 163 | @@ -126,9 +127,14 @@ |
| 164 | |
| 165 | - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event |
| 166 | { |
| 167 | - if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { |
| 168 | - self.state = UIGestureRecognizerStateFailed; |
| 169 | - return; |
| 170 | + if (self.state == UIGestureRecognizerStatePossible) { |
| 171 | + self.handlingTouchStart = touches.count > 0; |
| 172 | + BOOL shouldBegin = [self.delegate gestureRecognizerShouldBegin:self]; |
| 173 | + self.handlingTouchStart = NO; |
| 174 | + if (!shouldBegin) { |
| 175 | + self.state = UIGestureRecognizerStateFailed; |
| 176 | + return; |
| 177 | + } |
| 178 | } |
| 179 | |
| 180 | if (touches.count == 0) { |
| 181 | @@ -476,6 +482,23 @@ |
| 182 | |
| 183 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer |
| 184 | { |
| 185 | +#if !TARGET_OS_OSX && !TARGET_OS_TV |
| 186 | + RNBetterPanGestureRecognizer *recognizer = (RNBetterPanGestureRecognizer *)gestureRecognizer; |
| 187 | + if (!recognizer.handlingTouchStart && recognizer.allowedScrollTypesMask != 0 && recognizer.numberOfTouches == 0) { |
| 188 | + // Trackpad scroll events can begin without touchesMoved enforcing failure |
| 189 | + // offsets. Reject vertical scrolling before a horizontal row pan cancels |
| 190 | + // the enclosing UIScrollView. The manual touch-start preflight above has |
| 191 | + // no current translation yet, so leave that path to the touch callbacks. |
| 192 | + CGPoint translation = [recognizer translationInView:recognizer.view.window]; |
| 193 | + BOOL horizontalOnly = (!isnan(recognizer.activeOffsetXStart) || !isnan(recognizer.activeOffsetXEnd)) && |
| 194 | + isnan(recognizer.activeOffsetYStart) && isnan(recognizer.activeOffsetYEnd); |
| 195 | + if ([recognizer shouldFailUnderCustomCriteria] || |
| 196 | + (horizontalOnly && fabs(translation.y) > fabs(translation.x))) { |
| 197 | + return NO; |
| 198 | + } |
| 199 | + } |
| 200 | +#endif |
| 201 | + |
| 202 | RNGestureHandlerState savedState = _lastState; |
| 203 | BOOL shouldBegin = [super gestureRecognizerShouldBegin:gestureRecognizer]; |
| 204 | _lastState = savedState; |