From f939f1f02db5e4d746b321138369b9eca3fdfeca Mon Sep 17 00:00:00 2001 From: David Vacca <515103+mdvacca@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:25:37 -0700 Subject: [PATCH 1/2] Back out "Convert ReactViewGroup to Kotlin" Summary: Original commit changeset: 780350e7e449 Original Phabricator Diff: D62642663 Differential Revision: D63076763 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/views/modal/ReactModalHostView.kt | 16 +- .../react/views/view/ReactViewGroup.java | 966 ++++++++++++++++++ .../react/views/view/ReactViewGroup.kt | 826 --------------- .../react/views/view/ReactViewManager.kt | 10 +- 5 files changed, 980 insertions(+), 839 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 24c8a318da12ba..7fb74ff9b41bdd 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8326,6 +8326,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; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index e6403c31fb16e2..316efcd44b177f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -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 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java new file mode 100644 index 00000000000000..7f7a799c02e9b1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -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. + * + *

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); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt deleted file mode 100644 index c080ec54afebd4..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ /dev/null @@ -1,826 +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? = 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(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) - updateClippingRect() - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - 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) { - var children = checkNotNull(allChildren) - val count = allChildrenCount - val size = children.size - if (index == count) { - if (size == count) { - children = arrayOfNulls(size + ARRAY_CAPACITY_INCREMENT) - System.arraycopy(children, 0, children, 0, size) - allChildren = children - } - children[allChildrenCount++] = child - } else if (index < count) { - if (size == count) { - children = arrayOfNulls(size + ARRAY_CAPACITY_INCREMENT) - System.arraycopy(children, 0, children, 0, index) - System.arraycopy(children, index, children, index + 1, count - index) - allChildren = children - } else { - System.arraycopy(children, index, children, index + 1, count - index) - } - children[index] = child - allChildrenCount++ - } else { - throw IndexOutOfBoundsException("index=$index count=$count") - } - } - - private fun removeFromArray(index: Int) { - val children = checkNotNull(allChildren) - val count = allChildrenCount - if (index == count - 1) { - children[--allChildrenCount] = null - } else if (index in 0.. 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) - } - - // TODO: this is mutable! - 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 - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index aac6e9347b7389..4b6dc1957e26f1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -166,23 +166,23 @@ public open class ReactViewManager : ReactClippingViewManager() 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) } } } From 0e25a02185d4b8074405cb1b6f4a768dc6807ec8 Mon Sep 17 00:00:00 2001 From: David Vacca <515103+mdvacca@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:25:37 -0700 Subject: [PATCH 2/2] Back out "Convert ReactViewManager, ReactClippingViewManager to Kotlin" Summary: Original commit changeset: 09a81c91a1c9 Original Phabricator Diff: D62776270 Differential Revision: D63076764 --- .../ReactAndroid/api/ReactAndroid.api | 9 +- .../views/view/ReactClippingViewManager.java | 95 ++++ .../views/view/ReactClippingViewManager.kt | 73 --- .../react/views/view/ReactViewManager.java | 425 ++++++++++++++++++ .../react/views/view/ReactViewManager.kt | 390 ---------------- 5 files changed, 522 insertions(+), 470 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 7fb74ff9b41bdd..79ae4e24dd0f56 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8371,8 +8371,6 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro } public class com/facebook/react/views/view/ReactViewManager : com/facebook/react/views/view/ReactClippingViewManager { - public static final field Companion Lcom/facebook/react/views/view/ReactViewManager$Companion; - public static final field REACT_CLASS Ljava/lang/String; public fun ()V public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/view/ReactViewGroup; @@ -8383,7 +8381,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun nextFocusLeft (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusRight (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusUp (Lcom/facebook/react/views/view/ReactViewGroup;I)V - public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; + 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/view/ReactViewGroup;)Lcom/facebook/react/views/view/ReactViewGroup; public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V @@ -8412,7 +8410,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun setOverflow (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setPointerEvents (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setTVPreferredFocus (Lcom/facebook/react/views/view/ReactViewGroup;Z)V - public synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V + protected synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V protected fun setTransformProperty (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V } @@ -8423,9 +8421,6 @@ public class com/facebook/react/views/view/ReactViewManager$$PropsSetter : com/f public fun setProperty (Lcom/facebook/react/views/view/ReactViewManager;Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;Ljava/lang/Object;)V } -public final class com/facebook/react/views/view/ReactViewManager$Companion { -} - public final class com/facebook/react/views/view/ViewGroupClickEvent : com/facebook/react/uimanager/events/Event { public fun (I)V public fun (II)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java new file mode 100644 index 00000000000000..525183053a0ef0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java @@ -0,0 +1,95 @@ +/* + * 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.view.View; +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.annotations.ReactProp; + +/** + * View manager which handles clipped subviews. Useful for custom views which extends from {@link + * com.facebook.react.views.view.ReactViewGroup} + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public abstract class ReactClippingViewManager + extends ViewGroupManager { + + @ReactProp( + name = com.facebook.react.uimanager.ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) + public void setRemoveClippedSubviews(T view, boolean removeClippedSubviews) { + UiThreadUtil.assertOnUiThread(); + + view.setRemoveClippedSubviews(removeClippedSubviews); + } + + @Override + public void addView(T parent, View child, int index) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + parent.addViewWithSubviewClippingEnabled(child, index); + } else { + parent.addView(child, index); + } + } + + @Override + public int getChildCount(T parent) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getAllChildrenCount(); + } else { + return parent.getChildCount(); + } + } + + @Override + @Nullable + public View getChildAt(T parent, int index) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getChildAtWithSubviewClippingEnabled(index); + } else { + return parent.getChildAt(index); + } + } + + @Override + public void removeViewAt(T parent, int index) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + View child = getChildAt(parent, index); + if (child != null) { + if (child.getParent() != null) { + parent.removeView(child); + } + parent.removeViewWithSubviewClippingEnabled(child); + } + } else { + parent.removeViewAt(index); + } + } + + @Override + public void removeAllViews(T parent) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + parent.removeAllViewsWithSubviewClippingEnabled(); + } else { + parent.removeAllViews(); + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt deleted file mode 100644 index d77075ed705586..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt +++ /dev/null @@ -1,73 +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.view.View -import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.uimanager.ReactClippingViewGroupHelper -import com.facebook.react.uimanager.ViewGroupManager -import com.facebook.react.uimanager.annotations.ReactProp - -/** - * View manager which handles clipped subviews. Useful for custom views which extends from - * [com.facebook.react.views.view.ReactViewGroup] - */ -public abstract class ReactClippingViewManager : ViewGroupManager() { - - @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) - public open fun setRemoveClippedSubviews(view: T, removeClippedSubviews: Boolean) { - UiThreadUtil.assertOnUiThread() - view.removeClippedSubviews = removeClippedSubviews - } - - override fun addView(parent: T, child: View, index: Int) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - parent.addViewWithSubviewClippingEnabled(child, index) - } else { - parent.addView(child, index) - } - } - - override fun getChildCount(parent: T): Int = - if (parent.removeClippedSubviews) { - parent.allChildrenCount - } else { - parent.childCount - } - - override fun getChildAt(parent: T, index: Int): View? = - if (parent.removeClippedSubviews) { - parent.getChildAtWithSubviewClippingEnabled(index) - } else { - parent.getChildAt(index) - } - - override fun removeViewAt(parent: T, index: Int) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - val child = getChildAt(parent, index) ?: return - if (child.parent != null) { - parent.removeView(child) - } else { - parent.removeViewWithSubviewClippingEnabled(child) - } - } else { - parent.removeViewAt(index) - } - } - - override fun removeAllViews(parent: T) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - parent.removeAllViewsWithSubviewClippingEnabled() - } else { - parent.removeAllViews() - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java new file mode 100644 index 00000000000000..548fe524399cf9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -0,0 +1,425 @@ +/* + * 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.graphics.Rect; +import android.view.View; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.DynamicFromObject; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.BackgroundStyleApplicator; +import com.facebook.react.uimanager.LengthPercentage; +import com.facebook.react.uimanager.LengthPercentageType; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.Spacing; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.common.ViewUtil; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.style.BackgroundImageLayer; +import com.facebook.react.uimanager.style.BorderRadiusProp; +import com.facebook.react.uimanager.style.BorderStyle; +import com.facebook.react.uimanager.style.LogicalEdge; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** View manager for AndroidViews (plain React Views). */ +@ReactModule(name = ReactViewManager.REACT_CLASS) +@Nullsafe(Nullsafe.Mode.LOCAL) +public class ReactViewManager extends ReactClippingViewManager { + + @VisibleForTesting public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME; + + private static final int[] SPACING_TYPES = { + Spacing.ALL, + Spacing.LEFT, + Spacing.RIGHT, + Spacing.TOP, + Spacing.BOTTOM, + Spacing.START, + Spacing.END, + Spacing.BLOCK, + Spacing.BLOCK_END, + Spacing.BLOCK_START + }; + private static final int CMD_HOTSPOT_UPDATE = 1; + private static final int CMD_SET_PRESSED = 2; + private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate"; + + public ReactViewManager() { + super(); + + setupViewRecycling(); + } + + @Override + protected @Nullable ReactViewGroup prepareToRecycleView( + ThemedReactContext reactContext, ReactViewGroup view) { + // BaseViewManager + ReactViewGroup preparedView = super.prepareToRecycleView(reactContext, view); + if (preparedView != null) { + preparedView.recycleView(); + } + return view; + } + + @ReactProp(name = "accessible") + public void setAccessible(ReactViewGroup view, boolean accessible) { + view.setFocusable(accessible); + } + + @ReactProp(name = "hasTVPreferredFocus") + public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus) { + if (hasTVPreferredFocus) { + view.setFocusable(true); + view.setFocusableInTouchMode(true); + view.requestFocus(); + } + } + + @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") + public void setBackgroundImage(ReactViewGroup view, @Nullable ReadableArray backgroundImage) { + if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { + if (backgroundImage != null && backgroundImage.size() > 0) { + List backgroundImageLayers = new ArrayList<>(backgroundImage.size()); + for (int i = 0; i < backgroundImage.size(); i++) { + ReadableMap backgroundImageMap = backgroundImage.getMap(i); + BackgroundImageLayer layer = + new BackgroundImageLayer(backgroundImageMap, view.getContext()); + backgroundImageLayers.add(layer); + } + BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers); + } else { + BackgroundStyleApplicator.setBackgroundImage(view, null); + } + } + } + + @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) + public void nextFocusDown(ReactViewGroup view, int viewId) { + view.setNextFocusDownId(viewId); + } + + @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) + public void nextFocusForward(ReactViewGroup view, int viewId) { + view.setNextFocusForwardId(viewId); + } + + @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) + public void nextFocusLeft(ReactViewGroup view, int viewId) { + view.setNextFocusLeftId(viewId); + } + + @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) + public void nextFocusRight(ReactViewGroup view, int viewId) { + view.setNextFocusRightId(viewId); + } + + @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) + public void nextFocusUp(ReactViewGroup view, int viewId) { + view.setNextFocusUpId(viewId); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_RADIUS, + ViewProps.BORDER_TOP_LEFT_RADIUS, + ViewProps.BORDER_TOP_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_LEFT_RADIUS, + ViewProps.BORDER_TOP_START_RADIUS, + ViewProps.BORDER_TOP_END_RADIUS, + ViewProps.BORDER_BOTTOM_START_RADIUS, + ViewProps.BORDER_BOTTOM_END_RADIUS, + ViewProps.BORDER_END_END_RADIUS, + ViewProps.BORDER_END_START_RADIUS, + ViewProps.BORDER_START_END_RADIUS, + ViewProps.BORDER_START_START_RADIUS, + }) + public void setBorderRadius(ReactViewGroup view, int index, Dynamic rawBorderRadius) { + @Nullable LengthPercentage borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius); + + // We do not support percentage border radii on Paper in order to be consistent with iOS (to + // avoid developer surprise if it works on one platform but not another). + if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC + && borderRadius != null + && borderRadius.getType() == LengthPercentageType.PERCENT) { + borderRadius = null; + } + + BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius); + } + + /** + * @deprecated Use {@link #setBorderRadius(ReactViewGroup, int, Dynamic)} instead. + */ + @Deprecated(since = "0.75.0", forRemoval = true) + public void setBorderRadius(ReactViewGroup view, int index, float borderRadius) { + setBorderRadius(view, index, new DynamicFromObject(borderRadius)); + } + + @ReactProp(name = "borderStyle") + public void setBorderStyle(ReactViewGroup view, @Nullable String borderStyle) { + @Nullable + BorderStyle parsedBorderStyle = + borderStyle == null ? null : BorderStyle.fromString(borderStyle); + BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle); + } + + @ReactProp(name = "hitSlop") + public void setHitSlop(final ReactViewGroup view, Dynamic hitSlop) { + switch (hitSlop.getType()) { + case Map: + ReadableMap hitSlopMap = hitSlop.asMap(); + view.setHitSlopRect( + new Rect( + hitSlopMap.hasKey("left") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("left")) + : 0, + hitSlopMap.hasKey("top") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("top")) + : 0, + hitSlopMap.hasKey("right") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("right")) + : 0, + hitSlopMap.hasKey("bottom") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("bottom")) + : 0)); + break; + case Number: + int hitSlopValue = (int) PixelUtil.toPixelFromDIP(hitSlop.asDouble()); + view.setHitSlopRect(new Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)); + break; + default: + FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value " + hitSlop.getType()); + /* falls through */ + case Null: + view.setHitSlopRect(null); + break; + } + } + + @ReactProp(name = ViewProps.POINTER_EVENTS) + public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEventsStr) { + view.setPointerEvents(PointerEvents.parsePointerEvents(pointerEventsStr)); + } + + @ReactProp(name = "nativeBackgroundAndroid") + public void setNativeBackground(ReactViewGroup view, @Nullable ReadableMap bg) { + view.setTranslucentBackgroundDrawable( + bg == null + ? null + : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg)); + } + + @ReactProp(name = "nativeForegroundAndroid") + public void setNativeForeground(ReactViewGroup view, @Nullable ReadableMap fg) { + view.setForeground( + fg == null + ? null + : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), fg)); + } + + @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) + public void setNeedsOffscreenAlphaCompositing( + ReactViewGroup view, boolean needsOffscreenAlphaCompositing) { + view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_WIDTH, + ViewProps.BORDER_LEFT_WIDTH, + ViewProps.BORDER_RIGHT_WIDTH, + ViewProps.BORDER_TOP_WIDTH, + ViewProps.BORDER_BOTTOM_WIDTH, + ViewProps.BORDER_START_WIDTH, + ViewProps.BORDER_END_WIDTH, + }, + defaultFloat = Float.NaN) + public void setBorderWidth(ReactViewGroup view, int index, float width) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_COLOR, + ViewProps.BORDER_LEFT_COLOR, + ViewProps.BORDER_RIGHT_COLOR, + ViewProps.BORDER_TOP_COLOR, + ViewProps.BORDER_BOTTOM_COLOR, + ViewProps.BORDER_START_COLOR, + ViewProps.BORDER_END_COLOR, + ViewProps.BORDER_BLOCK_COLOR, + ViewProps.BORDER_BLOCK_END_COLOR, + ViewProps.BORDER_BLOCK_START_COLOR + }, + customType = "Color") + public void setBorderColor(ReactViewGroup view, int index, @Nullable Integer color) { + BackgroundStyleApplicator.setBorderColor( + view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color); + } + + @ReactProp(name = ViewProps.COLLAPSABLE) + public void setCollapsable(ReactViewGroup view, boolean collapsable) { + // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually + // handled in NativeViewHierarchyOptimizer + } + + @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) + public void setCollapsableChildren(ReactViewGroup view, boolean collapsableChildren) { + // no-op: it's here only so that "collapsableChildren" property is exported to JS. + } + + @ReactProp(name = "focusable") + public void setFocusable(final ReactViewGroup view, boolean focusable) { + if (focusable) { + view.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + final EventDispatcher mEventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag( + (ReactContext) view.getContext(), view.getId()); + if (mEventDispatcher == null) { + return; + } + mEventDispatcher.dispatchEvent( + new ViewGroupClickEvent( + UIManagerHelper.getSurfaceId(view.getContext()), view.getId())); + } + }); + + // Clickable elements are focusable. On API 26, this is taken care by setClickable. + // Explicitly calling setFocusable here for backward compatibility. + view.setFocusable(true /*isFocusable*/); + } else { + view.setOnClickListener(null); + view.setClickable(false); + // Don't set view.setFocusable(false) because we might still want it to be focusable for + // accessibility reasons + } + } + + @ReactProp(name = ViewProps.OVERFLOW) + public void setOverflow(ReactViewGroup view, String overflow) { + view.setOverflow(overflow); + } + + @ReactProp(name = "backfaceVisibility") + public void setBackfaceVisibility(ReactViewGroup view, String backfaceVisibility) { + view.setBackfaceVisibility(backfaceVisibility); + } + + @Override + public void setOpacity(ReactViewGroup view, float opacity) { + view.setOpacityIfPossible(opacity); + } + + @Override + protected void setTransformProperty( + ReactViewGroup view, + @Nullable ReadableArray transforms, + @Nullable ReadableArray transformOrigin) { + super.setTransformProperty(view, transforms, transformOrigin); + view.setBackfaceVisibilityDependantOpacity(); + } + + @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") + public void setBoxShadow(ReactViewGroup view, @Nullable ReadableArray shadows) { + BackgroundStyleApplicator.setBoxShadow(view, shadows); + } + + @Override + public void setBackgroundColor(ReactViewGroup view, @ColorInt int backgroundColor) { + BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor); + } + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactViewGroup createViewInstance(ThemedReactContext context) { + return new ReactViewGroup(context); + } + + @Override + public Map getCommandsMap() { + return MapBuilder.of(HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED); + } + + @Override + public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) { + switch (commandId) { + case CMD_HOTSPOT_UPDATE: + { + handleHotspotUpdate(root, args); + break; + } + case CMD_SET_PRESSED: + { + handleSetPressed(root, args); + break; + } + } + } + + @Override + public void receiveCommand(ReactViewGroup root, String commandId, @Nullable ReadableArray args) { + switch (commandId) { + case HOTSPOT_UPDATE_KEY: + { + handleHotspotUpdate(root, args); + break; + } + case "setPressed": + { + handleSetPressed(root, args); + break; + } + } + } + + private void handleSetPressed(ReactViewGroup root, @Nullable ReadableArray args) { + if (args == null || args.size() != 1) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'setPressed' command"); + } + root.setPressed(args.getBoolean(0)); + } + + private void handleHotspotUpdate(ReactViewGroup root, @Nullable ReadableArray args) { + if (args == null || args.size() != 2) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'updateHotspot' command"); + } + + float x = PixelUtil.toPixelFromDIP(args.getDouble(0)); + float y = PixelUtil.toPixelFromDIP(args.getDouble(1)); + root.drawableHotspotChanged(x, y); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt deleted file mode 100644 index 4b6dc1957e26f1..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ /dev/null @@ -1,390 +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.graphics.Rect -import android.view.View -import androidx.annotation.ColorInt -import com.facebook.common.logging.FLog -import com.facebook.react.bridge.Dynamic -import com.facebook.react.bridge.DynamicFromObject -import com.facebook.react.bridge.JSApplicationIllegalArgumentException -import com.facebook.react.bridge.ReactContext -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.ReadableType -import com.facebook.react.common.ReactConstants -import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.BackgroundStyleApplicator -import com.facebook.react.uimanager.LengthPercentage.Companion.setFromDynamic -import com.facebook.react.uimanager.LengthPercentageType -import com.facebook.react.uimanager.PixelUtil.dpToPx -import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents -import com.facebook.react.uimanager.Spacing -import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.UIManagerHelper -import com.facebook.react.uimanager.ViewProps -import com.facebook.react.uimanager.annotations.ReactProp -import com.facebook.react.uimanager.annotations.ReactPropGroup -import com.facebook.react.uimanager.common.UIManagerType -import com.facebook.react.uimanager.common.ViewUtil -import com.facebook.react.uimanager.style.BackgroundImageLayer -import com.facebook.react.uimanager.style.BorderRadiusProp -import com.facebook.react.uimanager.style.BorderStyle -import com.facebook.react.uimanager.style.BorderStyle.Companion.fromString -import com.facebook.react.uimanager.style.LogicalEdge -import com.facebook.react.uimanager.style.LogicalEdge.Companion.fromSpacingType - -/** View manager for AndroidViews (plain React Views). */ -@ReactModule(name = ReactViewManager.REACT_CLASS) -public open class ReactViewManager : ReactClippingViewManager() { - - init { - setupViewRecycling() - } - - override fun prepareToRecycleView( - reactContext: ThemedReactContext, - view: ReactViewGroup - ): ReactViewGroup { - // BaseViewManager - super.prepareToRecycleView(reactContext, view)?.recycleView() - return view - } - - @ReactProp(name = "accessible") - public open fun setAccessible(view: ReactViewGroup, accessible: Boolean): Unit { - view.isFocusable = accessible - } - - @ReactProp(name = "hasTVPreferredFocus") - public open fun setTVPreferredFocus(view: ReactViewGroup, hasTVPreferredFocus: Boolean): Unit { - if (hasTVPreferredFocus) { - view.isFocusable = true - view.isFocusableInTouchMode = true - view.requestFocus() - } - } - - @OptIn(UnstableReactNativeAPI::class) - @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") - public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?): Unit { - if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { - val size = backgroundImage?.size() - if (size != null && size > 0) { - val backgroundImageLayers = ArrayList(size) - repeat(size) { i -> - backgroundImageLayers.add(BackgroundImageLayer(backgroundImage.getMap(i), view.context)) - } - BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers) - } else { - BackgroundStyleApplicator.setBackgroundImage(view, null) - } - } - } - - @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) - public open fun nextFocusDown(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusDownId = viewId - } - - @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) - public open fun nextFocusForward(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusForwardId = viewId - } - - @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) - public open fun nextFocusLeft(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusLeftId = viewId - } - - @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) - public open fun nextFocusRight(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusRightId = viewId - } - - @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) - public open fun nextFocusUp(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusUpId = viewId - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_RADIUS, - ViewProps.BORDER_TOP_LEFT_RADIUS, - ViewProps.BORDER_TOP_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_LEFT_RADIUS, - ViewProps.BORDER_TOP_START_RADIUS, - ViewProps.BORDER_TOP_END_RADIUS, - ViewProps.BORDER_BOTTOM_START_RADIUS, - ViewProps.BORDER_BOTTOM_END_RADIUS, - ViewProps.BORDER_END_END_RADIUS, - ViewProps.BORDER_END_START_RADIUS, - ViewProps.BORDER_START_END_RADIUS, - ViewProps.BORDER_START_START_RADIUS]) - public open fun setBorderRadius( - view: ReactViewGroup, - index: Int, - rawBorderRadius: Dynamic - ): Unit { - var borderRadius = setFromDynamic(rawBorderRadius) - - // We do not support percentage border radii on Paper in order to be consistent with iOS (to - // avoid developer surprise if it works on one platform but not another). - if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC && - borderRadius?.type == LengthPercentageType.PERCENT) { - borderRadius = null - } - BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius) - } - - @Deprecated( - "Use setBorderRadius(ReactViewGroup, int, Dynamic) instead.", - ReplaceWith( - "setBorderRadius(view, index, DynamicFromObject(borderRadius))", - "com.facebook.react.bridge.DynamicFromObject.DynamicFromObject")) - public open fun setBorderRadius(view: ReactViewGroup, index: Int, borderRadius: Float): Unit { - this.setBorderRadius(view, index, DynamicFromObject(borderRadius)) - } - - @ReactProp(name = "borderStyle") - public open fun setBorderStyle(view: ReactViewGroup, borderStyle: String?): Unit { - val parsedBorderStyle = borderStyle?.let { BorderStyle.fromString(it) } - BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle) - } - - @ReactProp(name = "hitSlop") - public open fun setHitSlop(view: ReactViewGroup, hitSlop: Dynamic): Unit { - when (hitSlop.type) { - ReadableType.Map -> { - val hitSlopMap = hitSlop.asMap() - view.setHitSlopRect( - Rect( - getPixels(hitSlopMap, "left"), - getPixels(hitSlopMap, "top"), - getPixels(hitSlopMap, "right"), - getPixels(hitSlopMap, "bottom"))) - } - - ReadableType.Number -> { - val hitSlopValue = hitSlop.asDouble().dpToPx().toInt() - view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)) - } - - ReadableType.Null -> view.setHitSlopRect(null) - else -> { - FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}") - view.setHitSlopRect(null) - } - } - } - - private fun getPixels(map: ReadableMap, key: String): Int = - if (map.hasKey(key)) { - map.getDouble(key).dpToPx().toInt() - } else { - 0 - } - - @ReactProp(name = ViewProps.POINTER_EVENTS) - public open fun setPointerEvents(view: ReactViewGroup, pointerEventsStr: String?): Unit { - view.setPointerEvents(parsePointerEvents(pointerEventsStr)) - } - - @ReactProp(name = "nativeBackgroundAndroid") - public open fun setNativeBackground(view: ReactViewGroup, background: ReadableMap?): Unit { - val translucentBg = - background?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } - BackgroundStyleApplicator.setFeedbackUnderlay(view, translucentBg) - } - - @ReactProp(name = "nativeForegroundAndroid") - public open fun setNativeForeground(view: ReactViewGroup, foreground: ReadableMap?): Unit { - view.foreground = - foreground?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } - } - - @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) - public open fun setNeedsOffscreenAlphaCompositing( - view: ReactViewGroup, - needsOffscreenAlphaCompositing: Boolean - ): Unit { - view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_WIDTH, - ViewProps.BORDER_LEFT_WIDTH, - ViewProps.BORDER_RIGHT_WIDTH, - ViewProps.BORDER_TOP_WIDTH, - ViewProps.BORDER_BOTTOM_WIDTH, - ViewProps.BORDER_START_WIDTH, - ViewProps.BORDER_END_WIDTH], - defaultFloat = Float.NaN) - public open fun setBorderWidth(view: ReactViewGroup, index: Int, width: Float): Unit { - BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width) - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_COLOR, - ViewProps.BORDER_LEFT_COLOR, - ViewProps.BORDER_RIGHT_COLOR, - ViewProps.BORDER_TOP_COLOR, - ViewProps.BORDER_BOTTOM_COLOR, - ViewProps.BORDER_START_COLOR, - ViewProps.BORDER_END_COLOR, - ViewProps.BORDER_BLOCK_COLOR, - ViewProps.BORDER_BLOCK_END_COLOR, - ViewProps.BORDER_BLOCK_START_COLOR], - customType = "Color") - public open fun setBorderColor(view: ReactViewGroup, index: Int, color: Int?): Unit { - BackgroundStyleApplicator.setBorderColor( - view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color) - } - - @ReactProp(name = ViewProps.COLLAPSABLE) - @Suppress("UNUSED_PARAMETER") - public open fun setCollapsable(view: ReactViewGroup?, collapsable: Boolean): Unit { - // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually - // handled in NativeViewHierarchyOptimizer - } - - @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) - @Suppress("UNUSED_PARAMETER") - public open fun setCollapsableChildren( - view: ReactViewGroup?, - collapsableChildren: Boolean - ): Unit { - // no-op: it's here only so that "collapsableChildren" property is exported to JS. - } - - @ReactProp(name = "focusable") - public open fun setFocusable(view: ReactViewGroup, focusable: Boolean): Unit { - if (focusable) { - view.setOnClickListener { - val eventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag((view.context as ReactContext), view.id) - eventDispatcher?.dispatchEvent( - ViewGroupClickEvent(UIManagerHelper.getSurfaceId(view.context), view.id)) - } - - // Clickable elements are focusable. On API 26, this is taken care by setClickable. - // Explicitly calling setFocusable here for backward compatibility. - view.isFocusable = true - } else { - view.setOnClickListener(null) - view.isClickable = false - // Don't set view.setFocusable(false) because we might still want it to be focusable for - // accessibility reasons - } - } - - @ReactProp(name = ViewProps.OVERFLOW) - public open fun setOverflow(view: ReactViewGroup, overflow: String): Unit { - view.overflow = overflow - } - - @ReactProp(name = "backfaceVisibility") - public open fun setBackfaceVisibility(view: ReactViewGroup, backfaceVisibility: String): Unit { - view.setBackfaceVisibility(backfaceVisibility) - } - - override fun setOpacity(view: ReactViewGroup, opacity: Float) { - view.setOpacityIfPossible(opacity) - } - - override fun setTransformProperty( - view: ReactViewGroup, - transforms: ReadableArray?, - transformOrigin: ReadableArray? - ) { - super.setTransformProperty(view, transforms, transformOrigin) - view.setBackfaceVisibilityDependantOpacity() - } - - @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") - public open fun setBoxShadow(view: ReactViewGroup, shadows: ReadableArray?): Unit { - BackgroundStyleApplicator.setBoxShadow(view, shadows) - } - - override fun setBackgroundColor(view: ReactViewGroup, @ColorInt backgroundColor: Int) { - BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor) - } - - override fun getName(): String = REACT_CLASS - - public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = - ReactViewGroup(context) - - override fun getCommandsMap(): Map = - mapOf( - HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, - "setPressed" to CMD_SET_PRESSED, - ) - - @Deprecated("Deprecated in ViewManager") - override fun receiveCommand(root: ReactViewGroup, commandId: Int, args: ReadableArray?) { - when (commandId) { - CMD_HOTSPOT_UPDATE -> handleHotspotUpdate(root, args) - CMD_SET_PRESSED -> handleSetPressed(root, args) - else -> {} - } - } - - override fun receiveCommand(root: ReactViewGroup, commandId: String, args: ReadableArray?) { - when (commandId) { - HOTSPOT_UPDATE_KEY -> handleHotspotUpdate(root, args) - "setPressed" -> handleSetPressed(root, args) - else -> {} - } - } - - private fun handleSetPressed(root: ReactViewGroup, args: ReadableArray?) { - if (args?.size() != 1) { - throw JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'setPressed' command") - } - root.isPressed = args.getBoolean(0) - } - - private fun handleHotspotUpdate(root: ReactViewGroup, args: ReadableArray?) { - if (args?.size() != 2) { - throw JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'updateHotspot' command") - } - val x = args.getDouble(0).dpToPx() - val y = args.getDouble(1).dpToPx() - root.drawableHotspotChanged(x, y) - } - - public companion object { - public const val REACT_CLASS: String = ViewProps.VIEW_CLASS_NAME - private val SPACING_TYPES = - intArrayOf( - Spacing.ALL, - Spacing.LEFT, - Spacing.RIGHT, - Spacing.TOP, - Spacing.BOTTOM, - Spacing.START, - Spacing.END, - Spacing.BLOCK, - Spacing.BLOCK_END, - Spacing.BLOCK_START) - private const val CMD_HOTSPOT_UPDATE = 1 - private const val CMD_SET_PRESSED = 2 - private const val HOTSPOT_UPDATE_KEY = "hotspotUpdate" - } -}