Skip to content

Commit 9ebd8eb

Browse files
committed
Add delaysChildPressedState to native gesture
1 parent 5b70c4b commit 9ebd8eb

3 files changed

Lines changed: 80 additions & 2 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.facebook.react.views.textinput.ReactEditText
1616
import com.facebook.react.views.view.ReactViewGroup
1717
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
1818
import com.swmansion.gesturehandler.react.events.eventbuilders.NativeGestureHandlerEventDataBuilder
19+
import java.lang.reflect.Method
1920

2021
class NativeViewGestureHandler : GestureHandler() {
2122
override val isContinuous = true
@@ -37,6 +38,17 @@ class NativeViewGestureHandler : GestureHandler() {
3738
var yieldsToContinuousGestures = false
3839
private set
3940

41+
/**
42+
* When set, overrides whether the connected scrollable container delays the pressed state of
43+
* its children (see [android.view.ViewGroup.shouldDelayChildPressedState]). Only applies to
44+
* views implementing `HasChildPressedStateDelay` (React Native 0.87+), no-op otherwise. The
45+
* override is applied for the duration of a gesture.
46+
*/
47+
var delaysChildPressedState: Boolean? = null
48+
private set
49+
50+
private var pressedStateDelayOverriddenView: View? = null
51+
4052
private var hook: NativeViewGestureHandlerHook = defaultHook
4153

4254
private data class ActiveUpdateSnapshot(val pointerInside: Boolean, val numberOfPointers: Int, val pointerType: Int)
@@ -53,6 +65,7 @@ class NativeViewGestureHandler : GestureHandler() {
5365
disallowInterruption = DEFAULT_DISALLOW_INTERRUPTION
5466
yieldsToContinuousGestures = DEFAULT_YIELDS_TO_CONTINUOUS_GESTURES
5567
shouldCancelWhenOutside = DEFAULT_SHOULD_CANCEL_WHEN_OUTSIDE
68+
delaysChildPressedState = DEFAULT_DELAYS_CHILD_PRESSED_STATE
5669
}
5770

5871
override fun shouldRecognizeSimultaneously(handler: GestureHandler): Boolean {
@@ -115,6 +128,14 @@ class NativeViewGestureHandler : GestureHandler() {
115128
is ReactTextView -> this.hook = TextViewHook()
116129
is ReactViewGroup -> this.hook = ReactViewGroupHook()
117130
}
131+
132+
delaysChildPressedState?.let { delays ->
133+
this.view?.let {
134+
if (trySetChildPressedStateDelay(it, delays)) {
135+
pressedStateDelayOverriddenView = it
136+
}
137+
}
138+
}
118139
}
119140

120141
override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
@@ -179,6 +200,9 @@ class NativeViewGestureHandler : GestureHandler() {
179200
override fun onReset() {
180201
this.hook = defaultHook
181202
lastActiveUpdate = null
203+
// `null` restores the view's default pressed state delay behavior.
204+
pressedStateDelayOverriddenView?.let { trySetChildPressedStateDelay(it, null) }
205+
pressedStateDelayOverriddenView = null
182206
}
183207

184208
override fun dispatchHandlerUpdate(event: MotionEvent) {
@@ -214,6 +238,9 @@ class NativeViewGestureHandler : GestureHandler() {
214238
if (config.hasKey(KEY_YIELDS_TO_CONTINUOUS_GESTURES)) {
215239
handler.yieldsToContinuousGestures = config.getBoolean(KEY_YIELDS_TO_CONTINUOUS_GESTURES)
216240
}
241+
if (config.hasKey(KEY_DELAYS_CHILD_PRESSED_STATE)) {
242+
handler.delaysChildPressedState = config.getBoolean(KEY_DELAYS_CHILD_PRESSED_STATE)
243+
}
217244
}
218245

219246
override fun createEventBuilder(handler: NativeViewGestureHandler) = NativeGestureHandlerEventDataBuilder(handler)
@@ -222,6 +249,7 @@ class NativeViewGestureHandler : GestureHandler() {
222249
private const val KEY_SHOULD_ACTIVATE_ON_START = "shouldActivateOnStart"
223250
private const val KEY_DISALLOW_INTERRUPTION = "disallowInterruption"
224251
private const val KEY_YIELDS_TO_CONTINUOUS_GESTURES = "yieldsToContinuousGestures"
252+
private const val KEY_DELAYS_CHILD_PRESSED_STATE = "delaysChildPressedState"
225253
}
226254
}
227255

@@ -230,6 +258,28 @@ class NativeViewGestureHandler : GestureHandler() {
230258
private const val DEFAULT_SHOULD_ACTIVATE_ON_START = false
231259
private const val DEFAULT_DISALLOW_INTERRUPTION = false
232260
private const val DEFAULT_YIELDS_TO_CONTINUOUS_GESTURES = false
261+
private val DEFAULT_DELAYS_CHILD_PRESSED_STATE: Boolean? = null
262+
263+
// `HasChildPressedStateDelay` was introduced in React Native 0.87 — it's accessed via
264+
// reflection so that the library compiles and runs on older versions.
265+
private val childPressedStateDelaySetter: Method? by lazy {
266+
try {
267+
Class
268+
.forName("com.facebook.react.uimanager.HasChildPressedStateDelay")
269+
.getMethod("setHasChildPressedStateDelay", Boolean::class.javaObjectType)
270+
} catch (e: ReflectiveOperationException) {
271+
null
272+
}
273+
}
274+
275+
private fun trySetChildPressedStateDelay(view: View, value: Boolean?): Boolean {
276+
val setter = childPressedStateDelaySetter ?: return false
277+
if (!setter.declaringClass.isInstance(view)) {
278+
return false
279+
}
280+
setter.invoke(view, value)
281+
return true
282+
}
233283

234284
private fun tryIntercept(view: View, event: MotionEvent) = view is ViewGroup && view.onInterceptTouchEvent(event)
235285

packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,15 @@ @implementation RNNativeViewGestureHandler {
114114
BOOL _shouldActivateOnStart;
115115
BOOL _disallowInterruption;
116116
BOOL _yieldsToContinuousGestures;
117+
BOOL _delaysChildPressedState;
117118
RNGestureHandlerEventExtraData *_lastActiveExtraData;
118119
}
119120

120121
- (instancetype)initWithTag:(NSNumber *)tag
121122
{
122123
if ((self = [super initWithTag:tag])) {
123124
_recognizer = [[RNDummyGestureRecognizer alloc] initWithGestureHandler:self];
125+
_delaysChildPressedState = YES;
124126
}
125127
return self;
126128
}
@@ -131,6 +133,17 @@ - (void)updateConfig:(NSDictionary *)config
131133
_shouldActivateOnStart = [RCTConvert BOOL:config[@"shouldActivateOnStart"]];
132134
_disallowInterruption = [RCTConvert BOOL:config[@"disallowInterruption"]];
133135
_yieldsToContinuousGestures = [RCTConvert BOOL:config[@"yieldsToContinuousGestures"]];
136+
137+
id delaysChildPressedState = config[@"delaysChildPressedState"];
138+
_delaysChildPressedState = delaysChildPressedState == nil ? YES : [RCTConvert BOOL:delaysChildPressedState];
139+
140+
#if !TARGET_OS_OSX
141+
// Config may be updated after the handler is bound to a view — re-apply to the connected
142+
// scroll view if there is one.
143+
if (self.recognizer.view != nil) {
144+
[self retrieveScrollView:self.recognizer.view].delaysContentTouches = _delaysChildPressedState;
145+
}
146+
#endif
134147
}
135148

136149
#if !TARGET_OS_OSX
@@ -177,9 +190,10 @@ - (void)bindToView:(UIView *)view
177190

178191
// We can restore default scrollview behaviour to delay touches to scrollview's children
179192
// because gesture handler system can handle cancellation of scroll recognizer when JS responder
180-
// is set
193+
// is set. Setting `delaysChildPressedState` to `false` opts out of this, keeping touches delivered
194+
// to children immediately.
181195
UIScrollView *scrollView = [self retrieveScrollView:view];
182-
scrollView.delaysContentTouches = YES;
196+
scrollView.delaysContentTouches = _delaysChildPressedState;
183197
}
184198

185199
- (void)unbindFromView

packages/react-native-gesture-handler/src/v3/hooks/gestures/native/NativeTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ export type NativeGestureNativeProperties = {
2828
* `false`.
2929
*/
3030
yieldsToContinuousGestures?: boolean;
31+
32+
/**
33+
* When `false`, the wrapped scrollable container doesn't delay the pressed
34+
* state of its children, making them display press feedback immediately.
35+
*
36+
* On iOS this controls `delaysContentTouches` on the underlying
37+
* `UIScrollView`. On Android it controls whether the container delays the
38+
* pressed state of its children (see
39+
* `ViewGroup.shouldDelayChildPressedState`); requires React Native 0.87+.
40+
*
41+
* Defaults to `true`.
42+
*/
43+
delaysChildPressedState?: boolean;
3144
};
3245

3346
export const NativeHandlerNativeProperties = new Set<
@@ -36,6 +49,7 @@ export const NativeHandlerNativeProperties = new Set<
3649
'shouldActivateOnStart',
3750
'disallowInterruption',
3851
'yieldsToContinuousGestures',
52+
'delaysChildPressedState',
3953
]);
4054

4155
export type NativeHandlerData = {

0 commit comments

Comments
 (0)