Skip to content

Commit

Permalink
[Android] Change onTouchEvent to dispatchTouchEvent in `NativeVie…
Browse files Browse the repository at this point in the history
…wGestureHandler` (#3244)

## Description

Currently wrapping `WebView` into `NativeViewGestureHandler`  doesn't work as expected - events from `native` gesture do not reach `WebView`. This happens because inside `NativeViewGestureHandler` we call `onTouchEvent` method. In native hierarchy `WebView` is nested inside `ViewGroup`, which means that calling `onTouchEvent` instead of `dispatchTouchEvent` won't do anything to the `WebView`.

Should fix #2454 

Fixes #3196 

## Test plan

Tested on example app and the code below:

<details>
<summary>Test code</summary>

```jsx
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';

function WebViewScreen() {
  const native = Gesture.Native()
    .shouldActivateOnStart(true)
    .disallowInterruption(true);
  const pan = Gesture.Pan().onChange(console.log);

  const g = Gesture.Simultaneous(native, pan);

  return (
    <View style={styles.webViewContainer}>
      <GestureDetector gesture={g}>
        <WebView
          source={{ uri: 'https://templates.tiptap.dev/nw6Cmz6HfD' }}
          style={styles.webView}
          javaScriptEnabled={true}
          domStorageEnabled={true}
          startInLoadingState={true}
          onError={(syntheticEvent) => {
            const { nativeEvent } = syntheticEvent;
            console.warn('WebView error: ', nativeEvent);
          }}
        />
      </GestureDetector>
    </View>
  );
}

export default function BuggyApp() {
  return (
    <GestureHandlerRootView>
      <WebViewScreen />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    fontSize: 18,
  },
  webView: {
    flex: 1,
  },
  webViewContainer: {
    flex: 1,
    width: '100%',
  },
});
```

</details>
  • Loading branch information
m-bert authored Dec 4, 2024
1 parent 3d137ff commit f0604b0
Showing 1 changed file with 20 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.widget.ScrollView
import com.facebook.react.views.scroll.ReactScrollView
import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager
import com.swmansion.gesturehandler.react.isScreenReaderOn

Expand Down Expand Up @@ -79,6 +80,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
is ReactEditText -> this.hook = EditTextHook(this, view)
is ReactSwipeRefreshLayout -> this.hook = SwipeRefreshLayoutHook(this, view)
is ReactScrollView -> this.hook = ScrollViewHook()
is ReactViewGroup -> this.hook = ReactViewGroupHook()
}
}

Expand All @@ -99,7 +101,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
if (state == STATE_UNDETERMINED && !hook.canBegin(event)) {
cancel()
} else {
view.onTouchEvent(event)
hook.sendTouchEvent(view, event)
if ((state == STATE_UNDETERMINED || state == STATE_BEGAN) && view.isPressed) {
activate()
}
Expand All @@ -116,12 +118,12 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
when {
shouldActivateOnStart -> {
tryIntercept(view, event)
view.onTouchEvent(event)
hook.sendTouchEvent(view, event)
activate()
}

tryIntercept(view, event) -> {
view.onTouchEvent(event)
hook.sendTouchEvent(view, event)
activate()
}

Expand All @@ -136,7 +138,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
}
}
} else if (state == STATE_ACTIVE) {
view.onTouchEvent(event)
hook.sendTouchEvent(view, event)
}
}

Expand All @@ -145,7 +147,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
val event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0).apply {
action = MotionEvent.ACTION_CANCEL
}
view!!.onTouchEvent(event)
hook.sendTouchEvent(view, event)
event.recycle()
}

Expand Down Expand Up @@ -199,6 +201,11 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
* by this one.
*/
fun shouldCancelRootViewGestureHandlerIfNecessary() = false

/**
* Passes the event down to the underlying view using the correct method.
*/
fun sendTouchEvent(view: View?, event: MotionEvent) = view?.onTouchEvent(event)
}

private class EditTextHook(
Expand Down Expand Up @@ -278,4 +285,12 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
private class ScrollViewHook : NativeViewGestureHandlerHook {
override fun shouldCancelRootViewGestureHandlerIfNecessary() = true
}

private class ReactViewGroupHook : NativeViewGestureHandlerHook {
// There are cases where a native component is wrapped with a `ReactViewGroup` (the component is rendered
// inside a `<View />` component in JS). In such cases, calling `onTouchEvent` wouldn't work as those are
// ignored by the wrapper view. Instead `dispatchTouchEvent` can be used, which causes the view to dispatch
// the event to its children.
override fun sendTouchEvent(view: View?, event: MotionEvent) = view?.dispatchTouchEvent(event)
}
}

0 comments on commit f0604b0

Please sign in to comment.