Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow GestureHandlerRootView to be manually made active #2401

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import com.swmansion.gesturehandler.react.RNGestureHandlerRootView
import java.util.*

class GestureHandlerOrchestrator(
Expand Down Expand Up @@ -497,6 +498,17 @@ class GestureHandlerOrchestrator(
}

private fun extractGestureHandlers(viewGroup: ViewGroup, coords: FloatArray, pointerId: Int): Boolean {
if (viewGroup is RNGestureHandlerRootView && viewGroup != wrapperView && viewGroup.isActive()) {
// 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 All @@ -523,8 +535,19 @@ class GestureHandlerOrchestrator(
return false
}

private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int): Boolean =
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int): Boolean {
if (view is RNGestureHandlerRootView && view != wrapperView && view.isActive()) {
// 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
}

return when (viewConfigHelper.getPointerEventsConfigForView(view)) {
PointerEventsConfig.NONE -> {
// This view and its children can't be the target
false
Expand Down Expand Up @@ -569,6 +592,7 @@ class GestureHandlerOrchestrator(
)
}
}
}

private fun canReceiveEvents(view: View) =
view.visibility == View.VISIBLE && view.alpha >= minimumAlphaForTraversal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import com.facebook.react.views.view.ReactViewGroup

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

override fun onAttachedToWindow() {
super.onAttachedToWindow()
_enabled = !hasGestureHandlerEnabledRootView(this)
_enabled = active || !hasGestureHandlerEnabledRootView(this)
if (!_enabled) {
Log.i(
ReactConstants.TAG,
Expand Down Expand Up @@ -43,6 +45,17 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
super.requestDisallowInterceptTouchEvent(disallowIntercept)
}

// Those methods refer to different variables because we need to enforce at least
// one of the Root Views to be enabled to intecept events. By default only the
// top-most view is enabled, but users may want to enable more views to be able
// to work around some native views calling `requestDisallowInterceptTouchEvent`,
// which prevents gestures from working.
fun isActive() = _enabled

fun setActive(active: Boolean) {
this.active = 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 @@ -34,6 +35,11 @@ class RNGestureHandlerRootViewManager :
view.tearDown()
}

@ReactProp(name = "active")
override fun setActive(view: RNGestureHandlerRootView, active: Boolean) {
view.setActive(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
Expand Up @@ -20,6 +20,12 @@ public RNGestureHandlerRootViewManagerDelegate(U viewManager) {
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
super.setProperty(view, propName, value);
switch (propName) {
case "active":
mViewManager.setActive(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 @@ -12,5 +12,5 @@
import android.view.View;

public interface RNGestureHandlerRootViewManagerInterface<T extends View> {
// No props
void setActive(T view, boolean value);
}
4 changes: 4 additions & 0 deletions docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Note that `GestureHandlerRootView` acts like a normal `View`. So if you want it
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 active>` 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.
:::

### Linking

> **Important**: You only need to do this step if you're using React Native 0.59 or lower. Since v0.60, linking happens automatically.
Expand Down
4 changes: 3 additions & 1 deletion src/fabric/RNGestureHandlerRootViewNativeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { ViewProps } from 'react-native';

interface NativeProps extends ViewProps {}
interface NativeProps extends ViewProps {
active?: boolean;
}

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