Skip to content

Commit

Permalink
Support clipping to children everywhere using ReactViewBackgroundMana…
Browse files Browse the repository at this point in the history
…ger (#44734)

Summary:
Pull Request resolved: #44734

Fixes #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
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Jun 11, 2024
1 parent c9d7774 commit bfb3b70
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 27 deletions.
10 changes: 9 additions & 1 deletion packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -145,6 +144,7 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe

setOnHierarchyChangeListener(this);
setClipChildren(false);
mReactBackgroundManager.setOverflow(ViewProps.SCROLL);
}

public boolean getScrollEnabled() {
Expand Down Expand Up @@ -273,7 +273,7 @@ public void flashScrollIndicators() {

public void setOverflow(@Nullable String overflow) {
mOverflow = overflow;
invalidate();
mReactBackgroundManager.setOverflow(overflow == null ? ViewProps.SCROLL : overflow);
}

public void setMaintainVisibleContentPosition(
Expand Down Expand Up @@ -306,14 +306,8 @@ public Rect getOverflowInset() {
}

@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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -136,6 +135,7 @@ public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {
setOnHierarchyChangeListener(this);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
setClipChildren(false);
mReactBackgroundManager.setOverflow(ViewProps.SCROLL);

ViewCompat.setAccessibilityDelegate(this, new ReactScrollViewAccessibilityDelegate());
}
Expand Down Expand Up @@ -261,7 +261,7 @@ public void flashScrollIndicators() {

public void setOverflow(@Nullable String overflow) {
mOverflow = overflow;
invalidate();
mReactBackgroundManager.setOverflow(overflow == null ? ViewProps.SCROLL : overflow);
}

public void setMaintainVisibleContentPosition(
Expand Down Expand Up @@ -640,15 +640,16 @@ public void draw(Canvas canvas) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ protected void onDraw(Canvas canvas) {
setText(getSpanned());
}

mReactBackgroundManager.maybeClipToPaddingBox(canvas);

super.onDraw(canvas);
}

Expand Down Expand Up @@ -741,4 +743,8 @@ private void applyTextAttributes() {
super.setLetterSpacing(mLetterSpacing);
}
}

public void setOverflow(@Nullable String overflow) {
mReactBackgroundManager.setOverflow(overflow);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -220,4 +221,9 @@ public long measure(
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand Down Expand Up @@ -1226,6 +1227,16 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,11 @@ public void setBorderColor(ReactEditText view, int index, Integer color) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,29 +39,29 @@ public ReactViewBackgroundManager(View view) {
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);
Expand Down Expand Up @@ -84,4 +95,50 @@ public void setBorderRadius(float borderRadius, int position) {
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);
}
}
}

0 comments on commit bfb3b70

Please sign in to comment.