Skip to content

Commit 3bde637

Browse files
authored
bugfix(Android): zIndex issues on android #8061 (#8136)
1 parent 80063a6 commit 3bde637

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.reactnativenavigation.views.touch
22

33
import android.view.MotionEvent
4+
import android.view.View
5+
import android.view.ViewGroup
46
import androidx.annotation.VisibleForTesting
7+
import com.facebook.react.views.debuggingoverlay.DebuggingOverlay
58
import com.reactnativenavigation.options.params.Bool
69
import com.reactnativenavigation.options.params.NullBool
710
import com.reactnativenavigation.react.ReactView
811
import com.reactnativenavigation.utils.coordinatesInsideView
912
import com.reactnativenavigation.views.component.ComponentLayout
13+
import androidx.core.view.isVisible
1014

11-
open class OverlayTouchDelegate(private val component: ComponentLayout, private val reactView: ReactView) {
15+
open class OverlayTouchDelegate(
16+
private val component: ComponentLayout,
17+
private val reactView: ReactView
18+
) {
1219
var interceptTouchOutside: Bool = NullBool()
1320

1421
fun onInterceptTouchEvent(event: MotionEvent): Boolean {
@@ -19,8 +26,38 @@ open class OverlayTouchDelegate(private val component: ComponentLayout, private
1926
}
2027

2128
@VisibleForTesting
22-
open fun handleDown(event: MotionEvent) = when (event.coordinatesInsideView(reactView.getChildAt(0))) {
29+
open fun handleDown(event: MotionEvent) = when (isInsideView(event)) {
2330
true -> component.superOnInterceptTouchEvent(event)
2431
false -> interceptTouchOutside.isFalse
2532
}
33+
34+
/**
35+
* In new architecture, ReactView could have a DebugOverlay as a child that covers the entire screen.
36+
* We need to check if the touch event is inside the actual React content. So we go over all children
37+
* of the ReactView and check if the event is inside any of them except the DebugOverlay.
38+
*
39+
* Example of ReactView hierarchy:
40+
* ```
41+
* ReactView
42+
* └── ReactSurfaceView
43+
* ├── ReactViewGroup
44+
* │ └── DebuggingOverlay (covers entire screen)
45+
* └── ReactViewGroup (the content we care about)
46+
* ```
47+
*/
48+
private fun isInsideView(event: MotionEvent): Boolean {
49+
val reactViewSurface = this.reactView.getChildAt(0) as ViewGroup
50+
for (i in 0 until reactViewSurface.childCount) {
51+
val childItem = reactViewSurface.getChildAt(i)
52+
53+
if (!debuggingOverlay(childItem) && childItem.isVisible && event.coordinatesInsideView(childItem)) {
54+
return true
55+
}
56+
}
57+
return false
58+
}
59+
60+
private fun debuggingOverlay(childItem: View?): Boolean =
61+
childItem is ViewGroup && childItem.getChildAt(0) is DebuggingOverlay
62+
2663
}

lib/android/app/src/test/java/com/reactnativenavigation/views/OverlayTouchDelegateTest.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
package com.reactnativenavigation.views;
22

3+
import android.graphics.Rect;
34
import android.view.MotionEvent;
5+
import android.view.View;
6+
import android.view.ViewGroup;
47

8+
import com.facebook.react.views.debuggingoverlay.DebuggingOverlay;
59
import com.reactnativenavigation.BaseTest;
610
import com.reactnativenavigation.options.params.Bool;
711
import com.reactnativenavigation.react.ReactView;
812
import com.reactnativenavigation.views.component.ComponentLayout;
913
import com.reactnativenavigation.views.touch.OverlayTouchDelegate;
1014

1115
import org.junit.Test;
16+
import org.mockito.stubbing.Answer;
1217

1318
import static org.assertj.core.api.Java6Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.doAnswer;
1421
import static org.mockito.Mockito.mock;
1522
import static org.mockito.Mockito.spy;
1623
import static org.mockito.Mockito.times;
1724
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
1826

1927
public class OverlayTouchDelegateTest extends BaseTest {
2028
private OverlayTouchDelegate uut;
@@ -23,16 +31,60 @@ public class OverlayTouchDelegateTest extends BaseTest {
2331
private final MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 0);
2432
private final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 0);
2533
private ComponentLayout component;
34+
private ReactView reactView;
2635

2736
@Override
2837
public void beforeEach() {
29-
ReactView reactView = mock(ReactView.class);
38+
reactView = mock(ReactView.class);
3039
component = mock(ComponentLayout.class);
3140
uut = spy(new OverlayTouchDelegate(component, reactView));
3241
}
3342

43+
private void mockHierarchyWithDebuggingOverlay() {
44+
// Mock the hierarchy: ReactView -> ReactSurfaceView -> ReactViewGroup(s)
45+
ViewGroup reactSurfaceView = mock(ViewGroup.class);
46+
ViewGroup debuggingOverlayContainer = mock(ViewGroup.class);
47+
ViewGroup contentViewGroup = mock(ViewGroup.class);
48+
DebuggingOverlay debuggingOverlay = mock(DebuggingOverlay.class);
49+
50+
// Set up ReactView -> ReactSurfaceView
51+
when(reactView.getChildAt(0)).thenReturn(reactSurfaceView);
52+
when(reactView.getChildCount()).thenReturn(1);
53+
54+
// Set up ReactSurfaceView -> ReactViewGroup(s)
55+
// First child: ViewGroup with DebuggingOverlay (should be skipped)
56+
when(reactSurfaceView.getChildAt(0)).thenReturn(debuggingOverlayContainer);
57+
when(reactSurfaceView.getChildAt(1)).thenReturn(contentViewGroup);
58+
when(reactSurfaceView.getChildCount()).thenReturn(2);
59+
60+
// Set up debuggingOverlayContainer: has DebuggingOverlay as first child
61+
when(debuggingOverlayContainer.getChildAt(0)).thenReturn(debuggingOverlay);
62+
63+
// Set up contentViewGroup: not a DebuggingOverlay, visible, and coordinates
64+
// inside
65+
when(contentViewGroup.getChildAt(0)).thenReturn(null); // Not a DebuggingOverlay
66+
when(contentViewGroup.getVisibility()).thenReturn(View.VISIBLE); // For isVisible extension
67+
68+
// Set up getHitRect for coordinatesInsideView to work
69+
Rect hitRect = new Rect(0, 0, 100, 100);
70+
doAnswer((Answer<Void>) invocation -> {
71+
Rect rect = invocation.getArgument(0);
72+
rect.set(hitRect);
73+
return null;
74+
}).when(contentViewGroup).getHitRect(any(Rect.class));
75+
76+
// Also mock getHitRect for debuggingOverlayContainer (though it should be
77+
// skipped)
78+
doAnswer((Answer<Void>) invocation -> {
79+
Rect rect = invocation.getArgument(0);
80+
rect.set(new Rect(0, 0, 100, 100));
81+
return null;
82+
}).when(debuggingOverlayContainer).getHitRect(any(Rect.class));
83+
}
84+
3485
@Test
3586
public void downEventIsHandled() {
87+
mockHierarchyWithDebuggingOverlay();
3688
uut.setInterceptTouchOutside(new Bool(true));
3789
uut.onInterceptTouchEvent(downEvent);
3890
verify(uut, times(1)).handleDown(downEvent);

0 commit comments

Comments
 (0)