Support clipping to children everywhere using ReactViewBackgroundManager (#44734)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44734

Fixes https://github.com/facebook/react-native/issues/44671

This integrates functionality for clipping content to padding box into `ReactViewBackgroundManager`, to be shared between several ViewManagers. In practice, this means:

1. `overflow: hidden` now works on `Text` and `TextInput`
2. ScrollView children are now clipped to the interior of borders, included curved ones via borderRadius

This will be made more generic, then start being used in ReactViewGroup, and eventually ReactImage. That abstraction will then hide away extra background management we will use for shadows.

Different places in code currently do clipping in any of `draw()`, `onDraw()`, or `dispatchDraw()`. The distinction between these, is that `draw()` allows code to run before drawing background even, `onDraw()` is invoked before drawing foreground, and `dispatchDraw()` is before drawing children. We don't want to clip out borders/shadows, but do want to clip foreground content like text, so I used `onDraw()` here.

Changelog:
[Android][Fixed] - Better overflow support for ScrollView, Text, TextInput

Reviewed By: rozele

Differential Revision: D57953429

fbshipit-source-id: ca3b788deb4b32706df7db958877d18f525c039c
This commit is contained in:
Nick Gerleman
2024-06-11 16:13:28 -07:00
committed by Facebook GitHub Bot
parent c9d7774a24
commit bfb3b7008d
8 changed files with 115 additions and 27 deletions
@@ -6703,7 +6703,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android
public fun onChildViewAdded (Landroid/view/View;Landroid/view/View;)V
public fun onChildViewRemoved (Landroid/view/View;Landroid/view/View;)V
protected fun onDetachedFromWindow ()V
protected fun onDraw (Landroid/graphics/Canvas;)V
public fun onDraw (Landroid/graphics/Canvas;)V
public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z
protected fun onLayout (ZIIII)V
public fun onLayoutChange (Landroid/view/View;IIIIIIII)V
@@ -6832,6 +6832,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc
public fun onChildViewAdded (Landroid/view/View;Landroid/view/View;)V
public fun onChildViewRemoved (Landroid/view/View;Landroid/view/View;)V
protected fun onDetachedFromWindow ()V
public fun onDraw (Landroid/graphics/Canvas;)V
public fun onInitializeAccessibilityNodeInfo (Landroid/view/accessibility/AccessibilityNodeInfo;)V
public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z
protected fun onLayout (ZIIII)V
@@ -7383,6 +7384,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
public fun setMinimumFontSize (F)V
public fun setNotifyOnInlineViewLayout (Z)V
public fun setNumberOfLines (I)V
public fun setOverflow (Ljava/lang/String;)V
public fun setSpanned (Landroid/text/Spannable;)V
public fun setText (Lcom/facebook/react/views/text/ReactTextUpdate;)V
public fun setTextIsSelectable (Z)V
@@ -7408,6 +7410,7 @@ public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/r
protected fun onAfterUpdateTransaction (Lcom/facebook/react/views/text/ReactTextView;)V
protected synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View;
protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/text/ReactTextView;)Lcom/facebook/react/views/text/ReactTextView;
public fun setOverflow (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
public synthetic fun setPadding (Landroid/view/View;IIII)V
public fun setPadding (Lcom/facebook/react/views/text/ReactTextView;IIII)V
public synthetic fun updateExtraData (Landroid/view/View;Ljava/lang/Object;)V
@@ -7687,6 +7690,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp
public fun onAttachedToWindow ()V
public fun onCreateInputConnection (Landroid/view/inputmethod/EditorInfo;)Landroid/view/inputmethod/InputConnection;
public fun onDetachedFromWindow ()V
public fun onDraw (Landroid/graphics/Canvas;)V
public fun onFinishTemporaryDetach ()V
protected fun onFocusChanged (ZILandroid/graphics/Rect;)V
public fun onKeyUp (ILandroid/view/KeyEvent;)Z
@@ -7719,6 +7723,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp
public fun setLineHeight (I)V
public fun setMaxFontSizeMultiplier (F)V
public fun setOnKeyPress (Z)V
public fun setOverflow (Ljava/lang/String;)V
public fun setPlaceholder (Ljava/lang/String;)V
public fun setReturnKeyType (Ljava/lang/String;)V
public fun setScrollWatcher (Lcom/facebook/react/views/textinput/ScrollWatcher;)V
@@ -7804,6 +7809,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face
public fun setOnKeyPress (Lcom/facebook/react/views/textinput/ReactEditText;Z)V
public fun setOnScroll (Lcom/facebook/react/views/textinput/ReactEditText;Z)V
public fun setOnSelectionChange (Lcom/facebook/react/views/textinput/ReactEditText;Z)V
public fun setOverflow (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
public synthetic fun setPadding (Landroid/view/View;IIII)V
public fun setPadding (Lcom/facebook/react/views/textinput/ReactEditText;IIII)V
public fun setPlaceholder (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
@@ -7912,12 +7918,14 @@ public class com/facebook/react/views/view/ReactViewBackgroundManager {
public fun cleanup ()V
public fun getBackgroundColor ()I
public fun getBorderColor (I)I
public fun maybeClipToPaddingBox (Landroid/graphics/Canvas;)V
public fun setBackgroundColor (I)V
public fun setBorderColor (IFF)V
public fun setBorderRadius (F)V
public fun setBorderRadius (FI)V
public fun setBorderStyle (Ljava/lang/String;)V
public fun setBorderWidth (IF)V
public fun setOverflow (Ljava/lang/String;)V
}
public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView, com/facebook/react/uimanager/ReactZIndexedViewGroup {
@@ -84,7 +84,6 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final @Nullable OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect();
private final Rect mOverflowInset = new Rect();
private boolean mActivelyScrolling;
@@ -145,6 +144,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
setOnHierarchyChangeListener(this);
setClipChildren(false);
mReactBackgroundManager.setOverflow(ViewProps.SCROLL);
}
public boolean getScrollEnabled() {
@@ -273,7 +273,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
public void setOverflow(@Nullable String overflow) {
mOverflow = overflow;
invalidate();
mReactBackgroundManager.setOverflow(overflow == null ? ViewProps.SCROLL : overflow);
}
public void setMaintainVisibleContentPosition(
@@ -306,14 +306,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
}
@Override
protected void onDraw(Canvas canvas) {
if (DEBUG_MODE) {
FLog.i(TAG, "onDraw[%d]", getId());
}
getDrawingRect(mRect);
if (!ViewProps.VISIBLE.equals(mOverflow)) {
canvas.clipRect(mRect);
}
public void onDraw(Canvas canvas) {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
super.onDraw(canvas);
}
@@ -83,7 +83,6 @@ public class ReactScrollView extends ScrollView
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final @Nullable OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect(); // for reuse to avoid allocation
private final Rect mTempRect = new Rect();
private final Rect mOverflowInset = new Rect();
@@ -136,6 +135,7 @@ public class ReactScrollView extends ScrollView
setOnHierarchyChangeListener(this);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
setClipChildren(false);
mReactBackgroundManager.setOverflow(ViewProps.SCROLL);
ViewCompat.setAccessibilityDelegate(this, new ReactScrollViewAccessibilityDelegate());
}
@@ -261,7 +261,7 @@ public class ReactScrollView extends ScrollView
public void setOverflow(@Nullable String overflow) {
mOverflow = overflow;
invalidate();
mReactBackgroundManager.setOverflow(overflow == null ? ViewProps.SCROLL : overflow);
}
public void setMaintainVisibleContentPosition(
@@ -640,15 +640,16 @@ public class ReactScrollView extends ScrollView
mEndBackground.draw(canvas);
}
}
getDrawingRect(mRect);
if (!ViewProps.VISIBLE.equals(mOverflow)) {
canvas.clipRect(mRect);
}
super.draw(canvas);
}
@Override
public void onDraw(Canvas canvas) {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
super.onDraw(canvas);
}
/**
* This handles any sort of scrolling that may occur after a touch is finished. This may be
* momentum scrolling (fling) or because you have pagingEnabled on the scroll view. Because we
@@ -382,6 +382,8 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
setText(getSpanned());
}
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
super.onDraw(canvas);
}
@@ -741,4 +743,8 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
super.setLetterSpacing(mLetterSpacing);
}
}
public void setOverflow(@Nullable String overflow) {
mReactBackgroundManager.setOverflow(overflow);
}
}
@@ -23,6 +23,7 @@ import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.StateWrapper;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.internal.span.ReactClickableSpan;
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
import com.facebook.yoga.YogaMeasureMode;
@@ -220,4 +221,9 @@ public class ReactTextViewManager
public void setPadding(ReactTextView view, int left, int top, int right, int bottom) {
view.setPadding(left, top, right, bottom);
}
@ReactProp(name = "overflow")
public void setOverflow(ReactTextView view, @Nullable String overflow) {
view.setOverflow(overflow);
}
}
@@ -10,6 +10,7 @@ package com.facebook.react.views.textinput;
import static com.facebook.react.uimanager.UIManagerHelper.getReactContext;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
@@ -1226,6 +1227,16 @@ public class ReactEditText extends AppCompatEditText {
mEventDispatcher = eventDispatcher;
}
public void setOverflow(@Nullable String overflow) {
mReactBackgroundManager.setOverflow(overflow);
}
@Override
public void onDraw(Canvas canvas) {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
super.onDraw(canvas);
}
/**
* This class will redirect *TextChanged calls to the listeners only in the case where the text is
* changed by the user, and not explicitly set by JS.
@@ -1038,6 +1038,11 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent);
}
@ReactProp(name = "overflow")
public void setOverflow(ReactEditText view, @Nullable String overflow) {
view.setOverflow(overflow);
}
@Override
protected void onAfterUpdateTransaction(ReactEditText view) {
super.onAfterUpdateTransaction(view);
@@ -7,19 +7,30 @@
package com.facebook.react.views.view;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable;
/** Class that manages the background for views and borders. */
public class ReactViewBackgroundManager {
private static enum Overflow {
VISIBLE,
HIDDEN,
SCROLL,
}
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable;
private View mView;
private int mColor = Color.TRANSPARENT;
private Overflow mOverflow = Overflow.VISIBLE;
public ReactViewBackgroundManager(View view) {
mView = view;
@@ -28,29 +39,29 @@ public class ReactViewBackgroundManager {
public void cleanup() {
ViewCompat.setBackground(mView, null);
mView = null;
mReactBackgroundDrawable = null;
mCSSBackgroundDrawable = null;
}
private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
if (mReactBackgroundDrawable == null) {
mReactBackgroundDrawable = new ReactViewBackgroundDrawable(mView.getContext());
private CSSBackgroundDrawable getOrCreateReactViewBackground() {
if (mCSSBackgroundDrawable == null) {
mCSSBackgroundDrawable = new CSSBackgroundDrawable(mView.getContext());
Drawable backgroundDrawable = mView.getBackground();
ViewCompat.setBackground(
mView, null); // required so that drawable callback is cleared before we add the
// drawable back as a part of LayerDrawable
if (backgroundDrawable == null) {
ViewCompat.setBackground(mView, mReactBackgroundDrawable);
ViewCompat.setBackground(mView, mCSSBackgroundDrawable);
} else {
LayerDrawable layerDrawable =
new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, backgroundDrawable});
new LayerDrawable(new Drawable[] {mCSSBackgroundDrawable, backgroundDrawable});
ViewCompat.setBackground(mView, layerDrawable);
}
}
return mReactBackgroundDrawable;
return mCSSBackgroundDrawable;
}
public void setBackgroundColor(int color) {
if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) {
if (color == Color.TRANSPARENT && mCSSBackgroundDrawable == null) {
// don't do anything, no need to allocate ReactBackgroundDrawable for transparent background
} else {
getOrCreateReactViewBackground().setColor(color);
@@ -84,4 +95,50 @@ public class ReactViewBackgroundManager {
public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}
public void setOverflow(@Nullable String overflow) {
Overflow lastOverflow = mOverflow;
if ("hidden".equals(overflow)) {
mOverflow = Overflow.HIDDEN;
} else if ("scroll".equals(overflow)) {
mOverflow = Overflow.SCROLL;
} else {
mOverflow = Overflow.VISIBLE;
}
if (lastOverflow != mOverflow) {
mView.invalidate();
}
}
/**
* Sets the canvas clipping region to exclude any area below or outide of borders if "overflow" is
* set to clip to padding box.
*/
public void maybeClipToPaddingBox(Canvas canvas) {
if (mOverflow == Overflow.VISIBLE) {
return;
}
// The canvas may be scrolled, so we need to offset
Rect drawingRect = new Rect();
mView.getDrawingRect(drawingRect);
@Nullable CSSBackgroundDrawable cssBackground = mCSSBackgroundDrawable;
if (cssBackground == null) {
canvas.clipRect(drawingRect);
return;
}
@Nullable Path paddingBoxPath = cssBackground.getPaddingBoxPath();
if (paddingBoxPath != null) {
paddingBoxPath.offset(drawingRect.left, drawingRect.top);
canvas.clipPath(paddingBoxPath);
} else {
RectF paddingBoxRect = cssBackground.getPaddingBoxRect();
paddingBoxRect.offset(drawingRect.left, drawingRect.top);
canvas.clipRect(paddingBoxRect);
}
}
}