mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Back out "Redo "[RN][Android] Convert ReactViewGroup to Kotlin""
Summary: Backing out the stack since a same crash that previously effected many apps appeared again, and there are changes soon landing that will add more conflicts. Reviewed By: Abbondanzo Differential Revision: D63493331 fbshipit-source-id: 44658ffc99eb4ebc947f95bb6e6bde105ac88c93
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a886a2fa05
commit
8d22e3b249
@@ -8318,6 +8318,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro
|
||||
public fun draw (Landroid/graphics/Canvas;)V
|
||||
protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z
|
||||
protected fun getChildDrawingOrder (II)I
|
||||
public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z
|
||||
public fun getClippingRect (Landroid/graphics/Rect;)V
|
||||
public fun getHitSlopRect ()Landroid/graphics/Rect;
|
||||
public fun getOverflow ()Ljava/lang/String;
|
||||
|
||||
+8
-8
@@ -432,21 +432,21 @@ public class ReactModalHostView(context: ThemedReactContext) :
|
||||
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { eventDispatcher ->
|
||||
jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext)
|
||||
jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, true)
|
||||
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
||||
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
||||
}
|
||||
return super.onInterceptTouchEvent(ev)
|
||||
return super.onInterceptTouchEvent(event)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { eventDispatcher ->
|
||||
jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext)
|
||||
jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, false)
|
||||
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
||||
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
||||
}
|
||||
super.onTouchEvent(ev)
|
||||
super.onTouchEvent(event)
|
||||
// In case when there is no children interested in handling touch event, we return true from
|
||||
// the root view in order to receive subsequent events related to that gesture
|
||||
return true
|
||||
|
||||
+966
@@ -0,0 +1,966 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.view;
|
||||
|
||||
import static com.facebook.infer.annotation.Assertions.nullsafeFIXME;
|
||||
import static com.facebook.react.common.ReactConstants.TAG;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.BlendMode;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStructure;
|
||||
import android.view.animation.Animation;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.ReactNoCrashSoftException;
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
|
||||
import com.facebook.react.touch.OnInterceptTouchEventListener;
|
||||
import com.facebook.react.touch.ReactHitSlopView;
|
||||
import com.facebook.react.touch.ReactInterceptingViewGroup;
|
||||
import com.facebook.react.uimanager.BackgroundStyleApplicator;
|
||||
import com.facebook.react.uimanager.LengthPercentage;
|
||||
import com.facebook.react.uimanager.LengthPercentageType;
|
||||
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.PointerEvents;
|
||||
import com.facebook.react.uimanager.ReactClippingProhibitedView;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroup;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||
import com.facebook.react.uimanager.ReactOverflowViewWithInset;
|
||||
import com.facebook.react.uimanager.ReactPointerEventsView;
|
||||
import com.facebook.react.uimanager.ReactZIndexedViewGroup;
|
||||
import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable;
|
||||
import com.facebook.react.uimanager.style.BorderRadiusProp;
|
||||
import com.facebook.react.uimanager.style.BorderStyle;
|
||||
import com.facebook.react.uimanager.style.LogicalEdge;
|
||||
import com.facebook.react.uimanager.style.Overflow;
|
||||
|
||||
/**
|
||||
* Backing for a React View. Has support for borders, but since borders aren't common, lazy
|
||||
* initializes most of the storage needed for them.
|
||||
*/
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class ReactViewGroup extends ViewGroup
|
||||
implements ReactInterceptingViewGroup,
|
||||
ReactClippingViewGroup,
|
||||
ReactPointerEventsView,
|
||||
ReactHitSlopView,
|
||||
ReactZIndexedViewGroup,
|
||||
ReactOverflowViewWithInset {
|
||||
|
||||
private static final int ARRAY_CAPACITY_INCREMENT = 12;
|
||||
private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
|
||||
private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0);
|
||||
private final Rect mOverflowInset = new Rect();
|
||||
/* should only be used in {@link #updateClippingToRect} */
|
||||
private static final Rect sHelperRect = new Rect();
|
||||
|
||||
/**
|
||||
* This listener will be set for child views when removeClippedSubview property is enabled. When
|
||||
* children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent view
|
||||
* about that fact so that view can be attached/detached if necessary.
|
||||
*
|
||||
* <p>TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children
|
||||
* update their layout.
|
||||
*/
|
||||
private static final class ChildrenLayoutChangeListener implements View.OnLayoutChangeListener {
|
||||
|
||||
private final ReactViewGroup mParent;
|
||||
|
||||
private ChildrenLayoutChangeListener(ReactViewGroup parent) {
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChange(
|
||||
View v,
|
||||
int left,
|
||||
int top,
|
||||
int right,
|
||||
int bottom,
|
||||
int oldLeft,
|
||||
int oldTop,
|
||||
int oldRight,
|
||||
int oldBottom) {
|
||||
if (mParent.getRemoveClippedSubviews()) {
|
||||
mParent.updateSubviewClipStatus(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Following properties are here to support the option {@code removeClippedSubviews}. This is a
|
||||
// temporary optimization/hack that is mainly applicable to the large list of images. The way
|
||||
// it's implemented is that we store an additional array of children in view node. We selectively
|
||||
// remove some of the views (detach) from it while still storing them in that additional array.
|
||||
// We override all possible add methods for {@link ViewGroup} so that we can control this process
|
||||
// whenever the option is set. We also override {@link ViewGroup#getChildAt} and
|
||||
// {@link ViewGroup#getChildCount} so those methods may return views that are not attached.
|
||||
// This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}.
|
||||
private boolean mRemoveClippedSubviews;
|
||||
private @Nullable View[] mAllChildren;
|
||||
private int mAllChildrenCount;
|
||||
private @Nullable Rect mClippingRect;
|
||||
private @Nullable Rect mHitSlopRect;
|
||||
private Overflow mOverflow;
|
||||
private PointerEvents mPointerEvents;
|
||||
private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
|
||||
private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable;
|
||||
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
|
||||
private boolean mNeedsOffscreenAlphaCompositing;
|
||||
private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper;
|
||||
private @Nullable Path mPath;
|
||||
private float mBackfaceOpacity;
|
||||
private String mBackfaceVisibility;
|
||||
|
||||
public ReactViewGroup(Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all default values here as opposed to in the constructor or field defaults. It is important
|
||||
* that these properties are set during the constructor, but also on-demand whenever an existing
|
||||
* ReactTextView is recycled.
|
||||
*/
|
||||
private void initView() {
|
||||
setClipChildren(false);
|
||||
|
||||
mRemoveClippedSubviews = false;
|
||||
mAllChildren = null;
|
||||
mAllChildrenCount = 0;
|
||||
mClippingRect = null;
|
||||
mHitSlopRect = null;
|
||||
mOverflow = Overflow.VISIBLE;
|
||||
mPointerEvents = PointerEvents.AUTO;
|
||||
mChildrenLayoutChangeListener = null;
|
||||
mCSSBackgroundDrawable = null;
|
||||
mOnInterceptTouchEventListener = null;
|
||||
mNeedsOffscreenAlphaCompositing = false;
|
||||
mDrawingOrderHelper = null;
|
||||
mPath = null;
|
||||
mBackfaceOpacity = 1.f;
|
||||
mBackfaceVisibility = "visible";
|
||||
}
|
||||
|
||||
/* package */ void recycleView() {
|
||||
// Remove dangling listeners
|
||||
if (mAllChildren != null && mChildrenLayoutChangeListener != null) {
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
// Set default field values
|
||||
initView();
|
||||
mOverflowInset.setEmpty();
|
||||
sHelperRect.setEmpty();
|
||||
|
||||
// Remove any children
|
||||
removeAllViews();
|
||||
|
||||
// Reset background, borders
|
||||
updateBackgroundDrawable(null);
|
||||
|
||||
resetPointerEvents();
|
||||
}
|
||||
|
||||
private ViewGroupDrawingOrderHelper getDrawingOrderHelper() {
|
||||
if (mDrawingOrderHelper == null) {
|
||||
mDrawingOrderHelper = new ViewGroupDrawingOrderHelper(this);
|
||||
}
|
||||
return mDrawingOrderHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
setMeasuredDimension(
|
||||
MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
// No-op since UIManagerModule handles actually laying out children.
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall")
|
||||
public void requestLayout() {
|
||||
// No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and
|
||||
// `layout` is called on all RN-managed views by `NativeViewHierarchyManager`
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
@Override
|
||||
public void dispatchProvideStructure(ViewStructure structure) {
|
||||
try {
|
||||
super.dispatchProvideStructure(structure);
|
||||
} catch (NullPointerException e) {
|
||||
FLog.e(TAG, "NullPointerException when executing dispatchProvideStructure", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int color) {
|
||||
BackgroundStyleApplicator.setBackgroundColor(this, color);
|
||||
}
|
||||
|
||||
@Deprecated(since = "0.76.0", forRemoval = true)
|
||||
public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
|
||||
BackgroundStyleApplicator.setFeedbackUnderlay(this, background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) {
|
||||
mOnInterceptTouchEventListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (mOnInterceptTouchEventListener != null
|
||||
&& mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
|
||||
return true;
|
||||
}
|
||||
// We intercept the touch event if the children are not supposed to receive it.
|
||||
if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) {
|
||||
return true;
|
||||
}
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
// We do not accept the touch event if this view is not supposed to receive it.
|
||||
if (!PointerEvents.canBeTouchTarget(mPointerEvents)) {
|
||||
return false;
|
||||
}
|
||||
// The root view always assumes any view that was tapped wants the touch
|
||||
// and sends the event to JS as such.
|
||||
// We don't need to do bubbling in native (it's already happening in JS).
|
||||
// For an explanation of bubbling and capturing, see
|
||||
// http://javascript.info/tutorial/bubbling-and-capturing#capturing
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onHoverEvent(MotionEvent event) {
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
// Match the logic from onTouchEvent if pointer events are enabled
|
||||
return PointerEvents.canBeTouchTarget(mPointerEvents);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
|
||||
// We do not dispatch the motion event if its children are not supposed to receive it
|
||||
if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.dispatchGenericMotionEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* We override this to allow developers to determine whether they need offscreen alpha compositing
|
||||
* or not. See the documentation of needsOffscreenAlphaCompositing in View.js.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasOverlappingRendering() {
|
||||
return mNeedsOffscreenAlphaCompositing;
|
||||
}
|
||||
|
||||
/** See the documentation of needsOffscreenAlphaCompositing in View.js. */
|
||||
public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) {
|
||||
mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing;
|
||||
}
|
||||
|
||||
public void setBorderWidth(int position, float width) {
|
||||
BackgroundStyleApplicator.setBorderWidth(
|
||||
this, LogicalEdge.values()[position], PixelUtil.toDIPFromPixel(width));
|
||||
}
|
||||
|
||||
public void setBorderColor(int position, @Nullable Integer color) {
|
||||
BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.values()[position], color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead.
|
||||
*/
|
||||
@Deprecated(since = "0.75.0", forRemoval = true)
|
||||
public void setBorderRadius(float borderRadius) {
|
||||
setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead.
|
||||
*/
|
||||
@Deprecated(since = "0.75.0", forRemoval = true)
|
||||
public void setBorderRadius(float borderRadius, int position) {
|
||||
@Nullable
|
||||
LengthPercentage radius =
|
||||
Float.isNaN(borderRadius)
|
||||
? null
|
||||
: new LengthPercentage(borderRadius, LengthPercentageType.POINT);
|
||||
BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.values()[position], radius);
|
||||
}
|
||||
|
||||
public void setBorderRadius(BorderRadiusProp property, @Nullable LengthPercentage borderRadius) {
|
||||
BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius);
|
||||
}
|
||||
|
||||
public void setBorderStyle(@Nullable String style) {
|
||||
BackgroundStyleApplicator.setBorderStyle(
|
||||
this, style == null ? null : BorderStyle.fromString(style));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
|
||||
if (removeClippedSubviews == mRemoveClippedSubviews) {
|
||||
return;
|
||||
}
|
||||
mRemoveClippedSubviews = removeClippedSubviews;
|
||||
if (removeClippedSubviews) {
|
||||
mClippingRect = new Rect();
|
||||
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
|
||||
mAllChildrenCount = getChildCount();
|
||||
int initialSize = Math.max(12, mAllChildrenCount);
|
||||
mAllChildren = new View[initialSize];
|
||||
mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this);
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
View child = getChildAt(i);
|
||||
mAllChildren[i] = child;
|
||||
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
}
|
||||
updateClippingRect();
|
||||
} else {
|
||||
// Add all clipped views back, deallocate additional arrays, remove layoutChangeListener
|
||||
Assertions.assertNotNull(mClippingRect);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
Assertions.assertNotNull(mChildrenLayoutChangeListener);
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
}
|
||||
getDrawingRect(mClippingRect);
|
||||
updateClippingToRect(mClippingRect);
|
||||
mAllChildren = null;
|
||||
mClippingRect = null;
|
||||
mAllChildrenCount = 0;
|
||||
mChildrenLayoutChangeListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRemoveClippedSubviews() {
|
||||
return mRemoveClippedSubviews;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getClippingRect(Rect outClippingRect) {
|
||||
outClippingRect.set(nullsafeFIXME(mClippingRect, "Fix in Kotlin"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateClippingRect() {
|
||||
if (!mRemoveClippedSubviews) {
|
||||
return;
|
||||
}
|
||||
|
||||
Assertions.assertNotNull(mClippingRect);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
|
||||
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
|
||||
updateClippingToRect(mClippingRect);
|
||||
}
|
||||
|
||||
private void updateClippingToRect(Rect clippingRect) {
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
int clippedSoFar = 0;
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
updateSubviewClipStatus(clippingRect, i, clippedSoFar);
|
||||
if (mAllChildren[i].getParent() == null) {
|
||||
clippedSoFar++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
View child = Assertions.assertNotNull(mAllChildren)[idx];
|
||||
sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
boolean intersects =
|
||||
clippingRect.intersects(
|
||||
sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
|
||||
boolean needUpdateClippingRecursive = false;
|
||||
// We never want to clip children that are being animated, as this can easily break layout :
|
||||
// when layout animation changes size and/or position of views contained inside a listview that
|
||||
// clips offscreen children, we need to ensure that, when view exits the viewport, final size
|
||||
// and position is set prior to removing the view from its listview parent.
|
||||
// Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll,
|
||||
// it won't be size and located properly.
|
||||
Animation animation = child.getAnimation();
|
||||
boolean isAnimating = animation != null && !animation.hasEnded();
|
||||
if (!intersects && child.getParent() != null && !isAnimating) {
|
||||
// We can try saving on invalidate call here as the view that we remove is out of visible area
|
||||
// therefore invalidation is not necessary.
|
||||
removeViewInLayout(child);
|
||||
needUpdateClippingRecursive = true;
|
||||
} else if (intersects && child.getParent() == null) {
|
||||
addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true);
|
||||
invalidate();
|
||||
needUpdateClippingRecursive = true;
|
||||
} else if (intersects) {
|
||||
// If there is any intersection we need to inform the child to update its clipping rect
|
||||
needUpdateClippingRecursive = true;
|
||||
}
|
||||
if (needUpdateClippingRecursive) {
|
||||
if (child instanceof ReactClippingViewGroup) {
|
||||
// we don't use {@link sHelperRect} until the end of this loop, therefore it's safe
|
||||
// to call this method that may write to the same {@link sHelperRect} object.
|
||||
ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child;
|
||||
if (clippingChild.getRemoveClippedSubviews()) {
|
||||
clippingChild.updateClippingRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSubviewClipStatus(View subview) {
|
||||
if (!mRemoveClippedSubviews || getParent() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Assertions.assertNotNull(mClippingRect);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
|
||||
// do fast check whether intersect state changed
|
||||
sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom());
|
||||
boolean intersects =
|
||||
mClippingRect.intersects(
|
||||
sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom);
|
||||
|
||||
// If it was intersecting before, should be attached to the parent
|
||||
boolean oldIntersects = (subview.getParent() != null);
|
||||
|
||||
if (intersects != oldIntersects) {
|
||||
int clippedSoFar = 0;
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
if (mAllChildren[i] == subview) {
|
||||
updateSubviewClipStatus(mClippingRect, i, clippedSoFar);
|
||||
break;
|
||||
}
|
||||
if (mAllChildren[i].getParent() == null) {
|
||||
clippedSoFar++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
|
||||
return super.getChildVisibleRect(child, r, offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
if (mRemoveClippedSubviews) {
|
||||
updateClippingRect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mRemoveClippedSubviews) {
|
||||
updateClippingRect();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean customDrawOrderDisabled() {
|
||||
if (getId() == NO_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom draw order is disabled for Fabric.
|
||||
return ViewUtil.getUIManagerType(getId()) == UIManagerType.FABRIC;
|
||||
}
|
||||
|
||||
private void handleAddView(View view) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (!customDrawOrderDisabled()) {
|
||||
getDrawingOrderHelper().handleAddView(view);
|
||||
setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder());
|
||||
} else {
|
||||
setChildrenDrawingOrderEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoveView(@Nullable View view) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (!customDrawOrderDisabled()) {
|
||||
if (indexOfChild(view) == -1) {
|
||||
return;
|
||||
}
|
||||
getDrawingOrderHelper().handleRemoveView(view);
|
||||
setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder());
|
||||
} else {
|
||||
setChildrenDrawingOrderEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoveViews(int start, int count) {
|
||||
int endIndex = start + count;
|
||||
for (int index = start; index < endIndex; index++) {
|
||||
if (index < getChildCount()) {
|
||||
handleRemoveView(getChildAt(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, @Nullable ViewGroup.LayoutParams params) {
|
||||
// This will get called for every overload of addView so there is not need to override every
|
||||
// method.
|
||||
handleAddView(child);
|
||||
super.addView(child, index, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean addViewInLayout(
|
||||
View child, int index, LayoutParams params, boolean preventRequestLayout) {
|
||||
handleAddView(child);
|
||||
return super.addViewInLayout(child, index, params, preventRequestLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeView(@Nullable View view) {
|
||||
handleRemoveView(view);
|
||||
super.removeView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewAt(int index) {
|
||||
handleRemoveView(getChildAt(index));
|
||||
super.removeViewAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewInLayout(View view) {
|
||||
handleRemoveView(view);
|
||||
super.removeViewInLayout(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewsInLayout(int start, int count) {
|
||||
handleRemoveViews(start, count);
|
||||
super.removeViewsInLayout(start, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViews(int start, int count) {
|
||||
handleRemoveViews(start, count);
|
||||
super.removeViews(start, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getChildDrawingOrder(int childCount, int index) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (!customDrawOrderDisabled()) {
|
||||
return getDrawingOrderHelper().getChildDrawingOrder(childCount, index);
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getZIndexMappedChildIndex(int index) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (!customDrawOrderDisabled() && getDrawingOrderHelper().shouldEnableCustomDrawingOrder()) {
|
||||
return getDrawingOrderHelper().getChildDrawingOrder(getChildCount(), index);
|
||||
}
|
||||
|
||||
// Fabric behavior
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawingOrder() {
|
||||
if (customDrawOrderDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getDrawingOrderHelper().update();
|
||||
setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder());
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointerEvents getPointerEvents() {
|
||||
return mPointerEvents;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchSetPressed(boolean pressed) {
|
||||
// Prevents the ViewGroup from dispatching the pressed state
|
||||
// to it's children.
|
||||
}
|
||||
|
||||
public void setPointerEvents(PointerEvents pointerEvents) {
|
||||
mPointerEvents = pointerEvents;
|
||||
}
|
||||
|
||||
/*package*/ void resetPointerEvents() {
|
||||
mPointerEvents = PointerEvents.AUTO;
|
||||
}
|
||||
|
||||
/*package*/ int getAllChildrenCount() {
|
||||
return mAllChildrenCount;
|
||||
}
|
||||
|
||||
/*package*/ @Nullable
|
||||
View getChildAtWithSubviewClippingEnabled(int index) {
|
||||
return index >= 0 && index < mAllChildrenCount
|
||||
? Assertions.assertNotNull(mAllChildren)[index]
|
||||
: null;
|
||||
}
|
||||
|
||||
/*package*/ void addViewWithSubviewClippingEnabled(View child, int index) {
|
||||
addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam);
|
||||
}
|
||||
|
||||
/*package*/ void addViewWithSubviewClippingEnabled(
|
||||
final View child, int index, ViewGroup.LayoutParams params) {
|
||||
Assertions.assertCondition(mRemoveClippedSubviews);
|
||||
Assertions.assertNotNull(mClippingRect);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
addInArray(child, index);
|
||||
// we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally
|
||||
// attach it
|
||||
int clippedSoFar = 0;
|
||||
for (int i = 0; i < index; i++) {
|
||||
if (mAllChildren[i].getParent() == null) {
|
||||
clippedSoFar++;
|
||||
}
|
||||
}
|
||||
updateSubviewClipStatus(mClippingRect, index, clippedSoFar);
|
||||
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
|
||||
if (child instanceof ReactClippingProhibitedView) {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!child.isShown()) {
|
||||
ReactSoftExceptionLogger.logSoftException(
|
||||
TAG,
|
||||
new ReactNoCrashSoftException(
|
||||
"Child view has been added to Parent view in which it is clipped and not"
|
||||
+ " visible. This is not legal for this particular child view. Child: ["
|
||||
+ child.getId()
|
||||
+ "] "
|
||||
+ child.toString()
|
||||
+ " Parent: ["
|
||||
+ getId()
|
||||
+ "] "
|
||||
+ toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ void removeViewWithSubviewClippingEnabled(View view) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
Assertions.assertCondition(mRemoveClippedSubviews);
|
||||
Assertions.assertNotNull(mClippingRect);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
int index = indexOfChildInAllChildren(view);
|
||||
if (mAllChildren[index].getParent() != null) {
|
||||
int clippedSoFar = 0;
|
||||
for (int i = 0; i < index; i++) {
|
||||
if (mAllChildren[i].getParent() == null) {
|
||||
clippedSoFar++;
|
||||
}
|
||||
}
|
||||
removeViewsInLayout(index - clippedSoFar, 1);
|
||||
}
|
||||
removeFromArray(index);
|
||||
}
|
||||
|
||||
/*package*/ void removeAllViewsWithSubviewClippingEnabled() {
|
||||
Assertions.assertCondition(mRemoveClippedSubviews);
|
||||
Assertions.assertNotNull(mAllChildren);
|
||||
for (int i = 0; i < mAllChildrenCount; i++) {
|
||||
mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
|
||||
}
|
||||
removeAllViewsInLayout();
|
||||
mAllChildrenCount = 0;
|
||||
}
|
||||
|
||||
private int indexOfChildInAllChildren(View child) {
|
||||
final int count = mAllChildrenCount;
|
||||
final View[] children = Assertions.assertNotNull(mAllChildren);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (children[i] == child) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void addInArray(View child, int index) {
|
||||
View[] children = Assertions.assertNotNull(mAllChildren);
|
||||
final int count = mAllChildrenCount;
|
||||
final int size = children.length;
|
||||
if (index == count) {
|
||||
if (size == count) {
|
||||
mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
|
||||
System.arraycopy(children, 0, mAllChildren, 0, size);
|
||||
children = mAllChildren;
|
||||
}
|
||||
children[mAllChildrenCount++] = child;
|
||||
} else if (index < count) {
|
||||
if (size == count) {
|
||||
mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
|
||||
System.arraycopy(children, 0, mAllChildren, 0, index);
|
||||
System.arraycopy(children, index, mAllChildren, index + 1, count - index);
|
||||
children = mAllChildren;
|
||||
} else {
|
||||
System.arraycopy(children, index, children, index + 1, count - index);
|
||||
}
|
||||
children[index] = child;
|
||||
mAllChildrenCount++;
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFromArray(int index) {
|
||||
final View[] children = Assertions.assertNotNull(mAllChildren);
|
||||
final int count = mAllChildrenCount;
|
||||
if (index == count - 1) {
|
||||
children[--mAllChildrenCount] = null;
|
||||
} else if (index >= 0 && index < count) {
|
||||
System.arraycopy(children, index + 1, children, index, count - index - 1);
|
||||
children[--mAllChildrenCount] = null;
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needsIsolatedLayer() {
|
||||
if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
if (getChildAt(i).getTag(R.id.mix_blend_mode) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public int getBackgroundColor() {
|
||||
@Nullable Integer color = BackgroundStyleApplicator.getBackgroundColor(this);
|
||||
return color == null ? DEFAULT_BACKGROUND_COLOR : color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Rect getHitSlopRect() {
|
||||
return mHitSlopRect;
|
||||
}
|
||||
|
||||
public void setHitSlopRect(@Nullable Rect rect) {
|
||||
mHitSlopRect = rect;
|
||||
}
|
||||
|
||||
public void setOverflow(@Nullable String overflow) {
|
||||
if (overflow == null) {
|
||||
mOverflow = Overflow.VISIBLE;
|
||||
} else {
|
||||
@Nullable Overflow parsedOverflow = Overflow.fromString(overflow);
|
||||
mOverflow = parsedOverflow == null ? Overflow.VISIBLE : parsedOverflow;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getOverflow() {
|
||||
switch (mOverflow) {
|
||||
case HIDDEN:
|
||||
return "hidden";
|
||||
case SCROLL:
|
||||
return "scroll";
|
||||
case VISIBLE:
|
||||
return "visible";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOverflowInset(int left, int top, int right, int bottom) {
|
||||
if (needsIsolatedLayer()
|
||||
&& (mOverflowInset.left != left
|
||||
|| mOverflowInset.top != top
|
||||
|| mOverflowInset.right != right
|
||||
|| mOverflowInset.bottom != bottom)) {
|
||||
invalidate();
|
||||
}
|
||||
mOverflowInset.set(left, top, right, bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rect getOverflowInset() {
|
||||
return mOverflowInset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the background for the view or remove the background. It calls {@link
|
||||
* #setBackground(Drawable)} or {@link #setBackgroundDrawable(Drawable)} based on the sdk version.
|
||||
*
|
||||
* @param drawable {@link Drawable} The Drawable to use as the background, or null to remove the
|
||||
* background
|
||||
*/
|
||||
/* package */ void updateBackgroundDrawable(@Nullable Drawable drawable) {
|
||||
super.setBackground(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
&& ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC
|
||||
&& needsIsolatedLayer()) {
|
||||
|
||||
// Check if the view is a stacking context and has children, if it does, do the rendering
|
||||
// offscreen and then composite back. This follows the idea of group isolation on blending
|
||||
// https://www.w3.org/TR/compositing-1/#isolationblending
|
||||
Rect overflowInset = getOverflowInset();
|
||||
canvas.saveLayer(
|
||||
overflowInset.left,
|
||||
overflowInset.top,
|
||||
getWidth() + -overflowInset.right,
|
||||
getHeight() + -overflowInset.bottom,
|
||||
null);
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
} else {
|
||||
super.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
if (mOverflow != Overflow.VISIBLE || getTag(R.id.filter) != null) {
|
||||
BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
|
||||
}
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
|
||||
boolean drawWithZ = child.getElevation() > 0;
|
||||
|
||||
if (drawWithZ) {
|
||||
CanvasUtil.enableZ(canvas, true);
|
||||
}
|
||||
|
||||
BlendMode mixBlendMode = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) {
|
||||
mixBlendMode = (BlendMode) child.getTag(R.id.mix_blend_mode);
|
||||
if (mixBlendMode != null) {
|
||||
Paint p = new Paint();
|
||||
p.setBlendMode(mixBlendMode);
|
||||
Rect overflowInset = getOverflowInset();
|
||||
canvas.saveLayer(
|
||||
overflowInset.left,
|
||||
overflowInset.top,
|
||||
getWidth() + -overflowInset.right,
|
||||
getHeight() + -overflowInset.bottom,
|
||||
p);
|
||||
}
|
||||
}
|
||||
|
||||
boolean result = super.drawChild(canvas, child, drawingTime);
|
||||
|
||||
if (mixBlendMode != null) {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
if (drawWithZ) {
|
||||
CanvasUtil.enableZ(canvas, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setOpacityIfPossible(float opacity) {
|
||||
mBackfaceOpacity = opacity;
|
||||
setBackfaceVisibilityDependantOpacity();
|
||||
}
|
||||
|
||||
public void setBackfaceVisibility(String backfaceVisibility) {
|
||||
mBackfaceVisibility = backfaceVisibility;
|
||||
setBackfaceVisibilityDependantOpacity();
|
||||
}
|
||||
|
||||
public void setBackfaceVisibilityDependantOpacity() {
|
||||
boolean isBackfaceVisible = mBackfaceVisibility.equals("visible");
|
||||
|
||||
if (isBackfaceVisible) {
|
||||
setAlpha(mBackfaceOpacity);
|
||||
return;
|
||||
}
|
||||
|
||||
float rotationX = getRotationX();
|
||||
float rotationY = getRotationY();
|
||||
|
||||
boolean isFrontfaceVisible =
|
||||
(rotationX >= -90.f && rotationX < 90.f) && (rotationY >= -90.f && rotationY < 90.f);
|
||||
|
||||
if (isFrontfaceVisible) {
|
||||
setAlpha(mBackfaceOpacity);
|
||||
return;
|
||||
}
|
||||
|
||||
setAlpha(0);
|
||||
}
|
||||
}
|
||||
-844
@@ -1,844 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION") // ReactFeatureFlags
|
||||
|
||||
package com.facebook.react.views.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewStructure
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.infer.annotation.Assertions
|
||||
import com.facebook.react.R
|
||||
import com.facebook.react.bridge.ReactNoCrashSoftException
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
||||
import com.facebook.react.common.annotations.VisibleForTesting
|
||||
import com.facebook.react.config.ReactFeatureFlags
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
|
||||
import com.facebook.react.touch.OnInterceptTouchEventListener
|
||||
import com.facebook.react.touch.ReactHitSlopView
|
||||
import com.facebook.react.touch.ReactInterceptingViewGroup
|
||||
import com.facebook.react.uimanager.BackgroundStyleApplicator
|
||||
import com.facebook.react.uimanager.LengthPercentage
|
||||
import com.facebook.react.uimanager.LengthPercentageType
|
||||
import com.facebook.react.uimanager.MeasureSpecAssertions
|
||||
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
||||
import com.facebook.react.uimanager.PointerEvents
|
||||
import com.facebook.react.uimanager.ReactClippingProhibitedView
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroup
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper
|
||||
import com.facebook.react.uimanager.ReactOverflowViewWithInset
|
||||
import com.facebook.react.uimanager.ReactPointerEventsView
|
||||
import com.facebook.react.uimanager.ReactZIndexedViewGroup
|
||||
import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper
|
||||
import com.facebook.react.uimanager.common.UIManagerType
|
||||
import com.facebook.react.uimanager.common.ViewUtil
|
||||
import com.facebook.react.uimanager.style.BorderRadiusProp
|
||||
import com.facebook.react.uimanager.style.BorderStyle
|
||||
import com.facebook.react.uimanager.style.LogicalEdge
|
||||
import com.facebook.react.uimanager.style.Overflow
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Backing for a React View. Has support for borders, but since borders aren't common, lazy
|
||||
* initializes most of the storage needed for them.
|
||||
*/
|
||||
@OptIn(UnstableReactNativeAPI::class)
|
||||
public open class ReactViewGroup(context: Context) :
|
||||
ViewGroup(context),
|
||||
ReactInterceptingViewGroup,
|
||||
ReactClippingViewGroup,
|
||||
ReactPointerEventsView,
|
||||
ReactHitSlopView,
|
||||
ReactZIndexedViewGroup,
|
||||
ReactOverflowViewWithInset {
|
||||
|
||||
private companion object {
|
||||
private const val ARRAY_CAPACITY_INCREMENT = 12
|
||||
private const val DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT
|
||||
private val defaultLayoutParam = LayoutParams(0, 0)
|
||||
}
|
||||
|
||||
private val _overflowInset = Rect()
|
||||
|
||||
/**
|
||||
* This listener will be set for child views when removeClippedSubview property is enabled. When
|
||||
* children layout is updated, it will call [updateSubviewClipStatus] to notify parent view about
|
||||
* that fact so that view can be attached/detached if necessary.
|
||||
*
|
||||
* TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children
|
||||
* update their layout.
|
||||
*/
|
||||
private class ChildrenLayoutChangeListener(private val parent: ReactViewGroup) :
|
||||
OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
v: View,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int
|
||||
) {
|
||||
if (parent.removeClippedSubviews) {
|
||||
parent.updateSubviewClipStatus(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Following properties are here to support the option {@code removeClippedSubviews}. This is a
|
||||
// temporary optimization/hack that is mainly applicable to the large list of images. The way
|
||||
// it's implemented is that we store an additional array of children in view node. We selectively
|
||||
// remove some of the views (detach) from it while still storing them in that additional array.
|
||||
// We override all possible add methods for [ViewGroup] so that we can control this process
|
||||
// whenever the option is set. We also override [ViewGroup#getChildAt] and
|
||||
// [ViewGroup#getChildCount] so those methods may return views that are not attached.
|
||||
// This is risky but allows us to perform a correct cleanup in [NativeViewHierarchyManager].
|
||||
private var _removeClippedSubviews = false
|
||||
|
||||
private var allChildren: Array<View?>? = null
|
||||
internal var allChildrenCount: Int = 0
|
||||
private set
|
||||
|
||||
private var _clippingRect: Rect? = null
|
||||
public override var hitSlopRect: Rect? = null
|
||||
private var _overflow: Overflow = Overflow.VISIBLE
|
||||
private var _pointerEvents: PointerEvents = PointerEvents.AUTO
|
||||
private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null
|
||||
private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
|
||||
private var needsOffscreenAlphaCompositing = false
|
||||
private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null
|
||||
private var backfaceOpacity = 1f
|
||||
private var backfaceVisibility: String? = "visible"
|
||||
|
||||
/**
|
||||
* Set all default values here as opposed to in the constructor or field defaults. It is important
|
||||
* that these properties are set during the constructor, but also on-demand whenever an existing
|
||||
* ReactTextView is recycled.
|
||||
*/
|
||||
private fun initView() {
|
||||
clipChildren = false
|
||||
_removeClippedSubviews = false
|
||||
allChildren = null
|
||||
allChildrenCount = 0
|
||||
_clippingRect = null
|
||||
hitSlopRect = null
|
||||
_overflow = Overflow.VISIBLE
|
||||
resetPointerEvents()
|
||||
childrenLayoutChangeListener = null
|
||||
onInterceptTouchEventListener = null
|
||||
needsOffscreenAlphaCompositing = false
|
||||
_drawingOrderHelper = null
|
||||
backfaceOpacity = 1f
|
||||
backfaceVisibility = "visible"
|
||||
}
|
||||
|
||||
internal open fun recycleView(): Unit {
|
||||
// Remove dangling listeners
|
||||
val children = allChildren
|
||||
val listener = childrenLayoutChangeListener
|
||||
if (children != null && listener != null) {
|
||||
for (i in 0 until allChildrenCount) {
|
||||
children[i]?.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
// Set default field values
|
||||
initView()
|
||||
_overflowInset.setEmpty()
|
||||
|
||||
// Remove any children
|
||||
removeAllViews()
|
||||
|
||||
// Reset background, borders
|
||||
updateBackgroundDrawable(null)
|
||||
resetPointerEvents()
|
||||
}
|
||||
|
||||
private val drawingOrderHelper: ViewGroupDrawingOrderHelper
|
||||
get() {
|
||||
return _drawingOrderHelper
|
||||
?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it }
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec)
|
||||
setMeasuredDimension(
|
||||
MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
|
||||
}
|
||||
|
||||
// No-op since UIManagerModule handles actually laying out children.
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int): Unit = Unit
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
// No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and `layout`
|
||||
// is called on all RN-managed views by `NativeViewHierarchyManager`
|
||||
override fun requestLayout(): Unit = Unit
|
||||
|
||||
override fun dispatchProvideStructure(structure: ViewStructure) {
|
||||
try {
|
||||
super.dispatchProvideStructure(structure)
|
||||
} catch (e: NullPointerException) {
|
||||
FLog.e(ReactConstants.TAG, "NullPointerException when executing dispatchProvideStructure", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackgroundColor(color: Int) {
|
||||
BackgroundStyleApplicator.setBackgroundColor(this, color)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Don't use setTranslucentBackgroundDrawable as it was deprecated in React Native 0.76.0.",
|
||||
ReplaceWith(
|
||||
"BackgroundStyleApplicator.setFeedbackUnderlay(this, background)",
|
||||
"com.facebook.react.uimanager.BackgroundStyleApplicator"))
|
||||
public open fun setTranslucentBackgroundDrawable(background: Drawable?): Unit {
|
||||
BackgroundStyleApplicator.setFeedbackUnderlay(this, background)
|
||||
}
|
||||
|
||||
override fun setOnInterceptTouchEventListener(listener: OnInterceptTouchEventListener) {
|
||||
onInterceptTouchEventListener = listener
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||
if (onInterceptTouchEventListener?.onInterceptTouchEvent(this, ev) == true) {
|
||||
return true
|
||||
}
|
||||
// We intercept the touch event if the children are not supposed to receive it.
|
||||
return !PointerEvents.canChildrenBeTouchTarget(_pointerEvents) ||
|
||||
super.onInterceptTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
// We do not accept the touch event if this view is not supposed to receive it.
|
||||
// The root view always assumes any view that was tapped wants the touch
|
||||
// and sends the event to JS as such.
|
||||
// We don't need to do bubbling in native (it's already happening in JS).
|
||||
// For an explanation of bubbling and capturing, see
|
||||
// http://javascript.info/tutorial/bubbling-and-capturing#capturing
|
||||
return PointerEvents.canBeTouchTarget(_pointerEvents)
|
||||
}
|
||||
|
||||
override fun onHoverEvent(event: MotionEvent): Boolean =
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
// Match the logic from onTouchEvent if pointer events are enabled
|
||||
PointerEvents.canBeTouchTarget(_pointerEvents)
|
||||
} else {
|
||||
super.onHoverEvent(event)
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(ev: MotionEvent): Boolean =
|
||||
// We do not dispatch the motion event if its children are not supposed to receive it
|
||||
PointerEvents.canChildrenBeTouchTarget(_pointerEvents) || super.dispatchGenericMotionEvent(ev)
|
||||
|
||||
/**
|
||||
* We override this to allow developers to determine whether they need offscreen alpha compositing
|
||||
* or not. See the documentation of needsOffscreenAlphaCompositing in View.js.
|
||||
*/
|
||||
override fun hasOverlappingRendering(): Boolean = needsOffscreenAlphaCompositing
|
||||
|
||||
/** See the documentation of needsOffscreenAlphaCompositing in View.js. */
|
||||
public open fun setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing: Boolean): Unit {
|
||||
this.needsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing
|
||||
}
|
||||
|
||||
public open fun setBorderWidth(position: Int, width: Float): Unit {
|
||||
BackgroundStyleApplicator.setBorderWidth(this, LogicalEdge.entries[position], width.pxToDp())
|
||||
}
|
||||
|
||||
public open fun setBorderColor(position: Int, color: Int?): Unit {
|
||||
BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.entries[position], color)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)",
|
||||
ReplaceWith(
|
||||
"setBorderRadius(BorderRadiusProp.BORDER_RADIUS, borderRadius)",
|
||||
"com.facebook.react.uimanager.style.BorderRadiusProp",
|
||||
))
|
||||
@Suppress("DEPRECATION")
|
||||
public open fun setBorderRadius(borderRadius: Float): Unit {
|
||||
this.setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)",
|
||||
ReplaceWith(
|
||||
"setBorderRadius(BorderRadiusProp.entries[position], borderRadius)",
|
||||
"com.facebook.react.uimanager.style.BorderRadiusProp",
|
||||
))
|
||||
public open fun setBorderRadius(borderRadius: Float, position: Int): Unit {
|
||||
val radius =
|
||||
when {
|
||||
borderRadius.isNaN() -> null
|
||||
else -> LengthPercentage(borderRadius, LengthPercentageType.POINT)
|
||||
}
|
||||
BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.entries[position], radius)
|
||||
}
|
||||
|
||||
public open fun setBorderRadius(
|
||||
property: BorderRadiusProp,
|
||||
borderRadius: LengthPercentage?
|
||||
): Unit {
|
||||
BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius)
|
||||
}
|
||||
|
||||
public open fun setBorderStyle(style: String?): Unit {
|
||||
BackgroundStyleApplicator.setBorderStyle(this, style?.let { BorderStyle.fromString(style) })
|
||||
}
|
||||
|
||||
override fun setRemoveClippedSubviews(removeClippedSubviews: Boolean) {
|
||||
if (removeClippedSubviews == _removeClippedSubviews) {
|
||||
return
|
||||
}
|
||||
_removeClippedSubviews = removeClippedSubviews
|
||||
if (removeClippedSubviews) {
|
||||
val clippingRect = Rect()
|
||||
ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect)
|
||||
allChildrenCount = childCount
|
||||
val initialSize = max(12, allChildrenCount)
|
||||
val children = arrayOfNulls<View?>(initialSize)
|
||||
childrenLayoutChangeListener = ChildrenLayoutChangeListener(this)
|
||||
for (i in 0 until allChildrenCount) {
|
||||
children[i] =
|
||||
getChildAt(i).apply { addOnLayoutChangeListener(childrenLayoutChangeListener) }
|
||||
}
|
||||
_clippingRect = clippingRect
|
||||
allChildren = children
|
||||
updateClippingRect()
|
||||
} else {
|
||||
// Add all clipped views back, deallocate additional arrays, remove layoutChangeListener
|
||||
val clippingRect = checkNotNull(_clippingRect)
|
||||
val children = checkNotNull(allChildren)
|
||||
val listener = checkNotNull(childrenLayoutChangeListener)
|
||||
for (i in 0 until allChildrenCount) {
|
||||
children[i]?.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
getDrawingRect(clippingRect)
|
||||
updateClippingToRect(clippingRect)
|
||||
allChildren = null
|
||||
_clippingRect = null
|
||||
allChildrenCount = 0
|
||||
childrenLayoutChangeListener = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRemoveClippedSubviews(): Boolean = _removeClippedSubviews
|
||||
|
||||
override fun getClippingRect(outClippingRect: Rect) {
|
||||
outClippingRect.set(
|
||||
checkNotNull(_clippingRect) { "getClippingRect called when removeClippedSubviews not set" })
|
||||
}
|
||||
|
||||
override fun updateClippingRect() {
|
||||
if (!_removeClippedSubviews) {
|
||||
return
|
||||
}
|
||||
val clippingRect = checkNotNull(_clippingRect)
|
||||
checkNotNull(allChildren)
|
||||
ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect)
|
||||
updateClippingToRect(clippingRect)
|
||||
}
|
||||
|
||||
private fun updateClippingToRect(clippingRect: Rect) {
|
||||
val children = checkNotNull(allChildren)
|
||||
var clippedSoFar = 0
|
||||
for (i in 0 until allChildrenCount) {
|
||||
updateSubviewClipStatus(clippingRect, i, clippedSoFar)
|
||||
if (children[i]?.parent == null) {
|
||||
clippedSoFar++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSubviewClipStatus(clippingRect: Rect, idx: Int, clippedSoFar: Int) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
val child = checkNotNull(allChildren?.get(idx))
|
||||
val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom)
|
||||
var needUpdateClippingRecursive = false
|
||||
// We never want to clip children that are being animated, as this can easily break layout :
|
||||
// when layout animation changes size and/or position of views contained inside a listview that
|
||||
// clips offscreen children, we need to ensure that, when view exits the viewport, final size
|
||||
// and position is set prior to removing the view from its listview parent.
|
||||
// Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll,
|
||||
// it won't be size and located properly.
|
||||
val animation = child.animation
|
||||
val isAnimating = animation?.hasEnded() == false
|
||||
if (!intersects && child.parent != null && !isAnimating) {
|
||||
// We can try saving on invalidate call here as the view that we remove is out of visible
|
||||
// area
|
||||
// therefore invalidation is not necessary.
|
||||
removeViewInLayout(child)
|
||||
needUpdateClippingRecursive = true
|
||||
} else if (intersects && child.parent == null) {
|
||||
addViewInLayout(child, idx - clippedSoFar, defaultLayoutParam, true)
|
||||
invalidate()
|
||||
needUpdateClippingRecursive = true
|
||||
} else if (intersects) {
|
||||
// If there is any intersection we need to inform the child to update its clipping rect
|
||||
needUpdateClippingRecursive = true
|
||||
}
|
||||
if (needUpdateClippingRecursive &&
|
||||
child is ReactClippingViewGroup &&
|
||||
child.removeClippedSubviews) {
|
||||
child.updateClippingRect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSubviewClipStatus(subview: View) {
|
||||
if (!_removeClippedSubviews || parent == null) {
|
||||
return
|
||||
}
|
||||
val clippingRect = checkNotNull(_clippingRect)
|
||||
val children = checkNotNull(allChildren)
|
||||
|
||||
// do fast check whether intersect state changed
|
||||
val intersects =
|
||||
clippingRect.intersects(subview.left, subview.top, subview.right, subview.bottom)
|
||||
|
||||
// If it was intersecting before, should be attached to the parent
|
||||
val oldIntersects = subview.parent != null
|
||||
if (intersects != oldIntersects) {
|
||||
var clippedSoFar = 0
|
||||
for (i in 0 until allChildrenCount) {
|
||||
if (children[i] === subview) {
|
||||
updateSubviewClipStatus(clippingRect, i, clippedSoFar)
|
||||
break
|
||||
}
|
||||
if (children[i]?.parent == null) {
|
||||
clippedSoFar++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (_removeClippedSubviews) {
|
||||
updateClippingRect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (_removeClippedSubviews) {
|
||||
updateClippingRect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun customDrawOrderDisabled(): Boolean =
|
||||
// Custom draw order is disabled for Fabric.
|
||||
id != NO_ID && ViewUtil.getUIManagerType(id) == UIManagerType.FABRIC
|
||||
|
||||
private fun handleAddView(view: View) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
if (!customDrawOrderDisabled()) {
|
||||
drawingOrderHelper.handleAddView(view)
|
||||
isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder()
|
||||
} else {
|
||||
isChildrenDrawingOrderEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRemoveView(view: View?) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
if (!customDrawOrderDisabled()) {
|
||||
if (indexOfChild(view) == -1) {
|
||||
return
|
||||
}
|
||||
drawingOrderHelper.handleRemoveView(view)
|
||||
isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder()
|
||||
} else {
|
||||
isChildrenDrawingOrderEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRemoveViews(start: Int, count: Int) {
|
||||
val endIndex = start + count
|
||||
for (index in start until endIndex) {
|
||||
if (index < childCount) {
|
||||
handleRemoveView(getChildAt(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View, index: Int, params: LayoutParams?) {
|
||||
// This will get called for every overload of addView so there is not need to override every
|
||||
// method.
|
||||
handleAddView(child)
|
||||
super.addView(child, index, params)
|
||||
}
|
||||
|
||||
override fun addViewInLayout(
|
||||
child: View,
|
||||
index: Int,
|
||||
params: LayoutParams,
|
||||
preventRequestLayout: Boolean
|
||||
): Boolean {
|
||||
handleAddView(child)
|
||||
return super.addViewInLayout(child, index, params, preventRequestLayout)
|
||||
}
|
||||
|
||||
override fun removeView(view: View?) {
|
||||
handleRemoveView(view)
|
||||
super.removeView(view)
|
||||
}
|
||||
|
||||
override fun removeViewAt(index: Int) {
|
||||
handleRemoveView(getChildAt(index))
|
||||
super.removeViewAt(index)
|
||||
}
|
||||
|
||||
override fun removeViewInLayout(view: View) {
|
||||
handleRemoveView(view)
|
||||
super.removeViewInLayout(view)
|
||||
}
|
||||
|
||||
override fun removeViewsInLayout(start: Int, count: Int) {
|
||||
handleRemoveViews(start, count)
|
||||
super.removeViewsInLayout(start, count)
|
||||
}
|
||||
|
||||
override fun removeViews(start: Int, count: Int) {
|
||||
handleRemoveViews(start, count)
|
||||
super.removeViews(start, count)
|
||||
}
|
||||
|
||||
override fun getChildDrawingOrder(childCount: Int, index: Int): Int {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
return if (!customDrawOrderDisabled()) {
|
||||
drawingOrderHelper.getChildDrawingOrder(childCount, index)
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
override fun getZIndexMappedChildIndex(index: Int): Int {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
return if (!customDrawOrderDisabled() && drawingOrderHelper.shouldEnableCustomDrawingOrder()) {
|
||||
drawingOrderHelper.getChildDrawingOrder(childCount, index)
|
||||
} else {
|
||||
// Fabric behavior
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDrawingOrder() {
|
||||
if (customDrawOrderDisabled()) {
|
||||
return
|
||||
}
|
||||
drawingOrderHelper.update()
|
||||
isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun getPointerEvents(): PointerEvents = _pointerEvents
|
||||
|
||||
override fun dispatchSetPressed(pressed: Boolean) {
|
||||
// Prevents the ViewGroup from dispatching the pressed state
|
||||
// to it's children.
|
||||
}
|
||||
|
||||
public open fun setPointerEvents(pointerEvents: PointerEvents?): Unit {
|
||||
if (pointerEvents != null) {
|
||||
_pointerEvents = pointerEvents
|
||||
} else {
|
||||
resetPointerEvents()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun resetPointerEvents(): Unit {
|
||||
_pointerEvents = PointerEvents.AUTO
|
||||
}
|
||||
|
||||
internal open fun getChildAtWithSubviewClippingEnabled(index: Int): View? =
|
||||
if (index in 0 until allChildrenCount) {
|
||||
checkNotNull(allChildren)[index]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
internal open fun addViewWithSubviewClippingEnabled(child: View, index: Int): Unit {
|
||||
Assertions.assertCondition(_removeClippedSubviews)
|
||||
val clippingRect = checkNotNull(_clippingRect)
|
||||
val children = checkNotNull(allChildren)
|
||||
addInArray(child, index)
|
||||
// we add view as "clipped" and then run [updateSubviewClipStatus] to conditionally
|
||||
// attach it
|
||||
var clippedSoFar = 0
|
||||
for (i in 0 until index) {
|
||||
if (children[i]?.parent == null) {
|
||||
clippedSoFar++
|
||||
}
|
||||
}
|
||||
updateSubviewClipStatus(clippingRect, index, clippedSoFar)
|
||||
child.addOnLayoutChangeListener(childrenLayoutChangeListener)
|
||||
if (child is ReactClippingProhibitedView) {
|
||||
UiThreadUtil.runOnUiThread {
|
||||
if (!child.isShown) {
|
||||
ReactSoftExceptionLogger.logSoftException(
|
||||
ReactConstants.TAG,
|
||||
ReactNoCrashSoftException(
|
||||
"""
|
||||
|Child view has been added to Parent view in which it is clipped and not
|
||||
|visible. This is not legal for this particular child view. Child: [${child.id}]
|
||||
| $child Parent: [$id] $parent"""
|
||||
.trimMargin()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun removeViewWithSubviewClippingEnabled(view: View): Unit {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
Assertions.assertCondition(_removeClippedSubviews)
|
||||
checkNotNull(_clippingRect)
|
||||
val children = checkNotNull(allChildren)
|
||||
view.removeOnLayoutChangeListener(childrenLayoutChangeListener)
|
||||
val index = indexOfChildInAllChildren(view)
|
||||
if (children[index]?.parent != null) {
|
||||
var clippedSoFar = 0
|
||||
for (i in 0 until index) {
|
||||
if (children[i]?.parent == null) {
|
||||
clippedSoFar++
|
||||
}
|
||||
}
|
||||
removeViewsInLayout(index - clippedSoFar, 1)
|
||||
}
|
||||
removeFromArray(index)
|
||||
}
|
||||
|
||||
internal open fun removeAllViewsWithSubviewClippingEnabled(): Unit {
|
||||
Assertions.assertCondition(_removeClippedSubviews)
|
||||
val children = checkNotNull(allChildren)
|
||||
for (i in 0 until allChildrenCount) {
|
||||
children[i]?.removeOnLayoutChangeListener(childrenLayoutChangeListener)
|
||||
}
|
||||
removeAllViewsInLayout()
|
||||
allChildrenCount = 0
|
||||
}
|
||||
|
||||
private fun indexOfChildInAllChildren(child: View): Int {
|
||||
val count = allChildrenCount
|
||||
val children = checkNotNull(allChildren)
|
||||
return (0 until count).firstOrNull { i -> children[i] === child } ?: -1
|
||||
}
|
||||
|
||||
private fun addInArray(child: View, index: Int) {
|
||||
val children = growAllChildrenIfNeeded(index)
|
||||
children[index] = child
|
||||
allChildrenCount++
|
||||
}
|
||||
|
||||
/**
|
||||
* Grow the [allChildren] array if it's run out of space
|
||||
*
|
||||
* @param insertIndex index where child is being inserted, must be <= [allChildrenCount]
|
||||
* @return the non-null array that's backing [allChildren] after any potential resize, with a null
|
||||
* slot at [insertIndex]
|
||||
*/
|
||||
private fun growAllChildrenIfNeeded(insertIndex: Int): Array<View?> {
|
||||
val children = checkNotNull(allChildren)
|
||||
val count = allChildrenCount
|
||||
if (insertIndex > count) {
|
||||
throw IndexOutOfBoundsException("index=$insertIndex count=$count")
|
||||
}
|
||||
if (children.size > count) {
|
||||
// no need to resize, ensure index is free
|
||||
if (insertIndex < count) {
|
||||
System.arraycopy(children, insertIndex, children, insertIndex + 1, count - insertIndex)
|
||||
}
|
||||
return children
|
||||
}
|
||||
// need to resize the array
|
||||
val newArray =
|
||||
if (insertIndex == count) {
|
||||
// inserting at the end of the array
|
||||
children.copyOf(children.size + ARRAY_CAPACITY_INCREMENT)
|
||||
} else {
|
||||
// inserting within the array
|
||||
arrayOfNulls<View?>(children.size + ARRAY_CAPACITY_INCREMENT).apply {
|
||||
System.arraycopy(children, 0, this, 0, insertIndex)
|
||||
System.arraycopy(children, insertIndex, this, insertIndex + 1, count - insertIndex)
|
||||
}
|
||||
}
|
||||
allChildren = newArray
|
||||
return newArray
|
||||
}
|
||||
|
||||
private fun removeFromArray(index: Int) {
|
||||
val children = checkNotNull(allChildren)
|
||||
val count = allChildrenCount
|
||||
if (index == count - 1) {
|
||||
children[--allChildrenCount] = null
|
||||
} else if (index in 0 until count) {
|
||||
System.arraycopy(children, index + 1, children, index, count - index - 1)
|
||||
children[--allChildrenCount] = null
|
||||
} else {
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun needsIsolatedLayer(): Boolean {
|
||||
if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) {
|
||||
return false
|
||||
}
|
||||
return (0 until childCount).any { i -> getChildAt(i).getTag(R.id.mix_blend_mode) != null }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected open fun getBackgroundColor(): Int =
|
||||
BackgroundStyleApplicator.getBackgroundColor(this) ?: DEFAULT_BACKGROUND_COLOR
|
||||
|
||||
// TODO: convert to val
|
||||
public open fun setOverflow(overflow: String?): Unit {
|
||||
_overflow =
|
||||
if (overflow == null) {
|
||||
Overflow.VISIBLE
|
||||
} else {
|
||||
Overflow.fromString(overflow) ?: Overflow.VISIBLE
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun getOverflow(): String? =
|
||||
when (_overflow) {
|
||||
Overflow.HIDDEN -> "hidden"
|
||||
Overflow.SCROLL -> "scroll"
|
||||
Overflow.VISIBLE -> "visible"
|
||||
}
|
||||
|
||||
override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
if (needsIsolatedLayer() &&
|
||||
(_overflowInset.left != left ||
|
||||
_overflowInset.top != top ||
|
||||
_overflowInset.right != right ||
|
||||
_overflowInset.bottom != bottom)) {
|
||||
invalidate()
|
||||
}
|
||||
_overflowInset.set(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun getOverflowInset(): Rect = _overflowInset
|
||||
|
||||
/**
|
||||
* Set the background for the view or remove the background. It calls [setBackground(Drawable)] or
|
||||
* [setBackgroundDrawable(Drawable)] based on the sdk version.
|
||||
*
|
||||
* @param drawable [Drawable] The Drawable to use as the background, or null to remove the
|
||||
* background
|
||||
*/
|
||||
internal fun updateBackgroundDrawable(drawable: Drawable?): Unit {
|
||||
super.setBackground(drawable)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC &&
|
||||
needsIsolatedLayer()) {
|
||||
|
||||
// Check if the view is a stacking context and has children, if it does, do the rendering
|
||||
// offscreen and then composite back. This follows the idea of group isolation on blending
|
||||
// https://www.w3.org/TR/compositing-1/#isolationblending
|
||||
val overflowInset = this.overflowInset
|
||||
canvas.saveLayer(
|
||||
overflowInset.left.toFloat(),
|
||||
overflowInset.top.toFloat(),
|
||||
(width + -overflowInset.right).toFloat(),
|
||||
(height + -overflowInset.bottom).toFloat(),
|
||||
null)
|
||||
super.draw(canvas)
|
||||
canvas.restore()
|
||||
} else {
|
||||
super.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) {
|
||||
BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
|
||||
}
|
||||
super.dispatchDraw(canvas)
|
||||
}
|
||||
|
||||
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
|
||||
val drawWithZ = child.elevation > 0
|
||||
if (drawWithZ) {
|
||||
CanvasUtil.enableZ(canvas, true)
|
||||
}
|
||||
var mixBlendMode: BlendMode? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) {
|
||||
mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode
|
||||
if (mixBlendMode != null) {
|
||||
val p = Paint()
|
||||
p.blendMode = mixBlendMode
|
||||
val overflowInset = this.overflowInset
|
||||
canvas.saveLayer(
|
||||
overflowInset.left.toFloat(),
|
||||
overflowInset.top.toFloat(),
|
||||
(width + -overflowInset.right).toFloat(),
|
||||
(height + -overflowInset.bottom).toFloat(),
|
||||
p)
|
||||
}
|
||||
}
|
||||
val result = super.drawChild(canvas, child, drawingTime)
|
||||
if (mixBlendMode != null) {
|
||||
canvas.restore()
|
||||
}
|
||||
if (drawWithZ) {
|
||||
CanvasUtil.enableZ(canvas, false)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public open fun setOpacityIfPossible(opacity: Float): Unit {
|
||||
backfaceOpacity = opacity
|
||||
setBackfaceVisibilityDependantOpacity()
|
||||
}
|
||||
|
||||
public open fun setBackfaceVisibility(backfaceVisibility: String?): Unit {
|
||||
this.backfaceVisibility = backfaceVisibility
|
||||
setBackfaceVisibilityDependantOpacity()
|
||||
}
|
||||
|
||||
public open fun setBackfaceVisibilityDependantOpacity(): Unit {
|
||||
val isBackfaceVisible = backfaceVisibility == "visible"
|
||||
if (isBackfaceVisible) {
|
||||
alpha = backfaceOpacity
|
||||
return
|
||||
}
|
||||
val rotationX = rotationX
|
||||
val rotationY = rotationY
|
||||
val isFrontfaceVisible =
|
||||
rotationX >= -90f && rotationX < 90f && rotationY >= -90f && rotationY < 90f
|
||||
if (isFrontfaceVisible) {
|
||||
alpha = backfaceOpacity
|
||||
return
|
||||
}
|
||||
alpha = 0f
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -166,23 +166,23 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
|
||||
when (hitSlop.type) {
|
||||
ReadableType.Map -> {
|
||||
val hitSlopMap = hitSlop.asMap()
|
||||
view.hitSlopRect =
|
||||
view.setHitSlopRect(
|
||||
Rect(
|
||||
getPixels(hitSlopMap, "left"),
|
||||
getPixels(hitSlopMap, "top"),
|
||||
getPixels(hitSlopMap, "right"),
|
||||
getPixels(hitSlopMap, "bottom"))
|
||||
getPixels(hitSlopMap, "bottom")))
|
||||
}
|
||||
|
||||
ReadableType.Number -> {
|
||||
val hitSlopValue = hitSlop.asDouble().dpToPx().toInt()
|
||||
view.hitSlopRect = Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)
|
||||
view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue))
|
||||
}
|
||||
|
||||
ReadableType.Null -> view.hitSlopRect = null
|
||||
ReadableType.Null -> view.setHitSlopRect(null)
|
||||
else -> {
|
||||
FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}")
|
||||
view.hitSlopRect = null
|
||||
view.setHitSlopRect(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.view
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class ReactViewGroupTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ReactNativeFeatureFlagsForTests.setUp()
|
||||
context = Robolectric.buildActivity(Activity::class.java).create().get()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `View clipping - ensure allChildren properly resizes when adding views in sequence`() {
|
||||
val rvg = ReactViewGroup(context)
|
||||
rvg.left = 0
|
||||
rvg.right = 100
|
||||
rvg.top = 0
|
||||
rvg.bottom = 100
|
||||
FrameLayout(context).addView(rvg)
|
||||
rvg.removeClippedSubviews = true
|
||||
for (i in 0..20) {
|
||||
rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i)
|
||||
}
|
||||
rvg.updateClippingRect()
|
||||
assertThat(rvg.childCount).isEqualTo(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `View clipping - ensure allChildren properly resizes when adding views out of sequence`() {
|
||||
val rvg = ReactViewGroup(context)
|
||||
rvg.left = 0
|
||||
rvg.right = 100
|
||||
rvg.top = 0
|
||||
rvg.bottom = 100
|
||||
FrameLayout(context).addView(rvg)
|
||||
rvg.removeClippedSubviews = true
|
||||
for (i in 0..10) {
|
||||
rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i)
|
||||
}
|
||||
repeat(10) { rvg.addViewWithSubviewClippingEnabled(TestView(context, 90), 10) }
|
||||
rvg.updateClippingRect()
|
||||
assertThat(rvg.childCount).isEqualTo(20)
|
||||
}
|
||||
}
|
||||
|
||||
class TestView(context: Context, yPos: Int) : View(context) {
|
||||
init {
|
||||
left = 0
|
||||
right = 100
|
||||
top = yPos
|
||||
bottom = top + 10
|
||||
}
|
||||
}
|
||||
|
||||
class TestParent(context: Context) : ViewGroup(context) {
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit
|
||||
}
|
||||
Reference in New Issue
Block a user