Java tutorial
/* * Copyright (c) Facebook, Inc. and its 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.react.common.ReactConstants.TAG; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.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.react.bridge.ReactContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.modules.i18nmanager.I18nUtil; import com.facebook.react.touch.OnInterceptTouchEventListener; import com.facebook.react.touch.ReactHitSlopView; import com.facebook.react.touch.ReactInterceptingViewGroup; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.PointerEvents; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ReactPointerEventsView; import com.facebook.react.uimanager.ReactZIndexedViewGroup; import com.facebook.react.uimanager.RootView; import com.facebook.react.uimanager.RootViewUtil; import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaConstants; /** * Backing for a React View. Has support for borders, but since borders aren't common, lazy * initializes most of the storage needed for them. */ public class ReactViewGroup extends ViewGroup implements ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView, ReactZIndexedViewGroup { 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); /* should only be used in {@link #updateClippingToRect} */ private static final Rect sHelperRect = new Rect(); /** * This listener will be set for child views when removeClippedSubview property is enabled. When * children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent view * about that fact so that view can be attached/detached if necessary. * * <p>TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children * update their layout. */ private static final class ChildrenLayoutChangeListener implements View.OnLayoutChangeListener { private final ReactViewGroup mParent; private ChildrenLayoutChangeListener(ReactViewGroup parent) { mParent = parent; } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (mParent.getRemoveClippedSubviews()) { mParent.updateSubviewClipStatus(v); } } } // Following properties are here to support the option {@code removeClippedSubviews}. This is a // temporary optimization/hack that is mainly applicable to the large list of images. The way // it's implemented is that we store an additional array of children in view node. We selectively // remove some of the views (detach) from it while still storing them in that additional array. // We override all possible add methods for {@link ViewGroup} so that we can control this process // whenever the option is set. We also override {@link ViewGroup#getChildAt} and // {@link ViewGroup#getChildCount} so those methods may return views that are not attached. // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}. private boolean mRemoveClippedSubviews = false; private @Nullable View[] mAllChildren = null; private int mAllChildrenCount; private @Nullable Rect mClippingRect; private @Nullable Rect mHitSlopRect; private @Nullable String mOverflow; private PointerEvents mPointerEvents = PointerEvents.AUTO; private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; private boolean mNeedsOffscreenAlphaCompositing = false; private final ViewGroupDrawingOrderHelper mDrawingOrderHelper; private @Nullable Path mPath; private int mLayoutDirection; private float mBackfaceOpacity = 1.f; private String mBackfaceVisibility = "visible"; public ReactViewGroup(Context context) { super(context); setClipChildren(false); mDrawingOrderHelper = new ViewGroupDrawingOrderHelper(this); } @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 public void onRtlPropertiesChanged(int layoutDirection) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (mReactBackgroundDrawable != null) { mReactBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); } } } @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) { if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) { // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background } else { getOrCreateReactViewBackground().setColor(color); } } @Override public void setBackground(Drawable drawable) { throw new UnsupportedOperationException("This method is not supported for ReactViewGroup instances"); } public void setTranslucentBackgroundDrawable(@Nullable Drawable background) { // it's required to call setBackground to null, as in some of the cases we may set new // background to be a layer drawable that contains a drawable that has been previously setup // as a background previously. This will not work correctly as the drawable callback logic is // messed up in AOSP updateBackgroundDrawable(null); if (mReactBackgroundDrawable != null && background != null) { LayerDrawable layerDrawable = new LayerDrawable( new Drawable[] { mReactBackgroundDrawable, background }); updateBackgroundDrawable(layerDrawable); } else if (background != null) { updateBackgroundDrawable(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 (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) { 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 (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_NONE) { 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; } /** * 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) { getOrCreateReactViewBackground().setBorderWidth(position, width); } public void setBorderColor(int position, float rgb, float alpha) { getOrCreateReactViewBackground().setBorderColor(position, rgb, alpha); } public void setBorderRadius(float borderRadius) { ReactViewBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); backgroundDrawable.setRadius(borderRadius); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { final int UPDATED_LAYER_TYPE = backgroundDrawable.hasRoundedBorders() ? View.LAYER_TYPE_SOFTWARE : View.LAYER_TYPE_HARDWARE; if (UPDATED_LAYER_TYPE != getLayerType()) { setLayerType(UPDATED_LAYER_TYPE, null); } } } public void setBorderRadius(float borderRadius, int position) { ReactViewBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); backgroundDrawable.setRadius(borderRadius, position); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { final int UPDATED_LAYER_TYPE = backgroundDrawable.hasRoundedBorders() ? View.LAYER_TYPE_SOFTWARE : View.LAYER_TYPE_HARDWARE; if (UPDATED_LAYER_TYPE != getLayerType()) { setLayerType(UPDATED_LAYER_TYPE, null); } } } public void setBorderStyle(@Nullable String style) { getOrCreateReactViewBackground().setBorderStyle(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(mClippingRect); } @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. super.removeViewsInLayout(idx - clippedSoFar, 1); needUpdateClippingRecursive = true; } else if (intersects && child.getParent() == null) { super.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 ReactFeatureFlags.clipChildRectsIfOverflowIsHidden ? ReactClippingViewGroupHelper.getChildVisibleRectHelper(child, r, offset, this, mOverflow) : 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(); } } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { // This will get called for every overload of addView so there is not need to override every // method. mDrawingOrderHelper.handleAddView(child); setChildrenDrawingOrderEnabled(mDrawingOrderHelper.shouldEnableCustomDrawingOrder()); super.addView(child, index, params); } @Override public void removeView(View view) { UiThreadUtil.assertOnUiThread(); mDrawingOrderHelper.handleRemoveView(view); setChildrenDrawingOrderEnabled(mDrawingOrderHelper.shouldEnableCustomDrawingOrder()); super.removeView(view); } @Override public void removeViewAt(int index) { UiThreadUtil.assertOnUiThread(); mDrawingOrderHelper.handleRemoveView(getChildAt(index)); setChildrenDrawingOrderEnabled(mDrawingOrderHelper.shouldEnableCustomDrawingOrder()); super.removeViewAt(index); } @Override protected int getChildDrawingOrder(int childCount, int index) { return mDrawingOrderHelper.getChildDrawingOrder(childCount, index); } @Override public int getZIndexMappedChildIndex(int index) { if (mDrawingOrderHelper.shouldEnableCustomDrawingOrder()) { return mDrawingOrderHelper.getChildDrawingOrder(getChildCount(), index); } else { return index; } } @Override public void updateDrawingOrder() { mDrawingOrderHelper.update(); setChildrenDrawingOrderEnabled(mDrawingOrderHelper.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. } /*package*/ void setPointerEvents(PointerEvents pointerEvents) { mPointerEvents = pointerEvents; } /*package*/ int getAllChildrenCount() { return mAllChildrenCount; } /*package*/ View getChildAtWithSubviewClippingEnabled(int index) { return Assertions.assertNotNull(mAllChildren)[index]; } /*package*/ void addViewWithSubviewClippingEnabled(View child, int index) { addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam); } /*package*/ void addViewWithSubviewClippingEnabled(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); } /*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++; } } super.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); } } // This method also sets the child's mParent to null 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(); } } @VisibleForTesting public int getBackgroundColor() { if (getBackground() != null) { return ((ReactViewBackgroundDrawable) getBackground()).getColor(); } return DEFAULT_BACKGROUND_COLOR; } private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { if (mReactBackgroundDrawable == null) { mReactBackgroundDrawable = new ReactViewBackgroundDrawable(getContext()); Drawable backgroundDrawable = getBackground(); updateBackgroundDrawable(null); // required so that drawable callback is cleared before we add the // drawable back as a part of LayerDrawable if (backgroundDrawable == null) { updateBackgroundDrawable(mReactBackgroundDrawable); } else { LayerDrawable layerDrawable = new LayerDrawable( new Drawable[] { mReactBackgroundDrawable, backgroundDrawable }); updateBackgroundDrawable(layerDrawable); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { mLayoutDirection = I18nUtil.getInstance().isRTL(getContext()) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR; mReactBackgroundDrawable.setResolvedLayoutDirection(mLayoutDirection); } } return mReactBackgroundDrawable; } @Override public @Nullable Rect getHitSlopRect() { return mHitSlopRect; } public void setHitSlopRect(@Nullable Rect rect) { mHitSlopRect = rect; } public void setOverflow(String overflow) { mOverflow = overflow; invalidate(); } public @Nullable String getOverflow() { return mOverflow; } /** * 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 */ private void updateBackgroundDrawable(Drawable drawable) { super.setBackground(drawable); } @Override protected void dispatchDraw(Canvas canvas) { try { dispatchOverflowDraw(canvas); super.dispatchDraw(canvas); } catch (NullPointerException e) { FLog.e(TAG, "NullPointerException when executing ViewGroup.dispatchDraw method", e); } catch (StackOverflowError e) { // Adding special exception management for StackOverflowError for logging purposes. // This will be removed in the future. RootView rootView = RootViewUtil.getRootView(ReactViewGroup.this); if (rootView != null) { rootView.handleException(e); } else { if (getContext() instanceof ReactContext) { ReactContext reactContext = (ReactContext) getContext(); reactContext .handleException(new IllegalViewOperationException("StackOverflowException", this, e)); } else { throw e; } } } } private void dispatchOverflowDraw(Canvas canvas) { if (mOverflow != null) { switch (mOverflow) { case ViewProps.VISIBLE: if (mPath != null) { mPath.rewind(); } break; case ViewProps.HIDDEN: float left = 0f; float top = 0f; float right = getWidth(); float bottom = getHeight(); boolean hasClipPath = false; if (mReactBackgroundDrawable != null) { final RectF borderWidth = mReactBackgroundDrawable.getDirectionAwareBorderInsets(); if (borderWidth.top > 0 || borderWidth.left > 0 || borderWidth.bottom > 0 || borderWidth.right > 0) { left += borderWidth.left; top += borderWidth.top; right -= borderWidth.right; bottom -= borderWidth.bottom; } final float borderRadius = mReactBackgroundDrawable.getFullBorderRadius(); float topLeftBorderRadius = mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_LEFT); float topRightBorderRadius = mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_RIGHT); float bottomLeftBorderRadius = mReactBackgroundDrawable.getBorderRadiusOrDefaultTo(borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_LEFT); float bottomRightBorderRadius = mReactBackgroundDrawable.getBorderRadiusOrDefaultTo( borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_RIGHT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { final boolean isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL; float topStartBorderRadius = mReactBackgroundDrawable .getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_START); float topEndBorderRadius = mReactBackgroundDrawable .getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_END); float bottomStartBorderRadius = mReactBackgroundDrawable .getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_START); float bottomEndBorderRadius = mReactBackgroundDrawable .getBorderRadius(ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_END); if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(getContext())) { if (YogaConstants.isUndefined(topStartBorderRadius)) { topStartBorderRadius = topLeftBorderRadius; } if (YogaConstants.isUndefined(topEndBorderRadius)) { topEndBorderRadius = topRightBorderRadius; } if (YogaConstants.isUndefined(bottomStartBorderRadius)) { bottomStartBorderRadius = bottomLeftBorderRadius; } if (YogaConstants.isUndefined(bottomEndBorderRadius)) { bottomEndBorderRadius = bottomRightBorderRadius; } final float directionAwareTopLeftRadius = isRTL ? topEndBorderRadius : topStartBorderRadius; final float directionAwareTopRightRadius = isRTL ? topStartBorderRadius : topEndBorderRadius; final float directionAwareBottomLeftRadius = isRTL ? bottomEndBorderRadius : bottomStartBorderRadius; final float directionAwareBottomRightRadius = isRTL ? bottomStartBorderRadius : bottomEndBorderRadius; topLeftBorderRadius = directionAwareTopLeftRadius; topRightBorderRadius = directionAwareTopRightRadius; bottomLeftBorderRadius = directionAwareBottomLeftRadius; bottomRightBorderRadius = directionAwareBottomRightRadius; } else { final float directionAwareTopLeftRadius = isRTL ? topEndBorderRadius : topStartBorderRadius; final float directionAwareTopRightRadius = isRTL ? topStartBorderRadius : topEndBorderRadius; final float directionAwareBottomLeftRadius = isRTL ? bottomEndBorderRadius : bottomStartBorderRadius; final float directionAwareBottomRightRadius = isRTL ? bottomStartBorderRadius : bottomEndBorderRadius; if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) { topLeftBorderRadius = directionAwareTopLeftRadius; } if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) { topRightBorderRadius = directionAwareTopRightRadius; } if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) { bottomLeftBorderRadius = directionAwareBottomLeftRadius; } if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) { bottomRightBorderRadius = directionAwareBottomRightRadius; } } } if (topLeftBorderRadius > 0 || topRightBorderRadius > 0 || bottomRightBorderRadius > 0 || bottomLeftBorderRadius > 0) { if (mPath == null) { mPath = new Path(); } mPath.rewind(); mPath.addRoundRect(new RectF(left, top, right, bottom), new float[] { Math.max(topLeftBorderRadius - borderWidth.left, 0), Math.max(topLeftBorderRadius - borderWidth.top, 0), Math.max(topRightBorderRadius - borderWidth.right, 0), Math.max(topRightBorderRadius - borderWidth.top, 0), Math.max(bottomRightBorderRadius - borderWidth.right, 0), Math.max(bottomRightBorderRadius - borderWidth.bottom, 0), Math.max(bottomLeftBorderRadius - borderWidth.left, 0), Math.max(bottomLeftBorderRadius - borderWidth.bottom, 0), }, Path.Direction.CW); canvas.clipPath(mPath); hasClipPath = true; } } if (!hasClipPath) { canvas.clipRect(new RectF(left, top, right, bottom)); } break; default: break; } } } 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); } }