Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ If you're unsure if one of your dependencies already renders `GestureHandlerRoot
If you're using gesture handler in your component library, you may want to wrap your library's code in the `GestureHandlerRootView` component. This will avoid extra configuration for the user.
:::

:::tip
If you're having trouble with gestures not working when inside a component provided by a third-party library, even though you've wrapped the entry point with `<GestureHandlerRootView>`, you can try adding another `<GestureHandlerRootView unstable_forceActive>` closer to the place the gestures are defined. This way, you can prevent Android from canceling relevant gestures when one of the native views tries to grab lock for delivering touch events.
:::

### 3. Platform specific setup

#### [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.LayoutShadowNode;

@SuppressWarnings("deprecation")
public class RNGestureHandlerButtonManagerDelegate<T extends View, U extends BaseViewManager<T, ? extends LayoutShadowNode> & RNGestureHandlerButtonManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public RNGestureHandlerButtonManagerDelegate(U viewManager) {
super(viewManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.LayoutShadowNode;

@SuppressWarnings("deprecation")
public class RNGestureHandlerRootViewManagerDelegate<T extends View, U extends BaseViewManager<T, ? extends LayoutShadowNode> & RNGestureHandlerRootViewManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public RNGestureHandlerRootViewManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
super.setProperty(view, propName, value);
switch (propName) {
case "unstable_forceActive":
mViewManager.setUnstable_forceActive(view, value == null ? false : (boolean) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
import com.facebook.react.uimanager.ViewManagerWithGeneratedInterface;

public interface RNGestureHandlerRootViewManagerInterface<T extends View> extends ViewManagerWithGeneratedInterface {
// No props
void setUnstable_forceActive(T view, boolean value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
import com.swmansion.gesturehandler.react.RNGestureHandlerRootView
import java.util.*

class GestureHandlerOrchestrator(
Expand Down Expand Up @@ -475,6 +476,12 @@ class GestureHandlerOrchestrator(

while (parent != null) {
if (parent is ViewGroup) {
// Stop traversing the hierarchy when encountering another active root view to prevent
// gestures from being extracted multiple times by different orchestrators.
if (parent is RNGestureHandlerRootView && parent.isRootViewEnabled()) {
break
}

val parentViewGroup: ViewGroup = parent

handlerRegistry.getHandlersForView(parent)?.let {
Expand Down Expand Up @@ -562,12 +569,26 @@ class GestureHandlerOrchestrator(
extractGestureHandlers(wrapperView, tempCoords, pointerId, event)
}

private fun shouldIgnoreSubtreeIfGestureHandlerRootView(view: View) =
view is RNGestureHandlerRootView && view != wrapperView && view.isRootViewEnabled()

private fun extractGestureHandlers(
viewGroup: ViewGroup,
coords: FloatArray,
pointerId: Int,
event: MotionEvent,
): Boolean {
if (shouldIgnoreSubtreeIfGestureHandlerRootView(viewGroup)) {
// When we encounter another active root view while traversing the view hierarchy, we want
// to stop there so that it can handle the gesture attached under it itself.
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
// cancel all gestures handled by its parent root view) but there may be some gestures attached
// to views under it which should work. Adding another root view under that particular view
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
// We want to stop here so the gesture receives event only once.
return false
}

val childrenCount = viewGroup.childCount
for (i in childrenCount - 1 downTo 0) {
val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i)
Expand Down Expand Up @@ -595,52 +616,63 @@ class GestureHandlerOrchestrator(
}

private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int, event: MotionEvent): Boolean =
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
PointerEventsConfig.NONE -> {
// This view and its children can't be the target
false
}
PointerEventsConfig.BOX_ONLY -> {
// This view is the target, its children don't matter
(
recordViewHandlersForPointer(view, coords, pointerId, event) ||
shouldHandlerlessViewBecomeTouchTarget(view, coords)
)
}
PointerEventsConfig.BOX_NONE -> {
// This view can't be the target, but its children might
when (view) {
is ViewGroup -> {
extractGestureHandlers(view, coords, pointerId, event).also { found ->
// A child view is handling touch, also extract handlers attached to this view
if (found) {
recordViewHandlersForPointer(view, coords, pointerId, event)
if (shouldIgnoreSubtreeIfGestureHandlerRootView(view)) {
// When we encounter another active root view while traversing the view hierarchy, we want
// to stop there so that it can handle the gesture attached under it itself.
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
// cancel all gestures handled by its parent root view) but there may be some gestures attached
// to views under it which should work. Adding another root view under that particular view
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
// We want to stop here so the gesture receives event only once.
false
} else {
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
PointerEventsConfig.NONE -> {
// This view and its children can't be the target
false
}
PointerEventsConfig.BOX_ONLY -> {
// This view is the target, its children don't matter
(
recordViewHandlersForPointer(view, coords, pointerId, event) ||
shouldHandlerlessViewBecomeTouchTarget(view, coords)
)
}
PointerEventsConfig.BOX_NONE -> {
// This view can't be the target, but its children might
when (view) {
is ViewGroup -> {
extractGestureHandlers(view, coords, pointerId, event).also { found ->
// A child view is handling touch, also extract handlers attached to this view
if (found) {
recordViewHandlersForPointer(view, coords, pointerId, event)
}
}
}
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
// handlers attached to the text input, as it makes sense that gestures would work on a
// non-editable TextInput.
is EditText -> {
recordViewHandlersForPointer(view, coords, pointerId, event)
}
else -> false
}
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
// handlers attached to the text input, as it makes sense that gestures would work on a
// non-editable TextInput.
is EditText -> {
recordViewHandlersForPointer(view, coords, pointerId, event)
}
else -> false
}
}
PointerEventsConfig.AUTO -> {
// Either this view or one of its children is the target
val found = if (view is ViewGroup) {
extractGestureHandlers(view, coords, pointerId, event)
} else {
false
}
PointerEventsConfig.AUTO -> {
// Either this view or one of its children is the target
val found = if (view is ViewGroup) {
extractGestureHandlers(view, coords, pointerId, event)
} else {
false
}

(
recordViewHandlersForPointer(view, coords, pointerId, event) ||
found ||
shouldHandlerlessViewBecomeTouchTarget(view, coords)
)
(
recordViewHandlersForPointer(view, coords, pointerId, event) ||
found ||
shouldHandlerlessViewBecomeTouchTarget(view, coords)
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import com.facebook.react.views.view.ReactViewGroup

class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
private var rootViewEnabled = false
private var unstableForceActive = false
private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit

override fun onAttachedToWindow() {
super.onAttachedToWindow()
rootViewEnabled = !hasGestureHandlerEnabledRootView(this)
rootViewEnabled = unstableForceActive || !hasGestureHandlerEnabledRootView(this)
if (!rootViewEnabled) {
Log.i(
ReactConstants.TAG,
Expand Down Expand Up @@ -56,6 +58,12 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
rootHelper?.activateNativeHandlers(view)
}

fun isRootViewEnabled() = rootViewEnabled

fun setUnstableForceActive(active: Boolean) {
this.unstableForceActive = active
}

companion object {
private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean {
UiThreadUtil.assertOnUiThread()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerDelegate
import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerInterface

Expand Down Expand Up @@ -32,6 +33,11 @@ class RNGestureHandlerRootViewManager :
view.tearDown()
}

@ReactProp(name = "unstable_forceActive")
override fun setUnstable_forceActive(view: RNGestureHandlerRootView, active: Boolean) {
view.setUnstableForceActive(active)
}

/**
* The following event configuration is necessary even if you are not using
* GestureHandlerRootView component directly.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import { ViewProps, StyleSheet } from 'react-native';
import { StyleSheet } from 'react-native';
import { maybeInitializeFabric } from '../init';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent';

export interface GestureHandlerRootViewProps
extends PropsWithChildren<ViewProps> {}
extends PropsWithChildren<RootViewNativeProps> {}

export default function GestureHandlerRootView({
style,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import { View, ViewProps, StyleSheet } from 'react-native';
import { View, StyleSheet } from 'react-native';
import { maybeInitializeFabric } from '../init';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';

export interface GestureHandlerRootViewProps
extends PropsWithChildren<ViewProps> {}
extends PropsWithChildren<RootViewNativeProps> {}

export default function GestureHandlerRootView({
style,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { ViewProps } from 'react-native';

interface NativeProps extends ViewProps {}
export interface RootViewNativeProps extends ViewProps {
unstable_forceActive?: boolean;
}

export default codegenNativeComponent<NativeProps>('RNGestureHandlerRootView');
export default codegenNativeComponent<RootViewNativeProps>(
'RNGestureHandlerRootView'
);
Loading