Skip to content

Commit df9abf7

Browse files
Fix Issue 23870: Android API - View.getGlobalVisibleRect() does not properly clip result rect for ReactClippingViewGroups (#26334)
Summary: This PR addresses issue #23870 (`View.getGlobalVisibleRect()` is broken in some use cases) The issue affects the following Android APIs: - ViewGroup.getChildVisibleRect() - View.getGlobalVisibleRect() (Which calls into ViewGroup.getChildVisibleRect) - View.getLocalVisibleRect() (Which calls into View.getGlobalVisibleRect()) According to Android documentation, View.getGlobalVisibleRect() should provide a rect for a given view that has been clipped by the bounds of all of its parent views up the view hierarchy. It does so through the use of the recursive function ViewGroup.getChildVisibleRect(). Since React Native has a separate clipping mechanism that does not rely on Android UI's clipping implementation, ViewGroup.getChildVisibleRect() is unable to determine that a rect should be clipped if the clipping view is a ReactClippingViewGroup. This resultantly breaks some important use cases for things like testing with Detox, which relies on this functionality to tell when a component is on-screen, as explained in the above referenced issue. The rationale of the fix is essentially to implement logic analogous to [ViewGroup.getChildVisibleRect()](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#6176), discarding irrelevant Android clipping modes, and instead testing against the 'overflow' property, restoring the originally intended functionality. This is implemented as an override to ViewGroup.getChildVisibleRect() in the following classes: - ReactViewGroup - ReactScrollView - ReactHorizontalScrollView Unfortunately, since the public ViewGroup.getChildVisibleRect() API recurses into a `hide` annotated API which cannot be overridden, it was necessary to provide this override in each of the above React Native classes to ensure the superclass implementation would not be called, which would break the recursion. ## Changelog [Android] [Fixed] - View.getGlobalVisibleRect() clips result rect properly when overflow is 'hidden' Pull Request resolved: #26334 Test Plan: The functionality in question is neither used internally nor exposed by React Native, and thus only affects Android native modules that use the above referenced APIs. As such, I have primarily performed testing with a forked example project that had been submitted with issue #23870, originally by d4vidi. The example project can be found here: - [Configured to build against RN Master](https://github.com/davidbiedenbach/RNClipVisibilityBugDemo/tree/rn-master) - [Configured to build against PR branch](https://github.com/davidbiedenbach/RNClipVisibilityBugDemo/tree/fix-23870) (Original project here: https://github.com/d4vidi/RNClipVisibilityBugDemo) ### Bug in effect: When built against RN master, it can be observed that fully clipped views are reported as visible, as in the below screenshots. #### Views inside a ReactViewGroup do not report as clipped ![BugScreen1](https://user-images.githubusercontent.com/1563532/64999573-314b6300-d89d-11e9-985e-294bd51a0ba9.jpg) #### Views inside a ReactScrollView do not report as clipped ![BugScreen2](https://user-images.githubusercontent.com/1563532/64999580-38727100-d89d-11e9-8186-96b25c937edc.jpg) #### Views inside a ReactHorizontalScrollView do not report clipping properly ![BugScreen4](https://user-images.githubusercontent.com/1563532/64999588-3f00e880-d89d-11e9-9477-7b79e44c5e46.jpg) ### Bug fixed When built against the PR branch, fully-clipped views no longer report visible. #### Views inside a ReactViewGroup report clipping properly ![FixScreen1](https://user-images.githubusercontent.com/1563532/64999634-6b1c6980-d89d-11e9-8534-b26b638bf4d8.jpg) #### Views inside a ReactScrollView report clipping properly ![FixScreen2](https://user-images.githubusercontent.com/1563532/64999641-7079b400-d89d-11e9-8f95-4d6e28bcf833.jpg) #### Views inside a ReactHorizontalScrollView report clipping properly ![FixScreen4](https://user-images.githubusercontent.com/1563532/64999645-74a5d180-d89d-11e9-9754-170bb3b620a2.jpg) Reviewed By: mdvacca Differential Revision: D17782658 Pulled By: yungsters fbshipit-source-id: 0cd0d385898579a7a8a3e453f6ba681679ebe496
1 parent 3b51499 commit df9abf7

File tree

4 files changed

+66
-0
lines changed

4 files changed

+66
-0
lines changed

ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroupHelper.java

+48
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package com.facebook.react.uimanager;
88

99
import android.graphics.Rect;
10+
import android.graphics.RectF;
1011
import android.view.View;
1112
import android.view.ViewParent;
1213
import javax.annotation.concurrent.NotThreadSafe;
@@ -55,4 +56,51 @@ public static void calculateClippingRect(View view, Rect outputRect) {
5556
}
5657
view.getDrawingRect(outputRect);
5758
}
59+
60+
public static boolean getChildVisibleRectHelper(
61+
View child, Rect r, android.graphics.Point offset, View parent, String overflow) {
62+
// This is based on the Android ViewGroup implementation, modified to clip child rects
63+
// if overflow is set to ViewProps.HIDDEN. This effectively solves Issue #23870 which
64+
// appears to have been introduced by FLAG_CLIP_CHILDREN being forced false
65+
// regardless of whether clipping is desired.
66+
final RectF rect = new RectF();
67+
rect.set(r);
68+
69+
child.getMatrix().mapRect(rect);
70+
71+
final int dx = child.getLeft() - parent.getScrollX();
72+
final int dy = child.getTop() - parent.getScrollY();
73+
74+
rect.offset(dx, dy);
75+
76+
if (offset != null) {
77+
float[] position = new float[2];
78+
position[0] = offset.x;
79+
position[1] = offset.y;
80+
child.getMatrix().mapPoints(position);
81+
offset.x = Math.round(position[0]) + dx;
82+
offset.y = Math.round(position[1]) + dy;
83+
}
84+
85+
final int width = parent.getRight() - parent.getLeft();
86+
final int height = parent.getBottom() - parent.getTop();
87+
88+
boolean rectIsVisible = true;
89+
90+
ViewParent grandparent = parent.getParent();
91+
if (grandparent == null || ViewProps.HIDDEN.equals(overflow)) {
92+
rectIsVisible = rect.intersect(0, 0, width, height);
93+
}
94+
95+
r.set(
96+
(int) Math.floor(rect.left),
97+
(int) Math.floor(rect.top),
98+
(int) Math.ceil(rect.right),
99+
(int) Math.ceil(rect.bottom));
100+
101+
if (rectIsVisible && grandparent != null) {
102+
rectIsVisible = grandparent.getChildVisibleRect(parent, r, offset);
103+
}
104+
return rectIsVisible;
105+
}
58106
}

ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java

+6
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@ public void getClippingRect(Rect outClippingRect) {
491491
outClippingRect.set(Assertions.assertNotNull(mClippingRect));
492492
}
493493

494+
@Override
495+
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
496+
return ReactClippingViewGroupHelper.getChildVisibleRectHelper(
497+
child, r, offset, this, mOverflow);
498+
}
499+
494500
private int getSnapInterval() {
495501
if (mSnapInterval != 0) {
496502
return mSnapInterval;

ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java

+6
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,12 @@ public void getClippingRect(Rect outClippingRect) {
342342
outClippingRect.set(Assertions.assertNotNull(mClippingRect));
343343
}
344344

345+
@Override
346+
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
347+
return ReactClippingViewGroupHelper.getChildVisibleRectHelper(
348+
child, r, offset, this, mOverflow);
349+
}
350+
345351
@Override
346352
public void fling(int velocityY) {
347353
// Workaround.

ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java

+6
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ private void updateSubviewClipStatus(View subview) {
455455
}
456456
}
457457

458+
@Override
459+
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
460+
return ReactClippingViewGroupHelper.getChildVisibleRectHelper(
461+
child, r, offset, this, mOverflow);
462+
}
463+
458464
@Override
459465
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
460466
super.onSizeChanged(w, h, oldw, oldh);

0 commit comments

Comments
 (0)