Skip to content

Commit 16de556

Browse files
committed
Fixed #8061 zIndex issues on android
1 parent 6ebb7b6 commit 16de556

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

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,20 @@
11
package com.reactnativenavigation.views.touch
22

33
import android.view.MotionEvent
4+
import android.view.ViewGroup
45
import androidx.annotation.VisibleForTesting
6+
import com.facebook.react.views.debuggingoverlay.DebuggingOverlay
57
import com.reactnativenavigation.options.params.Bool
68
import com.reactnativenavigation.options.params.NullBool
79
import com.reactnativenavigation.react.ReactView
810
import com.reactnativenavigation.utils.coordinatesInsideView
911
import com.reactnativenavigation.views.component.ComponentLayout
12+
import androidx.core.view.isVisible
1013

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

1420
fun onInterceptTouchEvent(event: MotionEvent): Boolean {
@@ -19,8 +25,39 @@ open class OverlayTouchDelegate(private val component: ComponentLayout, private
1925
}
2026

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

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
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.invocation.InvocationOnMock;
17+
import org.mockito.stubbing.Answer;
1218

1319
import static org.assertj.core.api.Java6Assertions.assertThat;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.Mockito.doAnswer;
1422
import static org.mockito.Mockito.mock;
1523
import static org.mockito.Mockito.spy;
1624
import static org.mockito.Mockito.times;
1725
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
1827

1928
public class OverlayTouchDelegateTest extends BaseTest {
2029
private OverlayTouchDelegate uut;
@@ -23,14 +32,59 @@ public class OverlayTouchDelegateTest extends BaseTest {
2332
private final MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 0);
2433
private final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 0);
2534
private ComponentLayout component;
35+
private ReactView reactView;
2636

2737
@Override
2838
public void beforeEach() {
29-
ReactView reactView = mock(ReactView.class);
30-
component = mock(ComponentLayout.class);
39+
mockHierarchy();
3140
uut = spy(new OverlayTouchDelegate(component, reactView));
3241
}
3342

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

0 commit comments

Comments
 (0)