diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt b/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt index 2c789aee30c..81e3b74c653 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt @@ -1,14 +1,21 @@ package com.reactnativenavigation.views.touch import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.annotation.VisibleForTesting +import com.facebook.react.views.debuggingoverlay.DebuggingOverlay import com.reactnativenavigation.options.params.Bool import com.reactnativenavigation.options.params.NullBool import com.reactnativenavigation.react.ReactView import com.reactnativenavigation.utils.coordinatesInsideView import com.reactnativenavigation.views.component.ComponentLayout +import androidx.core.view.isVisible -open class OverlayTouchDelegate(private val component: ComponentLayout, private val reactView: ReactView) { +open class OverlayTouchDelegate( + private val component: ComponentLayout, + private val reactView: ReactView +) { var interceptTouchOutside: Bool = NullBool() fun onInterceptTouchEvent(event: MotionEvent): Boolean { @@ -19,8 +26,38 @@ open class OverlayTouchDelegate(private val component: ComponentLayout, private } @VisibleForTesting - open fun handleDown(event: MotionEvent) = when (event.coordinatesInsideView(reactView.getChildAt(0))) { + open fun handleDown(event: MotionEvent) = when (isInsideView(event)) { true -> component.superOnInterceptTouchEvent(event) false -> interceptTouchOutside.isFalse } + + /** + * In new architecture, ReactView could have a DebugOverlay as a child that covers the entire screen. + * We need to check if the touch event is inside the actual React content. So we go over all children + * of the ReactView and check if the event is inside any of them except the DebugOverlay. + * + * Example of ReactView hierarchy: + * ``` + * ReactView + * └── ReactSurfaceView + * ├── ReactViewGroup + * │ └── DebuggingOverlay (covers entire screen) + * └── ReactViewGroup (the content we care about) + * ``` + */ + private fun isInsideView(event: MotionEvent): Boolean { + val reactViewSurface = this.reactView.getChildAt(0) as ViewGroup + for (i in 0 until reactViewSurface.childCount) { + val childItem = reactViewSurface.getChildAt(i) + + if (!debuggingOverlay(childItem) && childItem.isVisible && event.coordinatesInsideView(childItem)) { + return true + } + } + return false + } + + private fun debuggingOverlay(childItem: View?): Boolean = + childItem is ViewGroup && childItem.getChildAt(0) is DebuggingOverlay + } \ No newline at end of file diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java index 8858f0a4561..80ba17d7ef0 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java @@ -1,7 +1,11 @@ package com.reactnativenavigation.views; +import android.graphics.Rect; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import com.facebook.react.views.debuggingoverlay.DebuggingOverlay; import com.reactnativenavigation.BaseTest; import com.reactnativenavigation.options.params.Bool; import com.reactnativenavigation.react.ReactView; @@ -9,12 +13,16 @@ import com.reactnativenavigation.views.touch.OverlayTouchDelegate; import org.junit.Test; +import org.mockito.stubbing.Answer; import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class OverlayTouchDelegateTest extends BaseTest { private OverlayTouchDelegate uut; @@ -23,16 +31,60 @@ public class OverlayTouchDelegateTest extends BaseTest { private final MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 0); private final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 0); private ComponentLayout component; + private ReactView reactView; @Override public void beforeEach() { - ReactView reactView = mock(ReactView.class); + reactView = mock(ReactView.class); component = mock(ComponentLayout.class); uut = spy(new OverlayTouchDelegate(component, reactView)); } + private void mockHierarchyWithDebuggingOverlay() { + // Mock the hierarchy: ReactView -> ReactSurfaceView -> ReactViewGroup(s) + ViewGroup reactSurfaceView = mock(ViewGroup.class); + ViewGroup debuggingOverlayContainer = mock(ViewGroup.class); + ViewGroup contentViewGroup = mock(ViewGroup.class); + DebuggingOverlay debuggingOverlay = mock(DebuggingOverlay.class); + + // Set up ReactView -> ReactSurfaceView + when(reactView.getChildAt(0)).thenReturn(reactSurfaceView); + when(reactView.getChildCount()).thenReturn(1); + + // Set up ReactSurfaceView -> ReactViewGroup(s) + // First child: ViewGroup with DebuggingOverlay (should be skipped) + when(reactSurfaceView.getChildAt(0)).thenReturn(debuggingOverlayContainer); + when(reactSurfaceView.getChildAt(1)).thenReturn(contentViewGroup); + when(reactSurfaceView.getChildCount()).thenReturn(2); + + // Set up debuggingOverlayContainer: has DebuggingOverlay as first child + when(debuggingOverlayContainer.getChildAt(0)).thenReturn(debuggingOverlay); + + // Set up contentViewGroup: not a DebuggingOverlay, visible, and coordinates + // inside + when(contentViewGroup.getChildAt(0)).thenReturn(null); // Not a DebuggingOverlay + when(contentViewGroup.getVisibility()).thenReturn(View.VISIBLE); // For isVisible extension + + // Set up getHitRect for coordinatesInsideView to work + Rect hitRect = new Rect(0, 0, 100, 100); + doAnswer((Answer) invocation -> { + Rect rect = invocation.getArgument(0); + rect.set(hitRect); + return null; + }).when(contentViewGroup).getHitRect(any(Rect.class)); + + // Also mock getHitRect for debuggingOverlayContainer (though it should be + // skipped) + doAnswer((Answer) invocation -> { + Rect rect = invocation.getArgument(0); + rect.set(new Rect(0, 0, 100, 100)); + return null; + }).when(debuggingOverlayContainer).getHitRect(any(Rect.class)); + } + @Test public void downEventIsHandled() { + mockHierarchyWithDebuggingOverlay(); uut.setInterceptTouchOutside(new Bool(true)); uut.onInterceptTouchEvent(downEvent); verify(uut, times(1)).handleDown(downEvent);