Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ yieldsToContinuousGestures: boolean | SharedValue<boolean>;

Composes with [`disallowInterruption`](#disallowinterruption). When both are `true`, this handler still cancels discrete gestures (`Tap`, `LongPress`, `Fling`) on activation but allows continuous gestures (`Pan`, `Pinch`, `Rotation`, `Native`, `Manual`, `Hover`) to interrupt it. No-op when `disallowInterruption` is `false`. Defaults to `false`.

<Badges platforms={['android', 'ios']}>
### delaysChildPressedState
</Badges>

```ts
delaysChildPressedState: boolean | SharedValue<boolean>;
```

When `true`, the wrapped scrollable container delays displaying the pressed state of its children until it's clear that the gesture is not a scroll. Set it to `false` to display the pressed state immediately. Defaults to `true`.

On iOS this controls [`delaysContentTouches`](https://developer.apple.com/documentation/uikit/uiscrollview/delayscontenttouches) on the underlying `UIScrollView`. On Android it controls whether the container delays the pressed state of its children (see [`shouldDelayChildPressedState`](https://developer.android.com/reference/android/view/ViewGroup#shouldDelayChildPressedState%28%29)) — on Android, this requires React Native 0.87 or newer and is a no-op on older versions.

<BaseGestureConfig />

## Callbacks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
import com.swmansion.gesturehandler.react.events.eventbuilders.NativeGestureHandlerEventDataBuilder
import java.lang.reflect.Method

class NativeViewGestureHandler : GestureHandler() {
override val isContinuous = true
Expand All @@ -37,6 +38,17 @@ class NativeViewGestureHandler : GestureHandler() {
var yieldsToContinuousGestures = false
private set

/**
* When set, overrides whether the connected scrollable container delays the pressed state of
* its children (see [android.view.ViewGroup.shouldDelayChildPressedState]). Only applies to
* views implementing `HasChildPressedStateDelay` (React Native 0.87+), no-op otherwise. The
* override is applied for the duration of a gesture.
*/
var delaysChildPressedState: Boolean? = null
private set

private var pressedStateDelayOverriddenView: View? = null

private var hook: NativeViewGestureHandlerHook = defaultHook

private data class ActiveUpdateSnapshot(val pointerInside: Boolean, val numberOfPointers: Int, val pointerType: Int)
Expand All @@ -53,6 +65,7 @@ class NativeViewGestureHandler : GestureHandler() {
disallowInterruption = DEFAULT_DISALLOW_INTERRUPTION
yieldsToContinuousGestures = DEFAULT_YIELDS_TO_CONTINUOUS_GESTURES
shouldCancelWhenOutside = DEFAULT_SHOULD_CANCEL_WHEN_OUTSIDE
delaysChildPressedState = DEFAULT_DELAYS_CHILD_PRESSED_STATE
}

override fun shouldRecognizeSimultaneously(handler: GestureHandler): Boolean {
Expand Down Expand Up @@ -115,6 +128,14 @@ class NativeViewGestureHandler : GestureHandler() {
is ReactTextView -> this.hook = TextViewHook()
is ReactViewGroup -> this.hook = ReactViewGroupHook()
}

delaysChildPressedState?.let { delays ->
this.view?.let {
if (trySetChildPressedStateDelay(it, delays)) {
pressedStateDelayOverriddenView = it
}
}
}
}

override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) {
Expand Down Expand Up @@ -179,6 +200,9 @@ class NativeViewGestureHandler : GestureHandler() {
override fun onReset() {
this.hook = defaultHook
lastActiveUpdate = null
// `null` restores the view's default pressed state delay behavior.
pressedStateDelayOverriddenView?.let { trySetChildPressedStateDelay(it, null) }
pressedStateDelayOverriddenView = null
}

override fun dispatchHandlerUpdate(event: MotionEvent) {
Expand Down Expand Up @@ -214,6 +238,9 @@ class NativeViewGestureHandler : GestureHandler() {
if (config.hasKey(KEY_YIELDS_TO_CONTINUOUS_GESTURES)) {
handler.yieldsToContinuousGestures = config.getBoolean(KEY_YIELDS_TO_CONTINUOUS_GESTURES)
}
if (config.hasKey(KEY_DELAYS_CHILD_PRESSED_STATE)) {
handler.delaysChildPressedState = config.getBoolean(KEY_DELAYS_CHILD_PRESSED_STATE)
}
}

override fun createEventBuilder(handler: NativeViewGestureHandler) = NativeGestureHandlerEventDataBuilder(handler)
Expand All @@ -222,6 +249,7 @@ class NativeViewGestureHandler : GestureHandler() {
private const val KEY_SHOULD_ACTIVATE_ON_START = "shouldActivateOnStart"
private const val KEY_DISALLOW_INTERRUPTION = "disallowInterruption"
private const val KEY_YIELDS_TO_CONTINUOUS_GESTURES = "yieldsToContinuousGestures"
private const val KEY_DELAYS_CHILD_PRESSED_STATE = "delaysChildPressedState"
}
}

Expand All @@ -230,6 +258,33 @@ class NativeViewGestureHandler : GestureHandler() {
private const val DEFAULT_SHOULD_ACTIVATE_ON_START = false
private const val DEFAULT_DISALLOW_INTERRUPTION = false
private const val DEFAULT_YIELDS_TO_CONTINUOUS_GESTURES = false
private val DEFAULT_DELAYS_CHILD_PRESSED_STATE: Boolean = true

// `HasChildPressedStateDelay` was introduced in React Native 0.87 — it's accessed via
// reflection so that the library compiles and runs on older versions.
private val childPressedStateDelaySetter: Method? by lazy {
try {
Class
.forName("com.facebook.react.uimanager.HasChildPressedStateDelay")
.getMethod("setHasChildPressedStateDelay", Boolean::class.javaObjectType)
} catch (e: ReflectiveOperationException) {
null
}
}

private fun trySetChildPressedStateDelay(view: View, value: Boolean?): Boolean {
val setter = childPressedStateDelaySetter ?: return false
if (!setter.declaringClass.isInstance(view)) {
return false
}

return try {
setter.invoke(view, value)
true
} catch (e: ReflectiveOperationException) {
false
}
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ @implementation RNNativeViewGestureHandler {
BOOL _shouldActivateOnStart;
BOOL _disallowInterruption;
BOOL _yieldsToContinuousGestures;
BOOL _delaysChildPressedState;
RNGestureHandlerEventExtraData *_lastActiveExtraData;
}

- (instancetype)initWithTag:(NSNumber *)tag
{
if ((self = [super initWithTag:tag])) {
_recognizer = [[RNDummyGestureRecognizer alloc] initWithGestureHandler:self];
_delaysChildPressedState = YES;
}
return self;
}
Expand All @@ -131,6 +133,17 @@ - (void)updateConfig:(NSDictionary *)config
_shouldActivateOnStart = [RCTConvert BOOL:config[@"shouldActivateOnStart"]];
_disallowInterruption = [RCTConvert BOOL:config[@"disallowInterruption"]];
_yieldsToContinuousGestures = [RCTConvert BOOL:config[@"yieldsToContinuousGestures"]];

id delaysChildPressedState = config[@"delaysChildPressedState"];
_delaysChildPressedState = delaysChildPressedState == nil ? YES : [RCTConvert BOOL:delaysChildPressedState];

#if !TARGET_OS_OSX
// Config may be updated after the handler is bound to a view — re-apply to the connected
// scroll view if there is one.
if (self.recognizer.view != nil) {
[self retrieveScrollView:self.recognizer.view].delaysContentTouches = _delaysChildPressedState;
}
#endif
}

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

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

- (void)unbindFromView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export type NativeGestureNativeProperties = {
* `false`.
*/
yieldsToContinuousGestures?: boolean;

/**
* When `true`, the wrapped scrollable container delays displaying the
* pressed state of its children until it's clear that the gesture is not a
* scroll. Set it to `false` to display the pressed state immediately.
*
* On iOS this controls `delaysContentTouches` on the underlying
* `UIScrollView`. On Android it controls whether the container delays the
* pressed state of its children (see
* `ViewGroup.shouldDelayChildPressedState`) — this requires React Native
* 0.87 or newer and is a no-op on older versions.
*
* Defaults to `true`.
*/
delaysChildPressedState?: boolean;
};

export const NativeHandlerNativeProperties = new Set<
Expand All @@ -36,6 +51,7 @@ export const NativeHandlerNativeProperties = new Set<
'shouldActivateOnStart',
'disallowInterruption',
'yieldsToContinuousGestures',
'delaysChildPressedState',
]);

export type NativeHandlerData = {
Expand Down
Loading