From bfb3b7008d3e83400154b0348e7ab680ee06aa8c Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 11 Jun 2024 16:13:28 -0700 Subject: [PATCH] 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 --- .../ReactAndroid/api/ReactAndroid.api | 10 ++- .../scroll/ReactHorizontalScrollView.java | 14 +--- .../react/views/scroll/ReactScrollView.java | 15 ++-- .../react/views/text/ReactTextView.java | 6 ++ .../views/text/ReactTextViewManager.java | 6 ++ .../react/views/textinput/ReactEditText.java | 11 +++ .../textinput/ReactTextInputManager.java | 5 ++ .../view/ReactViewBackgroundManager.java | 75 ++++++++++++++++--- 8 files changed, 115 insertions(+), 27 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index b4d2f96a04f..7058755cfc3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 4ffe87f9d30..7d8853ba446 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -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); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index e6698e3cbd2..56f486a23c6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -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 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index e0ae77db3c3..424160192fc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -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); + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index fe4bc770ff8..8ca3155b31a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -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); + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 2a09434f269..014e0b489dd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -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. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index c5c97898ede..2c225da1554 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -1038,6 +1038,11 @@ public class ReactTextInputManager extends BaseViewManager