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
@@ -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 {
Expand All @@ -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) {
Comment thread
gosha212 marked this conversation as resolved.
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

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
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;
import com.reactnativenavigation.views.component.ComponentLayout;
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;
Expand All @@ -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<Void>) 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<Void>) 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);
Expand Down