diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 888350fcf09e..17a5d0e030ad 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5395,6 +5395,53 @@ public abstract interface class com/facebook/react/uimanager/debug/NotThreadSafe public abstract fun onViewHierarchyUpdateFinished ()V } +public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : android/graphics/drawable/Drawable { + public fun (Landroid/content/Context;)V + public fun borderBoxPath ()Landroid/graphics/Path; + public fun draw (Landroid/graphics/Canvas;)V + public fun getAlpha ()I + public fun getBorderColor (I)I + public fun getBorderRadius (Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation;)F + public fun getBorderRadiusOrDefaultTo (FLcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation;)F + public fun getBorderWidthOrDefaultTo (FI)F + public fun getDirectionAwareBorderInsets ()Landroid/graphics/RectF; + public fun getFullBorderRadius ()F + public fun getFullBorderWidth ()F + public fun getOpacity ()I + public fun getOutline (Landroid/graphics/Outline;)V + public fun getResolvedLayoutDirection ()I + public fun hasRoundedBorders ()Z + protected fun onBoundsChange (Landroid/graphics/Rect;)V + public fun onResolvedLayoutDirectionChanged (I)Z + public fun paddingBoxPath ()Landroid/graphics/Path; + public fun setAlpha (I)V + public fun setBorderColor (IFF)V + public fun setBorderStyle (Ljava/lang/String;)V + public fun setBorderWidth (IF)V + public fun setColor (I)V + public fun setColorFilter (Landroid/graphics/ColorFilter;)V + public fun setRadius (F)V + public fun setRadius (FI)V + public fun setResolvedLayoutDirection (I)Z +} + +public final class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation : java/lang/Enum { + public static final field BOTTOM_END Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field BOTTOM_LEFT Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field BOTTOM_RIGHT Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field BOTTOM_START Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field END_END Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field END_START Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field START_END Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field START_START Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field TOP_END Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field TOP_LEFT Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field TOP_RIGHT Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static final field TOP_START Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; + public static fun values ()[Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable$BorderRadiusLocation; +} + public abstract interface class com/facebook/react/uimanager/events/BatchEventDispatchedListener { public abstract fun onBatchEventDispatched ()V } @@ -7509,8 +7556,6 @@ public class com/facebook/react/views/unimplementedview/ReactUnimplementedViewMa public class com/facebook/react/views/view/ColorUtil { public fun ()V - public static fun getOpacityFromColor (I)I - public static fun multiplyColorAlpha (II)I public static fun normalize (DDDD)I } @@ -7543,49 +7588,8 @@ public class com/facebook/react/views/view/ReactDrawableHelper { public static fun createDrawableFromJSDescription (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;)Landroid/graphics/drawable/Drawable; } -public class com/facebook/react/views/view/ReactViewBackgroundDrawable : android/graphics/drawable/Drawable { +public class com/facebook/react/views/view/ReactViewBackgroundDrawable : com/facebook/react/uimanager/drawable/CSSBackgroundDrawable { public fun (Landroid/content/Context;)V - public fun draw (Landroid/graphics/Canvas;)V - public fun getAlpha ()I - public fun getBorderColor (I)I - public fun getBorderRadius (Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation;)F - public fun getBorderRadiusOrDefaultTo (FLcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation;)F - public fun getBorderWidthOrDefaultTo (FI)F - public fun getDirectionAwareBorderInsets ()Landroid/graphics/RectF; - public fun getFullBorderRadius ()F - public fun getFullBorderWidth ()F - public fun getOpacity ()I - public fun getOutline (Landroid/graphics/Outline;)V - public fun getResolvedLayoutDirection ()I - public fun hasRoundedBorders ()Z - protected fun onBoundsChange (Landroid/graphics/Rect;)V - public fun onResolvedLayoutDirectionChanged (I)Z - public fun setAlpha (I)V - public fun setBorderColor (IFF)V - public fun setBorderStyle (Ljava/lang/String;)V - public fun setBorderWidth (IF)V - public fun setColor (I)V - public fun setColorFilter (Landroid/graphics/ColorFilter;)V - public fun setRadius (F)V - public fun setRadius (FI)V - public fun setResolvedLayoutDirection (I)Z -} - -public final class com/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation : java/lang/Enum { - public static final field BOTTOM_END Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field BOTTOM_LEFT Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field BOTTOM_RIGHT Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field BOTTOM_START Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field END_END Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field END_START Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field START_END Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field START_START Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field TOP_END Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field TOP_LEFT Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field TOP_RIGHT Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static final field TOP_START Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; - public static fun values ()[Lcom/facebook/react/views/view/ReactViewBackgroundDrawable$BorderRadiusLocation; } public class com/facebook/react/views/view/ReactViewBackgroundManager { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java new file mode 100644 index 000000000000..83d598d97671 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java @@ -0,0 +1,1458 @@ +/* + * 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.uimanager.drawable; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.view.View; +import androidx.annotation.Nullable; +import androidx.core.graphics.ColorUtils; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.modules.i18nmanager.I18nUtil; +import com.facebook.react.uimanager.FloatUtil; +import com.facebook.react.uimanager.Spacing; +import java.util.Arrays; +import java.util.Locale; + +/** + * A subclass of {@link Drawable} used for background of {@link + * com.facebook.react.views.view.ReactViewGroup}. It supports drawing background color and borders + * (including rounded borders) by providing a react friendly API (setter for each of those + * properties). + * + *

The implementation tries to allocate as few objects as possible depending on which properties + * are set. E.g. for views with rounded background/borders we allocate {@code + * mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view + * have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only + * background color is set we won't allocate any extra/unnecessary objects. + */ +public class CSSBackgroundDrawable extends Drawable { + + private static final int DEFAULT_BORDER_COLOR = Color.BLACK; + private static final int DEFAULT_BORDER_RGB = 0x00FFFFFF & DEFAULT_BORDER_COLOR; + private static final int DEFAULT_BORDER_ALPHA = (0xFF000000 & DEFAULT_BORDER_COLOR) >>> 24; + // ~0 == 0xFFFFFFFF, all bits set to 1. + private static final int ALL_BITS_SET = ~0; + // 0 == 0x00000000, all bits set to 0. + private static final int ALL_BITS_UNSET = 0; + + private enum BorderStyle { + SOLID, + DASHED, + DOTTED; + + public static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { + switch (style) { + case SOLID: + return null; + + case DASHED: + return new DashPathEffect( + new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); + + case DOTTED: + return new DashPathEffect( + new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); + + default: + return null; + } + } + }; + + /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ + private @Nullable Spacing mBorderWidth; + private @Nullable Spacing mBorderRGB; + private @Nullable Spacing mBorderAlpha; + private @Nullable BorderStyle mBorderStyle; + + private @Nullable Path mInnerClipPathForBorderRadius; + private @Nullable Path mBackgroundColorRenderPath; + private @Nullable Path mOuterClipPathForBorderRadius; + private @Nullable Path mPathForBorderRadiusOutline; + private @Nullable Path mPathForBorder; + private final Path mPathForSingleBorder = new Path(); + private @Nullable Path mCenterDrawPath; + private @Nullable RectF mInnerClipTempRectForBorderRadius; + private @Nullable RectF mOuterClipTempRectForBorderRadius; + private @Nullable RectF mTempRectForBorderRadiusOutline; + private @Nullable RectF mTempRectForCenterDrawPath; + private @Nullable PointF mInnerTopLeftCorner; + private @Nullable PointF mInnerTopRightCorner; + private @Nullable PointF mInnerBottomRightCorner; + private @Nullable PointF mInnerBottomLeftCorner; + private boolean mNeedUpdatePathForBorderRadius = false; + private float mBorderRadius = Float.NaN; + + /* Used by all types of background and for drawing borders */ + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int mColor = Color.TRANSPARENT; + private int mAlpha = 255; + + // There is a small gap between the edges of adjacent paths + // such as between the mBackgroundColorRenderPath and its border. + // The smallest amount (found to be 0.8f) is used to extend + // the paths, overlapping them and closing the visible gap. + private final float mGapBetweenPaths = 0.8f; + + private @Nullable float[] mBorderCornerRadii; + private final Context mContext; + private int mLayoutDirection; + + public enum BorderRadiusLocation { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_RIGHT, + BOTTOM_LEFT, + TOP_START, + TOP_END, + BOTTOM_START, + BOTTOM_END, + END_END, + END_START, + START_END, + START_START + } + + public CSSBackgroundDrawable(Context context) { + mContext = context; + } + + @Override + public void draw(Canvas canvas) { + updatePathEffect(); + if (!hasRoundedBorders()) { + drawRectangularBackgroundWithBorders(canvas); + } else { + drawRoundedBackgroundWithBorders(canvas); + } + } + + public boolean hasRoundedBorders() { + if (!Float.isNaN(mBorderRadius) && mBorderRadius > 0) { + return true; + } + + if (mBorderCornerRadii != null) { + for (final float borderRadii : mBorderCornerRadii) { + if (!Float.isNaN(borderRadii) && borderRadii > 0) { + return true; + } + } + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mNeedUpdatePathForBorderRadius = true; + } + + @Override + public void setAlpha(int alpha) { + if (alpha != mAlpha) { + mAlpha = alpha; + invalidateSelf(); + } + } + + @Override + public int getAlpha() { + return mAlpha; + } + + @Override + public void setColorFilter(ColorFilter cf) { + // do nothing + } + + @Override + public int getOpacity() { + return (Color.alpha(mColor) * mAlpha) >> 8; + } + + /* Android's elevation implementation requires this to be implemented to know where to draw the shadow. */ + @Override + public void getOutline(Outline outline) { + if ((!Float.isNaN(mBorderRadius) && mBorderRadius > 0) || mBorderCornerRadii != null) { + updatePath(); + + outline.setConvexPath(mPathForBorderRadiusOutline); + } else { + outline.setRect(getBounds()); + } + } + + public void setBorderWidth(int position, float width) { + if (mBorderWidth == null) { + mBorderWidth = new Spacing(); + } + if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { + mBorderWidth.set(position, width); + switch (position) { + case Spacing.ALL: + case Spacing.LEFT: + case Spacing.BOTTOM: + case Spacing.RIGHT: + case Spacing.TOP: + case Spacing.START: + case Spacing.END: + mNeedUpdatePathForBorderRadius = true; + } + invalidateSelf(); + } + } + + public void setBorderColor(int position, float rgb, float alpha) { + this.setBorderRGB(position, rgb); + this.setBorderAlpha(position, alpha); + mNeedUpdatePathForBorderRadius = true; + } + + private void setBorderRGB(int position, float rgb) { + // set RGB component + if (mBorderRGB == null) { + mBorderRGB = new Spacing(DEFAULT_BORDER_RGB); + } + if (!FloatUtil.floatsEqual(mBorderRGB.getRaw(position), rgb)) { + mBorderRGB.set(position, rgb); + invalidateSelf(); + } + } + + private void setBorderAlpha(int position, float alpha) { + // set Alpha component + if (mBorderAlpha == null) { + mBorderAlpha = new Spacing(DEFAULT_BORDER_ALPHA); + } + if (!FloatUtil.floatsEqual(mBorderAlpha.getRaw(position), alpha)) { + mBorderAlpha.set(position, alpha); + invalidateSelf(); + } + } + + public void setBorderStyle(@Nullable String style) { + BorderStyle borderStyle = + style == null ? null : BorderStyle.valueOf(style.toUpperCase(Locale.US)); + if (mBorderStyle != borderStyle) { + mBorderStyle = borderStyle; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setRadius(float radius) { + if (!FloatUtil.floatsEqual(mBorderRadius, radius)) { + mBorderRadius = radius; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setRadius(float radius, int position) { + if (mBorderCornerRadii == null) { + mBorderCornerRadii = new float[12]; + Arrays.fill(mBorderCornerRadii, Float.NaN); + } + + if (!FloatUtil.floatsEqual(mBorderCornerRadii[position], radius)) { + mBorderCornerRadii[position] = radius; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public float getFullBorderRadius() { + return Float.isNaN(mBorderRadius) ? 0 : mBorderRadius; + } + + public float getBorderRadius(final BorderRadiusLocation location) { + return getBorderRadiusOrDefaultTo(Float.NaN, location); + } + + public float getBorderRadiusOrDefaultTo( + final float defaultValue, final BorderRadiusLocation location) { + if (mBorderCornerRadii == null) { + return defaultValue; + } + + final float radius = mBorderCornerRadii[location.ordinal()]; + + if (Float.isNaN(radius)) { + return defaultValue; + } + + return radius; + } + + public void setColor(int color) { + mColor = color; + invalidateSelf(); + } + + /** Similar to Drawable.getLayoutDirection, but available in APIs < 23. */ + public int getResolvedLayoutDirection() { + return mLayoutDirection; + } + + /** Similar to Drawable.setLayoutDirection, but available in APIs < 23. */ + public boolean setResolvedLayoutDirection(int layoutDirection) { + if (mLayoutDirection != layoutDirection) { + mLayoutDirection = layoutDirection; + return onResolvedLayoutDirectionChanged(layoutDirection); + } + return false; + } + + /** Similar to Drawable.onLayoutDirectionChanged, but available in APIs < 23. */ + public boolean onResolvedLayoutDirectionChanged(int layoutDirection) { + return false; + } + + @VisibleForTesting + public int getColor() { + return mColor; + } + + public Path borderBoxPath() { + updatePath(); + return mOuterClipPathForBorderRadius; + } + + public Path paddingBoxPath() { + updatePath(); + return mInnerClipPathForBorderRadius; + } + + private void drawRoundedBackgroundWithBorders(Canvas canvas) { + updatePath(); + canvas.save(); + + // Clip outer border + canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); + + // Draws the View without its border first (with background color fill) + int useColor = ColorUtils.setAlphaComponent(mColor, getOpacity()); + if (Color.alpha(useColor) != 0) { // color is not transparent + mPaint.setColor(useColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(mBackgroundColorRenderPath, mPaint); + } + + final RectF borderWidth = getDirectionAwareBorderInsets(); + int colorLeft = getBorderColor(Spacing.LEFT); + int colorTop = getBorderColor(Spacing.TOP); + int colorRight = getBorderColor(Spacing.RIGHT); + int colorBottom = getBorderColor(Spacing.BOTTOM); + + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + if (borderWidth.top > 0 + || borderWidth.bottom > 0 + || borderWidth.left > 0 + || borderWidth.right > 0) { + + // If it's a full and even border draw inner rect path with stroke + final float fullBorderWidth = getFullBorderWidth(); + int borderColor = getBorderColor(Spacing.ALL); + if (borderWidth.top == fullBorderWidth + && borderWidth.bottom == fullBorderWidth + && borderWidth.left == fullBorderWidth + && borderWidth.right == fullBorderWidth + && colorLeft == borderColor + && colorTop == borderColor + && colorRight == borderColor + && colorBottom == borderColor) { + if (fullBorderWidth > 0) { + mPaint.setColor(multiplyColorAlpha(borderColor, mAlpha)); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(fullBorderWidth); + canvas.drawPath(mCenterDrawPath, mPaint); + } + } + // In the case of uneven border widths/colors draw quadrilateral in each direction + else { + mPaint.setStyle(Paint.Style.FILL); + + // Clip inner border + canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + int colorStart = getBorderColor(Spacing.START); + int colorEnd = getBorderColor(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (!isBorderColorDefined(Spacing.START)) { + colorStart = colorLeft; + } + + if (!isBorderColorDefined(Spacing.END)) { + colorEnd = colorRight; + } + + final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final int directionAwareColorRight = isRTL ? colorStart : colorEnd; + + colorLeft = directionAwareColorLeft; + colorRight = directionAwareColorRight; + } else { + final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final int directionAwareColorRight = isRTL ? colorStart : colorEnd; + + final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); + final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); + final boolean isDirectionAwareColorLeftDefined = + isRTL ? isColorEndDefined : isColorStartDefined; + final boolean isDirectionAwareColorRightDefined = + isRTL ? isColorStartDefined : isColorEndDefined; + + if (isDirectionAwareColorLeftDefined) { + colorLeft = directionAwareColorLeft; + } + + if (isDirectionAwareColorRightDefined) { + colorRight = directionAwareColorRight; + } + } + + final float left = mOuterClipTempRectForBorderRadius.left; + final float right = mOuterClipTempRectForBorderRadius.right; + final float top = mOuterClipTempRectForBorderRadius.top; + final float bottom = mOuterClipTempRectForBorderRadius.bottom; + + // mGapBetweenPaths is used to close the gap between the diagonal + // edges of the quadrilaterals on adjacent sides of the rectangle + if (borderWidth.left > 0) { + final float x1 = left; + final float y1 = top - mGapBetweenPaths; + final float x2 = mInnerTopLeftCorner.x; + final float y2 = mInnerTopLeftCorner.y - mGapBetweenPaths; + final float x3 = mInnerBottomLeftCorner.x; + final float y3 = mInnerBottomLeftCorner.y + mGapBetweenPaths; + final float x4 = left; + final float y4 = bottom + mGapBetweenPaths; + + drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.top > 0) { + final float x1 = left - mGapBetweenPaths; + final float y1 = top; + final float x2 = mInnerTopLeftCorner.x - mGapBetweenPaths; + final float y2 = mInnerTopLeftCorner.y; + final float x3 = mInnerTopRightCorner.x + mGapBetweenPaths; + final float y3 = mInnerTopRightCorner.y; + final float x4 = right + mGapBetweenPaths; + final float y4 = top; + + drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.right > 0) { + final float x1 = right; + final float y1 = top - mGapBetweenPaths; + final float x2 = mInnerTopRightCorner.x; + final float y2 = mInnerTopRightCorner.y - mGapBetweenPaths; + final float x3 = mInnerBottomRightCorner.x; + final float y3 = mInnerBottomRightCorner.y + mGapBetweenPaths; + final float x4 = right; + final float y4 = bottom + mGapBetweenPaths; + + drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.bottom > 0) { + final float x1 = left - mGapBetweenPaths; + final float y1 = bottom; + final float x2 = mInnerBottomLeftCorner.x - mGapBetweenPaths; + final float y2 = mInnerBottomLeftCorner.y; + final float x3 = mInnerBottomRightCorner.x + mGapBetweenPaths; + final float y3 = mInnerBottomRightCorner.y; + final float x4 = right + mGapBetweenPaths; + final float y4 = bottom; + + drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); + } + } + } + + canvas.restore(); + } + + private void updatePath() { + if (!mNeedUpdatePathForBorderRadius) { + return; + } + + mNeedUpdatePathForBorderRadius = false; + + if (mInnerClipPathForBorderRadius == null) { + mInnerClipPathForBorderRadius = new Path(); + } + + if (mBackgroundColorRenderPath == null) { + mBackgroundColorRenderPath = new Path(); + } + + if (mOuterClipPathForBorderRadius == null) { + mOuterClipPathForBorderRadius = new Path(); + } + + if (mPathForBorderRadiusOutline == null) { + mPathForBorderRadiusOutline = new Path(); + } + + if (mCenterDrawPath == null) { + mCenterDrawPath = new Path(); + } + + if (mInnerClipTempRectForBorderRadius == null) { + mInnerClipTempRectForBorderRadius = new RectF(); + } + + if (mOuterClipTempRectForBorderRadius == null) { + mOuterClipTempRectForBorderRadius = new RectF(); + } + + if (mTempRectForBorderRadiusOutline == null) { + mTempRectForBorderRadiusOutline = new RectF(); + } + + if (mTempRectForCenterDrawPath == null) { + mTempRectForCenterDrawPath = new RectF(); + } + + mInnerClipPathForBorderRadius.reset(); + mBackgroundColorRenderPath.reset(); + mOuterClipPathForBorderRadius.reset(); + mPathForBorderRadiusOutline.reset(); + mCenterDrawPath.reset(); + + mInnerClipTempRectForBorderRadius.set(getBounds()); + mOuterClipTempRectForBorderRadius.set(getBounds()); + mTempRectForBorderRadiusOutline.set(getBounds()); + mTempRectForCenterDrawPath.set(getBounds()); + + final RectF borderWidth = getDirectionAwareBorderInsets(); + + int colorLeft = getBorderColor(Spacing.LEFT); + int colorTop = getBorderColor(Spacing.TOP); + int colorRight = getBorderColor(Spacing.RIGHT); + int colorBottom = getBorderColor(Spacing.BOTTOM); + int borderColor = getBorderColor(Spacing.ALL); + + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + // Clip border ONLY if its color is non transparent + if (Color.alpha(colorLeft) != 0 + && Color.alpha(colorTop) != 0 + && Color.alpha(colorRight) != 0 + && Color.alpha(colorBottom) != 0 + && Color.alpha(borderColor) != 0) { + + mInnerClipTempRectForBorderRadius.top += borderWidth.top; + mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; + mInnerClipTempRectForBorderRadius.left += borderWidth.left; + mInnerClipTempRectForBorderRadius.right -= borderWidth.right; + } + + mTempRectForCenterDrawPath.top += borderWidth.top * 0.5f; + mTempRectForCenterDrawPath.bottom -= borderWidth.bottom * 0.5f; + mTempRectForCenterDrawPath.left += borderWidth.left * 0.5f; + mTempRectForCenterDrawPath.right -= borderWidth.right * 0.5f; + + final float borderRadius = getFullBorderRadius(); + float topLeftRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_LEFT); + float topRightRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_RIGHT); + float bottomLeftRadius = + getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_LEFT); + float bottomRightRadius = + getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT); + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + float topStartRadius = getBorderRadius(BorderRadiusLocation.TOP_START); + float topEndRadius = getBorderRadius(BorderRadiusLocation.TOP_END); + float bottomStartRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_START); + float bottomEndRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_END); + + float endEndRadius = getBorderRadius(BorderRadiusLocation.END_END); + float endStartRadius = getBorderRadius(BorderRadiusLocation.END_START); + float startEndRadius = getBorderRadius(BorderRadiusLocation.START_END); + float startStartRadius = getBorderRadius(BorderRadiusLocation.START_START); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (Float.isNaN(topStartRadius)) { + topStartRadius = topLeftRadius; + } + + if (Float.isNaN(topEndRadius)) { + topEndRadius = topRightRadius; + } + + if (Float.isNaN(bottomStartRadius)) { + bottomStartRadius = bottomLeftRadius; + } + + if (Float.isNaN(bottomEndRadius)) { + bottomEndRadius = bottomRightRadius; + } + + final float logicalTopStartRadius = + Float.isNaN(topStartRadius) ? startStartRadius : topStartRadius; + final float logicalTopEndRadius = Float.isNaN(topEndRadius) ? startEndRadius : topEndRadius; + final float logicalBottomStartRadius = + Float.isNaN(bottomStartRadius) ? endStartRadius : bottomStartRadius; + final float logicalBottomEndRadius = + Float.isNaN(bottomEndRadius) ? endEndRadius : bottomEndRadius; + + final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; + final float directionAwareTopRightRadius = + isRTL ? logicalTopStartRadius : logicalTopEndRadius; + final float directionAwareBottomLeftRadius = + isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; + final float directionAwareBottomRightRadius = + isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; + + topLeftRadius = directionAwareTopLeftRadius; + topRightRadius = directionAwareTopRightRadius; + bottomLeftRadius = directionAwareBottomLeftRadius; + bottomRightRadius = directionAwareBottomRightRadius; + } else { + final float logicalTopStartRadius = + Float.isNaN(topStartRadius) ? startStartRadius : topStartRadius; + final float logicalTopEndRadius = Float.isNaN(topEndRadius) ? startEndRadius : topEndRadius; + final float logicalBottomStartRadius = + Float.isNaN(bottomStartRadius) ? endStartRadius : bottomStartRadius; + final float logicalBottomEndRadius = + Float.isNaN(bottomEndRadius) ? endEndRadius : bottomEndRadius; + + final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; + final float directionAwareTopRightRadius = + isRTL ? logicalTopStartRadius : logicalTopEndRadius; + final float directionAwareBottomLeftRadius = + isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; + final float directionAwareBottomRightRadius = + isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; + + if (!Float.isNaN(directionAwareTopLeftRadius)) { + topLeftRadius = directionAwareTopLeftRadius; + } + + if (!Float.isNaN(directionAwareTopRightRadius)) { + topRightRadius = directionAwareTopRightRadius; + } + + if (!Float.isNaN(directionAwareBottomLeftRadius)) { + bottomLeftRadius = directionAwareBottomLeftRadius; + } + + if (!Float.isNaN(directionAwareBottomRightRadius)) { + bottomRightRadius = directionAwareBottomRightRadius; + } + } + + final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderWidth.left, 0); + final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderWidth.top, 0); + final float innerTopRightRadiusX = Math.max(topRightRadius - borderWidth.right, 0); + final float innerTopRightRadiusY = Math.max(topRightRadius - borderWidth.top, 0); + final float innerBottomRightRadiusX = Math.max(bottomRightRadius - borderWidth.right, 0); + final float innerBottomRightRadiusY = Math.max(bottomRightRadius - borderWidth.bottom, 0); + final float innerBottomLeftRadiusX = Math.max(bottomLeftRadius - borderWidth.left, 0); + final float innerBottomLeftRadiusY = Math.max(bottomLeftRadius - borderWidth.bottom, 0); + + mInnerClipPathForBorderRadius.addRoundRect( + mInnerClipTempRectForBorderRadius, + new float[] { + innerTopLeftRadiusX, + innerTopLeftRadiusY, + innerTopRightRadiusX, + innerTopRightRadiusY, + innerBottomRightRadiusX, + innerBottomRightRadiusY, + innerBottomLeftRadiusX, + innerBottomLeftRadiusY, + }, + Path.Direction.CW); + + // There is a small gap between mBackgroundColorRenderPath and its + // border. mGapBetweenPaths is used to slightly enlarge the rectangle + // (mInnerClipTempRectForBorderRadius), ensuring the border can be + // drawn on top without the gap. + mBackgroundColorRenderPath.addRoundRect( + mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths, + new float[] { + innerTopLeftRadiusX, + innerTopLeftRadiusY, + innerTopRightRadiusX, + innerTopRightRadiusY, + innerBottomRightRadiusX, + innerBottomRightRadiusY, + innerBottomLeftRadiusX, + innerBottomLeftRadiusY, + }, + Path.Direction.CW); + + mOuterClipPathForBorderRadius.addRoundRect( + mOuterClipTempRectForBorderRadius, + new float[] { + topLeftRadius, + topLeftRadius, + topRightRadius, + topRightRadius, + bottomRightRadius, + bottomRightRadius, + bottomLeftRadius, + bottomLeftRadius + }, + Path.Direction.CW); + + float extraRadiusForOutline = 0; + + if (mBorderWidth != null) { + extraRadiusForOutline = mBorderWidth.get(Spacing.ALL) / 2f; + } + + mPathForBorderRadiusOutline.addRoundRect( + mTempRectForBorderRadiusOutline, + new float[] { + topLeftRadius + extraRadiusForOutline, + topLeftRadius + extraRadiusForOutline, + topRightRadius + extraRadiusForOutline, + topRightRadius + extraRadiusForOutline, + bottomRightRadius + extraRadiusForOutline, + bottomRightRadius + extraRadiusForOutline, + bottomLeftRadius + extraRadiusForOutline, + bottomLeftRadius + extraRadiusForOutline + }, + Path.Direction.CW); + + mCenterDrawPath.addRoundRect( + mTempRectForCenterDrawPath, + new float[] { + Math.max( + topLeftRadius - borderWidth.left * 0.5f, + (borderWidth.left > 0.0f) ? (topLeftRadius / borderWidth.left) : 0.0f), + Math.max( + topLeftRadius - borderWidth.top * 0.5f, + (borderWidth.top > 0.0f) ? (topLeftRadius / borderWidth.top) : 0.0f), + Math.max( + topRightRadius - borderWidth.right * 0.5f, + (borderWidth.right > 0.0f) ? (topRightRadius / borderWidth.right) : 0.0f), + Math.max( + topRightRadius - borderWidth.top * 0.5f, + (borderWidth.top > 0.0f) ? (topRightRadius / borderWidth.top) : 0.0f), + Math.max( + bottomRightRadius - borderWidth.right * 0.5f, + (borderWidth.right > 0.0f) ? (bottomRightRadius / borderWidth.right) : 0.0f), + Math.max( + bottomRightRadius - borderWidth.bottom * 0.5f, + (borderWidth.bottom > 0.0f) ? (bottomRightRadius / borderWidth.bottom) : 0.0f), + Math.max( + bottomLeftRadius - borderWidth.left * 0.5f, + (borderWidth.left > 0.0f) ? (bottomLeftRadius / borderWidth.left) : 0.0f), + Math.max( + bottomLeftRadius - borderWidth.bottom * 0.5f, + (borderWidth.bottom > 0.0f) ? (bottomLeftRadius / borderWidth.bottom) : 0.0f) + }, + Path.Direction.CW); + + /** + * Rounded Multi-Colored Border Algorithm: + * + *

Let O (for outer) = (top, left, bottom, right) be the rectangle that represents the size + * and position of a view V. Since the box-sizing of all React Native views is border-box, any + * border of V will render inside O. + * + *

Let BorderWidth = (borderTop, borderLeft, borderBottom, borderRight). + * + *

Let I (for inner) = O - BorderWidth. + * + *

Then, remembering that O and I are rectangles and that I is inside O, O - I gives us the + * border of V. Therefore, we can use canvas.clipPath to draw V's border. + * + *

canvas.clipPath(O, Region.OP.INTERSECT); + * + *

canvas.clipPath(I, Region.OP.DIFFERENCE); + * + *

canvas.drawRect(O, paint); + * + *

This lets us draw non-rounded single-color borders. + * + *

To extend this algorithm to rounded single-color borders, we: + * + *

1. Curve the corners of O by the (border radii of V) using Path#addRoundRect. + * + *

2. Curve the corners of I by (border radii of V - border widths of V) using + * Path#addRoundRect. + * + *

Let O' = curve(O, border radii of V). + * + *

Let I' = curve(I, border radii of V - border widths of V) + * + *

The rationale behind this decision is the (first sentence of the) following section in the + * CSS Backgrounds and Borders Module Level 3: + * https://www.w3.org/TR/css3-background/#the-border-radius. + * + *

After both O and I have been curved, we can execute the following lines once again to + * render curved single-color borders: + * + *

canvas.clipPath(O, Region.OP.INTERSECT); + * + *

canvas.clipPath(I, Region.OP.DIFFERENCE); + * + *

canvas.drawRect(O, paint); + * + *

To extend this algorithm to rendering multi-colored rounded borders, we render each side + * of the border as its own quadrilateral. Suppose that we were handling the case where all the + * border radii are 0. Then, the four quadrilaterals would be: + * + *

Left: (O.left, O.top), (I.left, I.top), (I.left, I.bottom), (O.left, O.bottom) + * + *

Top: (O.left, O.top), (I.left, I.top), (I.right, I.top), (O.right, O.top) + * + *

Right: (O.right, O.top), (I.right, I.top), (I.right, I.bottom), (O.right, O.bottom) + * + *

Bottom: (O.right, O.bottom), (I.right, I.bottom), (I.left, I.bottom), (O.left, O.bottom) + * + *

Now, lets consider what happens when we render a rounded border (radii != 0). For the sake + * of simplicity, let's focus on the top edge of the Left border: + * + *

Let borderTopLeftRadius = 5. Let borderLeftWidth = 1. Let borderTopWidth = 2. + * + *

We know that O is curved by the ellipse E_O (a = 5, b = 5). We know that I is curved by + * the ellipse E_I (a = 5 - 1, b = 5 - 2). + * + *

Since we have clipping, it should be safe to set the top-left point of the Left + * quadrilateral's top edge to (O.left, O.top). + * + *

But, what should the top-right point be? + * + *

The fact that the border is curved shouldn't change the slope (nor the position) of the + * line connecting the top-left and top-right points of the Left quadrilateral's top edge. + * Therefore, The top-right point should lie somewhere on the line L = (1 - a) * (O.left, O.top) + * + a * (I.left, I.top). + * + *

a != 0, because then the top-left and top-right points would be the same and + * borderLeftWidth = 1. a != 1, because then the top-right point would not touch an edge of the + * ellipse E_I. We want the top-right point to touch an edge of the inner ellipse because the + * border curves with E_I on the top-left corner of V. + * + *

Therefore, it must be the case that a > 1. Two natural locations of the top-right point + * exist: 1. The first intersection of L with E_I. 2. The second intersection of L with E_I. + * + *

We choose the top-right point of the top edge of the Left quadrilateral to be an arbitrary + * intersection of L with E_I. + */ + if (mInnerTopLeftCorner == null) { + mInnerTopLeftCorner = new PointF(); + } + + /** Compute mInnerTopLeftCorner */ + mInnerTopLeftCorner.x = mInnerClipTempRectForBorderRadius.left; + mInnerTopLeftCorner.y = mInnerClipTempRectForBorderRadius.top; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.top, + mInnerClipTempRectForBorderRadius.left + 2 * innerTopLeftRadiusX, + mInnerClipTempRectForBorderRadius.top + 2 * innerTopLeftRadiusY, + + // Line Start + mOuterClipTempRectForBorderRadius.left, + mOuterClipTempRectForBorderRadius.top, + + // Line End + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.top, + + // Result + mInnerTopLeftCorner); + + /** Compute mInnerBottomLeftCorner */ + if (mInnerBottomLeftCorner == null) { + mInnerBottomLeftCorner = new PointF(); + } + + mInnerBottomLeftCorner.x = mInnerClipTempRectForBorderRadius.left; + mInnerBottomLeftCorner.y = mInnerClipTempRectForBorderRadius.bottom; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomLeftRadiusY, + mInnerClipTempRectForBorderRadius.left + 2 * innerBottomLeftRadiusX, + mInnerClipTempRectForBorderRadius.bottom, + + // Line Start + mOuterClipTempRectForBorderRadius.left, + mOuterClipTempRectForBorderRadius.bottom, + + // Line End + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.bottom, + + // Result + mInnerBottomLeftCorner); + + /** Compute mInnerTopRightCorner */ + if (mInnerTopRightCorner == null) { + mInnerTopRightCorner = new PointF(); + } + + mInnerTopRightCorner.x = mInnerClipTempRectForBorderRadius.right; + mInnerTopRightCorner.y = mInnerClipTempRectForBorderRadius.top; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.right - 2 * innerTopRightRadiusX, + mInnerClipTempRectForBorderRadius.top, + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.top + 2 * innerTopRightRadiusY, + + // Line Start + mOuterClipTempRectForBorderRadius.right, + mOuterClipTempRectForBorderRadius.top, + + // Line End + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.top, + + // Result + mInnerTopRightCorner); + + /** Compute mInnerBottomRightCorner */ + if (mInnerBottomRightCorner == null) { + mInnerBottomRightCorner = new PointF(); + } + + mInnerBottomRightCorner.x = mInnerClipTempRectForBorderRadius.right; + mInnerBottomRightCorner.y = mInnerClipTempRectForBorderRadius.bottom; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.right - 2 * innerBottomRightRadiusX, + mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomRightRadiusY, + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.bottom, + + // Line Start + mOuterClipTempRectForBorderRadius.right, + mOuterClipTempRectForBorderRadius.bottom, + + // Line End + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.bottom, + + // Result + mInnerBottomRightCorner); + } + + private static void getEllipseIntersectionWithLine( + double ellipseBoundsLeft, + double ellipseBoundsTop, + double ellipseBoundsRight, + double ellipseBoundsBottom, + double lineStartX, + double lineStartY, + double lineEndX, + double lineEndY, + PointF result) { + final double ellipseCenterX = (ellipseBoundsLeft + ellipseBoundsRight) / 2; + final double ellipseCenterY = (ellipseBoundsTop + ellipseBoundsBottom) / 2; + + /** + * Step 1: + * + *

Translate the line so that the ellipse is at the origin. + * + *

Why? It makes the math easier by changing the ellipse equation from ((x - + * ellipseCenterX)/a)^2 + ((y - ellipseCenterY)/b)^2 = 1 to (x/a)^2 + (y/b)^2 = 1. + */ + lineStartX -= ellipseCenterX; + lineStartY -= ellipseCenterY; + lineEndX -= ellipseCenterX; + lineEndY -= ellipseCenterY; + + /** + * Step 2: + * + *

Ellipse equation: (x/a)^2 + (y/b)^2 = 1 Line equation: y = mx + c + */ + final double a = Math.abs(ellipseBoundsRight - ellipseBoundsLeft) / 2; + final double b = Math.abs(ellipseBoundsBottom - ellipseBoundsTop) / 2; + final double m = (lineEndY - lineStartY) / (lineEndX - lineStartX); + final double c = lineStartY - m * lineStartX; // Just a point on the line + + /** + * Step 3: + * + *

Substitute the Line equation into the Ellipse equation. Solve for x. Eventually, you'll + * have to use the quadratic formula. + * + *

Quadratic formula: Ax^2 + Bx + C = 0 + */ + final double A = (b * b + a * a * m * m); + final double B = 2 * a * a * c * m; + final double C = (a * a * (c * c - b * b)); + + /** + * Step 4: + * + *

Apply Quadratic formula. D = determinant / 2A + */ + final double D = Math.sqrt(-C / A + Math.pow(B / (2 * A), 2)); + final double x2 = -B / (2 * A) - D; + final double y2 = m * x2 + c; + + /** + * Step 5: + * + *

Undo the space transformation in Step 5. + */ + final double x = x2 + ellipseCenterX; + final double y = y2 + ellipseCenterY; + + if (!Double.isNaN(x) && !Double.isNaN(y)) { + result.x = (float) x; + result.y = (float) y; + } + } + + public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { + if (mBorderWidth == null) { + return defaultValue; + } + + final float width = mBorderWidth.getRaw(spacingType); + + if (Float.isNaN(width)) { + return defaultValue; + } + + return width; + } + + /** Set type of border */ + private void updatePathEffect() { + // Used for rounded border and rounded background + PathEffect mPathEffectForBorderStyle = + mBorderStyle != null ? BorderStyle.getPathEffect(mBorderStyle, getFullBorderWidth()) : null; + + mPaint.setPathEffect(mPathEffectForBorderStyle); + } + + private void updatePathEffect(int borderWidth) { + PathEffect pathEffectForBorderStyle = null; + if (mBorderStyle != null) { + pathEffectForBorderStyle = BorderStyle.getPathEffect(mBorderStyle, borderWidth); + } + mPaint.setPathEffect(pathEffectForBorderStyle); + } + + /** For rounded borders we use default "borderWidth" property. */ + public float getFullBorderWidth() { + return (mBorderWidth != null && !Float.isNaN(mBorderWidth.getRaw(Spacing.ALL))) + ? mBorderWidth.getRaw(Spacing.ALL) + : 0f; + } + + /** + * Quickly determine if all the set border colors are equal. Bitwise AND all the set colors + * together, then OR them all together. If the AND and the OR are the same, then the colors are + * compatible, so return this color. + * + *

Used to avoid expensive path creation and expensive calls to canvas.drawPath + * + * @return A compatible border color, or zero if the border colors are not compatible. + */ + private static int fastBorderCompatibleColorOrZero( + int borderLeft, + int borderTop, + int borderRight, + int borderBottom, + int colorLeft, + int colorTop, + int colorRight, + int colorBottom) { + int andSmear = + (borderLeft > 0 ? colorLeft : ALL_BITS_SET) + & (borderTop > 0 ? colorTop : ALL_BITS_SET) + & (borderRight > 0 ? colorRight : ALL_BITS_SET) + & (borderBottom > 0 ? colorBottom : ALL_BITS_SET); + int orSmear = + (borderLeft > 0 ? colorLeft : ALL_BITS_UNSET) + | (borderTop > 0 ? colorTop : ALL_BITS_UNSET) + | (borderRight > 0 ? colorRight : ALL_BITS_UNSET) + | (borderBottom > 0 ? colorBottom : ALL_BITS_UNSET); + return andSmear == orSmear ? andSmear : 0; + } + + private void drawRectangularBackgroundWithBorders(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); + + int useColor = multiplyColorAlpha(mColor, mAlpha); + if (Color.alpha(useColor) != 0) { // color is not transparent + mPaint.setColor(useColor); + canvas.drawRect(getBounds(), mPaint); + } + + final RectF borderWidth = getDirectionAwareBorderInsets(); + + final int borderLeft = Math.round(borderWidth.left); + final int borderTop = Math.round(borderWidth.top); + final int borderRight = Math.round(borderWidth.right); + final int borderBottom = Math.round(borderWidth.bottom); + + // maybe draw borders? + if (borderLeft > 0 || borderRight > 0 || borderTop > 0 || borderBottom > 0) { + Rect bounds = getBounds(); + + int colorLeft = getBorderColor(Spacing.LEFT); + int colorTop = getBorderColor(Spacing.TOP); + int colorRight = getBorderColor(Spacing.RIGHT); + int colorBottom = getBorderColor(Spacing.BOTTOM); + + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + int colorStart = getBorderColor(Spacing.START); + int colorEnd = getBorderColor(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (!isBorderColorDefined(Spacing.START)) { + colorStart = colorLeft; + } + + if (!isBorderColorDefined(Spacing.END)) { + colorEnd = colorRight; + } + + final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final int directionAwareColorRight = isRTL ? colorStart : colorEnd; + + colorLeft = directionAwareColorLeft; + colorRight = directionAwareColorRight; + } else { + final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final int directionAwareColorRight = isRTL ? colorStart : colorEnd; + + final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); + final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); + final boolean isDirectionAwareColorLeftDefined = + isRTL ? isColorEndDefined : isColorStartDefined; + final boolean isDirectionAwareColorRightDefined = + isRTL ? isColorStartDefined : isColorEndDefined; + + if (isDirectionAwareColorLeftDefined) { + colorLeft = directionAwareColorLeft; + } + + if (isDirectionAwareColorRightDefined) { + colorRight = directionAwareColorRight; + } + } + + int left = bounds.left; + int top = bounds.top; + + // Check for fast path to border drawing. + int fastBorderColor = + fastBorderCompatibleColorOrZero( + borderLeft, + borderTop, + borderRight, + borderBottom, + colorLeft, + colorTop, + colorRight, + colorBottom); + + if (fastBorderColor != 0) { + if (Color.alpha(fastBorderColor) != 0) { + // Border color is not transparent. + int right = bounds.right; + int bottom = bounds.bottom; + + mPaint.setColor(fastBorderColor); + mPaint.setStyle(Paint.Style.STROKE); + if (borderLeft > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.left); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left + width / 2, top); + mPathForSingleBorder.lineTo(left + width / 2, bottom); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderTop > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.top); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left, top + width / 2); + mPathForSingleBorder.lineTo(right, top + width / 2); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderRight > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.right); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(right - width / 2, top); + mPathForSingleBorder.lineTo(right - width / 2, bottom); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderBottom > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.bottom); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left, bottom - width / 2); + mPathForSingleBorder.lineTo(right, bottom - width / 2); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + } + } else { + // If the path drawn previously is of the same color, + // there would be a slight white space between borders + // with anti-alias set to true. + // Therefore we need to disable anti-alias, and + // after drawing is done, we will re-enable it. + + mPaint.setAntiAlias(false); + + int width = bounds.width(); + int height = bounds.height(); + + if (borderLeft > 0) { + final float x1 = left; + final float y1 = top; + final float x2 = left + borderLeft; + final float y2 = top + borderTop; + final float x3 = left + borderLeft; + final float y3 = top + height - borderBottom; + final float x4 = left; + final float y4 = top + height; + + drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderTop > 0) { + final float x1 = left; + final float y1 = top; + final float x2 = left + borderLeft; + final float y2 = top + borderTop; + final float x3 = left + width - borderRight; + final float y3 = top + borderTop; + final float x4 = left + width; + final float y4 = top; + + drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderRight > 0) { + final float x1 = left + width; + final float y1 = top; + final float x2 = left + width; + final float y2 = top + height; + final float x3 = left + width - borderRight; + final float y3 = top + height - borderBottom; + final float x4 = left + width - borderRight; + final float y4 = top + borderTop; + + drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderBottom > 0) { + final float x1 = left; + final float y1 = top + height; + final float x2 = left + width; + final float y2 = top + height; + final float x3 = left + width - borderRight; + final float y3 = top + height - borderBottom; + final float x4 = left + borderLeft; + final float y4 = top + height - borderBottom; + + drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); + } + + // re-enable anti alias + mPaint.setAntiAlias(true); + } + } + } + + private void drawQuadrilateral( + Canvas canvas, + int fillColor, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3, + float x4, + float y4) { + if (fillColor == Color.TRANSPARENT) { + return; + } + + if (mPathForBorder == null) { + mPathForBorder = new Path(); + } + + mPaint.setColor(fillColor); + mPathForBorder.reset(); + mPathForBorder.moveTo(x1, y1); + mPathForBorder.lineTo(x2, y2); + mPathForBorder.lineTo(x3, y3); + mPathForBorder.lineTo(x4, y4); + mPathForBorder.lineTo(x1, y1); + canvas.drawPath(mPathForBorder, mPaint); + } + + private static int colorFromAlphaAndRGBComponents(float alpha, float rgb) { + int rgbComponent = 0x00FFFFFF & (int) rgb; + int alphaComponent = 0xFF000000 & ((int) alpha) << 24; + + return rgbComponent | alphaComponent; + } + + private boolean isBorderColorDefined(int position) { + final float rgb = mBorderRGB != null ? mBorderRGB.get(position) : Float.NaN; + final float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : Float.NaN; + return !Float.isNaN(rgb) && !Float.isNaN(alpha); + } + + public int getBorderColor(int position) { + float rgb = mBorderRGB != null ? mBorderRGB.get(position) : DEFAULT_BORDER_RGB; + float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : DEFAULT_BORDER_ALPHA; + + return CSSBackgroundDrawable.colorFromAlphaAndRGBComponents(alpha, rgb); + } + + public RectF getDirectionAwareBorderInsets() { + final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); + final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); + final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); + float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); + float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); + + if (mBorderWidth != null) { + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + float borderStartWidth = mBorderWidth.getRaw(Spacing.START); + float borderEndWidth = mBorderWidth.getRaw(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (Float.isNaN(borderStartWidth)) { + borderStartWidth = borderLeftWidth; + } + + if (Float.isNaN(borderEndWidth)) { + borderEndWidth = borderRightWidth; + } + + final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; + final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; + + borderLeftWidth = directionAwareBorderLeftWidth; + borderRightWidth = directionAwareBorderRightWidth; + } else { + final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; + final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; + + if (!Float.isNaN(directionAwareBorderLeftWidth)) { + borderLeftWidth = directionAwareBorderLeftWidth; + } + + if (!Float.isNaN(directionAwareBorderRightWidth)) { + borderRightWidth = directionAwareBorderRightWidth; + } + } + } + + return new RectF(borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth); + } + + /** + * Multiplies the color with the given alpha. + * + * @param color color to be multiplied + * @param alpha value between 0 and 255 + * @return multiplied color + */ + private static int multiplyColorAlpha(int color, int alpha) { + if (alpha == 255) { + return color; + } + if (alpha == 0) { + return color & 0x00FFFFFF; + } + alpha = alpha + (alpha >> 7); // make it 0..256 + int colorAlpha = color >>> 24; + int multipliedAlpha = colorAlpha * alpha >> 8; + return (multipliedAlpha << 24) | (color & 0x00FFFFFF); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java index 133c8e133465..67d4b459a0df 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java @@ -7,7 +7,6 @@ package com.facebook.react.views.view; -import android.graphics.PixelFormat; import com.facebook.infer.annotation.Nullsafe; /** @@ -18,43 +17,6 @@ @Nullsafe(Nullsafe.Mode.LOCAL) public class ColorUtil { - /** - * Multiplies the color with the given alpha. - * - * @param color color to be multiplied - * @param alpha value between 0 and 255 - * @return multiplied color - */ - public static int multiplyColorAlpha(int color, int alpha) { - if (alpha == 255) { - return color; - } - if (alpha == 0) { - return color & 0x00FFFFFF; - } - alpha = alpha + (alpha >> 7); // make it 0..256 - int colorAlpha = color >>> 24; - int multipliedAlpha = colorAlpha * alpha >> 8; - return (multipliedAlpha << 24) | (color & 0x00FFFFFF); - } - - /** - * Gets the opacity from a color. Inspired by Android ColorDrawable. - * - * @param color color to get opacity from - * @return opacity expressed by one of PixelFormat constants - */ - public static int getOpacityFromColor(int color) { - int colorAlpha = color >>> 24; - if (colorAlpha == 255) { - return PixelFormat.OPAQUE; - } else if (colorAlpha == 0) { - return PixelFormat.TRANSPARENT; - } else { - return PixelFormat.TRANSLUCENT; - } - } - /** * Converts individual {r, g, b, a} channel values to a single integer representation of the color * as 0xAARRGGBB. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java index c93128a89aa2..f9c43850864a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java @@ -8,1432 +8,16 @@ package com.facebook.react.views.view; import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.DashPathEffect; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PathEffect; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.drawable.Drawable; -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.modules.i18nmanager.I18nUtil; -import com.facebook.react.uimanager.FloatUtil; -import com.facebook.react.uimanager.Spacing; -import com.facebook.yoga.YogaConstants; -import java.util.Arrays; -import java.util.Locale; +import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable; /** - * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports drawing - * background color and borders (including rounded borders) by providing a react friendly API - * (setter for each of those properties). - * - *

The implementation tries to allocate as few objects as possible depending on which properties - * are set. E.g. for views with rounded background/borders we allocate {@code - * mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view - * have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only - * background color is set we won't allocate any extra/unnecessary objects. + * @deprecated Please use {@link CSSBackgroundDrawable} instead */ -public class ReactViewBackgroundDrawable extends Drawable { - - private static final int DEFAULT_BORDER_COLOR = Color.BLACK; - private static final int DEFAULT_BORDER_RGB = 0x00FFFFFF & DEFAULT_BORDER_COLOR; - private static final int DEFAULT_BORDER_ALPHA = (0xFF000000 & DEFAULT_BORDER_COLOR) >>> 24; - // ~0 == 0xFFFFFFFF, all bits set to 1. - private static final int ALL_BITS_SET = ~0; - // 0 == 0x00000000, all bits set to 0. - private static final int ALL_BITS_UNSET = 0; - - private enum BorderStyle { - SOLID, - DASHED, - DOTTED; - - public static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { - switch (style) { - case SOLID: - return null; - - case DASHED: - return new DashPathEffect( - new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); - - case DOTTED: - return new DashPathEffect( - new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); - - default: - return null; - } - } - }; - - /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ - private @Nullable Spacing mBorderWidth; - private @Nullable Spacing mBorderRGB; - private @Nullable Spacing mBorderAlpha; - private @Nullable BorderStyle mBorderStyle; - - private @Nullable Path mInnerClipPathForBorderRadius; - private @Nullable Path mBackgroundColorRenderPath; - private @Nullable Path mOuterClipPathForBorderRadius; - private @Nullable Path mPathForBorderRadiusOutline; - private @Nullable Path mPathForBorder; - private final Path mPathForSingleBorder = new Path(); - private @Nullable Path mCenterDrawPath; - private @Nullable RectF mInnerClipTempRectForBorderRadius; - private @Nullable RectF mOuterClipTempRectForBorderRadius; - private @Nullable RectF mTempRectForBorderRadiusOutline; - private @Nullable RectF mTempRectForCenterDrawPath; - private @Nullable PointF mInnerTopLeftCorner; - private @Nullable PointF mInnerTopRightCorner; - private @Nullable PointF mInnerBottomRightCorner; - private @Nullable PointF mInnerBottomLeftCorner; - private boolean mNeedUpdatePathForBorderRadius = false; - private float mBorderRadius = YogaConstants.UNDEFINED; - - /* Used by all types of background and for drawing borders */ - private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private int mColor = Color.TRANSPARENT; - private int mAlpha = 255; - - // There is a small gap between the edges of adjacent paths - // such as between the mBackgroundColorRenderPath and its border. - // The smallest amount (found to be 0.8f) is used to extend - // the paths, overlapping them and closing the visible gap. - private final float mGapBetweenPaths = 0.8f; - - private @Nullable float[] mBorderCornerRadii; - private final Context mContext; - private int mLayoutDirection; - - public enum BorderRadiusLocation { - TOP_LEFT, - TOP_RIGHT, - BOTTOM_RIGHT, - BOTTOM_LEFT, - TOP_START, - TOP_END, - BOTTOM_START, - BOTTOM_END, - END_END, - END_START, - START_END, - START_START - } - - public ReactViewBackgroundDrawable(Context context) { - mContext = context; - } - - @Override - public void draw(Canvas canvas) { - updatePathEffect(); - if (!hasRoundedBorders()) { - drawRectangularBackgroundWithBorders(canvas); - } else { - drawRoundedBackgroundWithBorders(canvas); - } - } - - public boolean hasRoundedBorders() { - if (!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) { - return true; - } - - if (mBorderCornerRadii != null) { - for (final float borderRadii : mBorderCornerRadii) { - if (!YogaConstants.isUndefined(borderRadii) && borderRadii > 0) { - return true; - } - } - } - - return false; - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - mNeedUpdatePathForBorderRadius = true; - } - - @Override - public void setAlpha(int alpha) { - if (alpha != mAlpha) { - mAlpha = alpha; - invalidateSelf(); - } - } - - @Override - public int getAlpha() { - return mAlpha; - } - - @Override - public void setColorFilter(ColorFilter cf) { - // do nothing - } - - @Override - public int getOpacity() { - return ColorUtil.getOpacityFromColor(ColorUtil.multiplyColorAlpha(mColor, mAlpha)); - } - - /* Android's elevation implementation requires this to be implemented to know where to draw the shadow. */ - @Override - public void getOutline(Outline outline) { - if ((!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) - || mBorderCornerRadii != null) { - updatePath(); - - outline.setConvexPath(mPathForBorderRadiusOutline); - } else { - outline.setRect(getBounds()); - } - } - - public void setBorderWidth(int position, float width) { - if (mBorderWidth == null) { - mBorderWidth = new Spacing(); - } - if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { - mBorderWidth.set(position, width); - switch (position) { - case Spacing.ALL: - case Spacing.LEFT: - case Spacing.BOTTOM: - case Spacing.RIGHT: - case Spacing.TOP: - case Spacing.START: - case Spacing.END: - mNeedUpdatePathForBorderRadius = true; - } - invalidateSelf(); - } - } - - public void setBorderColor(int position, float rgb, float alpha) { - this.setBorderRGB(position, rgb); - this.setBorderAlpha(position, alpha); - mNeedUpdatePathForBorderRadius = true; - } - - private void setBorderRGB(int position, float rgb) { - // set RGB component - if (mBorderRGB == null) { - mBorderRGB = new Spacing(DEFAULT_BORDER_RGB); - } - if (!FloatUtil.floatsEqual(mBorderRGB.getRaw(position), rgb)) { - mBorderRGB.set(position, rgb); - invalidateSelf(); - } - } - - private void setBorderAlpha(int position, float alpha) { - // set Alpha component - if (mBorderAlpha == null) { - mBorderAlpha = new Spacing(DEFAULT_BORDER_ALPHA); - } - if (!FloatUtil.floatsEqual(mBorderAlpha.getRaw(position), alpha)) { - mBorderAlpha.set(position, alpha); - invalidateSelf(); - } - } - - public void setBorderStyle(@Nullable String style) { - BorderStyle borderStyle = - style == null ? null : BorderStyle.valueOf(style.toUpperCase(Locale.US)); - if (mBorderStyle != borderStyle) { - mBorderStyle = borderStyle; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public void setRadius(float radius) { - if (!FloatUtil.floatsEqual(mBorderRadius, radius)) { - mBorderRadius = radius; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public void setRadius(float radius, int position) { - if (mBorderCornerRadii == null) { - mBorderCornerRadii = new float[12]; - Arrays.fill(mBorderCornerRadii, YogaConstants.UNDEFINED); - } - - if (!FloatUtil.floatsEqual(mBorderCornerRadii[position], radius)) { - mBorderCornerRadii[position] = radius; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public float getFullBorderRadius() { - return YogaConstants.isUndefined(mBorderRadius) ? 0 : mBorderRadius; - } - - public float getBorderRadius(final BorderRadiusLocation location) { - return getBorderRadiusOrDefaultTo(YogaConstants.UNDEFINED, location); - } - - public float getBorderRadiusOrDefaultTo( - final float defaultValue, final BorderRadiusLocation location) { - if (mBorderCornerRadii == null) { - return defaultValue; - } - - final float radius = mBorderCornerRadii[location.ordinal()]; - - if (YogaConstants.isUndefined(radius)) { - return defaultValue; - } - - return radius; - } - - public void setColor(int color) { - mColor = color; - invalidateSelf(); - } - - /** Similar to Drawable.getLayoutDirection, but available in APIs < 23. */ - public int getResolvedLayoutDirection() { - return mLayoutDirection; - } - - /** Similar to Drawable.setLayoutDirection, but available in APIs < 23. */ - public boolean setResolvedLayoutDirection(int layoutDirection) { - if (mLayoutDirection != layoutDirection) { - mLayoutDirection = layoutDirection; - return onResolvedLayoutDirectionChanged(layoutDirection); - } - return false; - } - - /** Similar to Drawable.onLayoutDirectionChanged, but available in APIs < 23. */ - public boolean onResolvedLayoutDirectionChanged(int layoutDirection) { - return false; - } - - @VisibleForTesting - public int getColor() { - return mColor; - } - - private void drawRoundedBackgroundWithBorders(Canvas canvas) { - updatePath(); - canvas.save(); - - // Clip outer border - canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); - - // Draws the View without its border first (with background color fill) - int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); - if (Color.alpha(useColor) != 0) { // color is not transparent - mPaint.setColor(useColor); - mPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(mBackgroundColorRenderPath, mPaint); - } - - final RectF borderWidth = getDirectionAwareBorderInsets(); - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - if (borderWidth.top > 0 - || borderWidth.bottom > 0 - || borderWidth.left > 0 - || borderWidth.right > 0) { - - // If it's a full and even border draw inner rect path with stroke - final float fullBorderWidth = getFullBorderWidth(); - int borderColor = getBorderColor(Spacing.ALL); - if (borderWidth.top == fullBorderWidth - && borderWidth.bottom == fullBorderWidth - && borderWidth.left == fullBorderWidth - && borderWidth.right == fullBorderWidth - && colorLeft == borderColor - && colorTop == borderColor - && colorRight == borderColor - && colorBottom == borderColor) { - if (fullBorderWidth > 0) { - mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha)); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(fullBorderWidth); - canvas.drawPath(mCenterDrawPath, mPaint); - } - } - // In the case of uneven border widths/colors draw quadrilateral in each direction - else { - mPaint.setStyle(Paint.Style.FILL); - - // Clip inner border - canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - int colorStart = getBorderColor(Spacing.START); - int colorEnd = getBorderColor(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (!isBorderColorDefined(Spacing.START)) { - colorStart = colorLeft; - } - - if (!isBorderColorDefined(Spacing.END)) { - colorEnd = colorRight; - } - - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - colorLeft = directionAwareColorLeft; - colorRight = directionAwareColorRight; - } else { - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); - final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); - final boolean isDirectionAwareColorLeftDefined = - isRTL ? isColorEndDefined : isColorStartDefined; - final boolean isDirectionAwareColorRightDefined = - isRTL ? isColorStartDefined : isColorEndDefined; - - if (isDirectionAwareColorLeftDefined) { - colorLeft = directionAwareColorLeft; - } - - if (isDirectionAwareColorRightDefined) { - colorRight = directionAwareColorRight; - } - } - - final float left = mOuterClipTempRectForBorderRadius.left; - final float right = mOuterClipTempRectForBorderRadius.right; - final float top = mOuterClipTempRectForBorderRadius.top; - final float bottom = mOuterClipTempRectForBorderRadius.bottom; - - // mGapBetweenPaths is used to close the gap between the diagonal - // edges of the quadrilaterals on adjacent sides of the rectangle - if (borderWidth.left > 0) { - final float x1 = left; - final float y1 = top - mGapBetweenPaths; - final float x2 = mInnerTopLeftCorner.x; - final float y2 = mInnerTopLeftCorner.y - mGapBetweenPaths; - final float x3 = mInnerBottomLeftCorner.x; - final float y3 = mInnerBottomLeftCorner.y + mGapBetweenPaths; - final float x4 = left; - final float y4 = bottom + mGapBetweenPaths; - - drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.top > 0) { - final float x1 = left - mGapBetweenPaths; - final float y1 = top; - final float x2 = mInnerTopLeftCorner.x - mGapBetweenPaths; - final float y2 = mInnerTopLeftCorner.y; - final float x3 = mInnerTopRightCorner.x + mGapBetweenPaths; - final float y3 = mInnerTopRightCorner.y; - final float x4 = right + mGapBetweenPaths; - final float y4 = top; - - drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.right > 0) { - final float x1 = right; - final float y1 = top - mGapBetweenPaths; - final float x2 = mInnerTopRightCorner.x; - final float y2 = mInnerTopRightCorner.y - mGapBetweenPaths; - final float x3 = mInnerBottomRightCorner.x; - final float y3 = mInnerBottomRightCorner.y + mGapBetweenPaths; - final float x4 = right; - final float y4 = bottom + mGapBetweenPaths; - - drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.bottom > 0) { - final float x1 = left - mGapBetweenPaths; - final float y1 = bottom; - final float x2 = mInnerBottomLeftCorner.x - mGapBetweenPaths; - final float y2 = mInnerBottomLeftCorner.y; - final float x3 = mInnerBottomRightCorner.x + mGapBetweenPaths; - final float y3 = mInnerBottomRightCorner.y; - final float x4 = right + mGapBetweenPaths; - final float y4 = bottom; - - drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); - } - } - } - - canvas.restore(); - } - - private void updatePath() { - if (!mNeedUpdatePathForBorderRadius) { - return; - } - - mNeedUpdatePathForBorderRadius = false; - - if (mInnerClipPathForBorderRadius == null) { - mInnerClipPathForBorderRadius = new Path(); - } - - if (mBackgroundColorRenderPath == null) { - mBackgroundColorRenderPath = new Path(); - } - - if (mOuterClipPathForBorderRadius == null) { - mOuterClipPathForBorderRadius = new Path(); - } - - if (mPathForBorderRadiusOutline == null) { - mPathForBorderRadiusOutline = new Path(); - } - - if (mCenterDrawPath == null) { - mCenterDrawPath = new Path(); - } - - if (mInnerClipTempRectForBorderRadius == null) { - mInnerClipTempRectForBorderRadius = new RectF(); - } - - if (mOuterClipTempRectForBorderRadius == null) { - mOuterClipTempRectForBorderRadius = new RectF(); - } - - if (mTempRectForBorderRadiusOutline == null) { - mTempRectForBorderRadiusOutline = new RectF(); - } - - if (mTempRectForCenterDrawPath == null) { - mTempRectForCenterDrawPath = new RectF(); - } - - mInnerClipPathForBorderRadius.reset(); - mBackgroundColorRenderPath.reset(); - mOuterClipPathForBorderRadius.reset(); - mPathForBorderRadiusOutline.reset(); - mCenterDrawPath.reset(); - - mInnerClipTempRectForBorderRadius.set(getBounds()); - mOuterClipTempRectForBorderRadius.set(getBounds()); - mTempRectForBorderRadiusOutline.set(getBounds()); - mTempRectForCenterDrawPath.set(getBounds()); - - final RectF borderWidth = getDirectionAwareBorderInsets(); - - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - int borderColor = getBorderColor(Spacing.ALL); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - // Clip border ONLY if its color is non transparent - if (Color.alpha(colorLeft) != 0 - && Color.alpha(colorTop) != 0 - && Color.alpha(colorRight) != 0 - && Color.alpha(colorBottom) != 0 - && Color.alpha(borderColor) != 0) { - - mInnerClipTempRectForBorderRadius.top += borderWidth.top; - mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; - mInnerClipTempRectForBorderRadius.left += borderWidth.left; - mInnerClipTempRectForBorderRadius.right -= borderWidth.right; - } - - mTempRectForCenterDrawPath.top += borderWidth.top * 0.5f; - mTempRectForCenterDrawPath.bottom -= borderWidth.bottom * 0.5f; - mTempRectForCenterDrawPath.left += borderWidth.left * 0.5f; - mTempRectForCenterDrawPath.right -= borderWidth.right * 0.5f; - - final float borderRadius = getFullBorderRadius(); - float topLeftRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_LEFT); - float topRightRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_RIGHT); - float bottomLeftRadius = - getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_LEFT); - float bottomRightRadius = - getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT); - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float topStartRadius = getBorderRadius(BorderRadiusLocation.TOP_START); - float topEndRadius = getBorderRadius(BorderRadiusLocation.TOP_END); - float bottomStartRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_START); - float bottomEndRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_END); - - float endEndRadius = getBorderRadius(BorderRadiusLocation.END_END); - float endStartRadius = getBorderRadius(BorderRadiusLocation.END_START); - float startEndRadius = getBorderRadius(BorderRadiusLocation.START_END); - float startStartRadius = getBorderRadius(BorderRadiusLocation.START_START); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (YogaConstants.isUndefined(topStartRadius)) { - topStartRadius = topLeftRadius; - } - - if (YogaConstants.isUndefined(topEndRadius)) { - topEndRadius = topRightRadius; - } - - if (YogaConstants.isUndefined(bottomStartRadius)) { - bottomStartRadius = bottomLeftRadius; - } - - if (YogaConstants.isUndefined(bottomEndRadius)) { - bottomEndRadius = bottomRightRadius; - } - - final float logicalTopStartRadius = - YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; - final float logicalTopEndRadius = - YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; - final float logicalBottomStartRadius = - YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; - final float logicalBottomEndRadius = - YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; - - final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; - final float directionAwareTopRightRadius = - isRTL ? logicalTopStartRadius : logicalTopEndRadius; - final float directionAwareBottomLeftRadius = - isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; - final float directionAwareBottomRightRadius = - isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; - - topLeftRadius = directionAwareTopLeftRadius; - topRightRadius = directionAwareTopRightRadius; - bottomLeftRadius = directionAwareBottomLeftRadius; - bottomRightRadius = directionAwareBottomRightRadius; - } else { - final float logicalTopStartRadius = - YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; - final float logicalTopEndRadius = - YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; - final float logicalBottomStartRadius = - YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; - final float logicalBottomEndRadius = - YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; - - final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; - final float directionAwareTopRightRadius = - isRTL ? logicalTopStartRadius : logicalTopEndRadius; - final float directionAwareBottomLeftRadius = - isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; - final float directionAwareBottomRightRadius = - isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; - - if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) { - topLeftRadius = directionAwareTopLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) { - topRightRadius = directionAwareTopRightRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) { - bottomLeftRadius = directionAwareBottomLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) { - bottomRightRadius = directionAwareBottomRightRadius; - } - } - - final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderWidth.left, 0); - final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderWidth.top, 0); - final float innerTopRightRadiusX = Math.max(topRightRadius - borderWidth.right, 0); - final float innerTopRightRadiusY = Math.max(topRightRadius - borderWidth.top, 0); - final float innerBottomRightRadiusX = Math.max(bottomRightRadius - borderWidth.right, 0); - final float innerBottomRightRadiusY = Math.max(bottomRightRadius - borderWidth.bottom, 0); - final float innerBottomLeftRadiusX = Math.max(bottomLeftRadius - borderWidth.left, 0); - final float innerBottomLeftRadiusY = Math.max(bottomLeftRadius - borderWidth.bottom, 0); - - mInnerClipPathForBorderRadius.addRoundRect( - mInnerClipTempRectForBorderRadius, - new float[] { - innerTopLeftRadiusX, - innerTopLeftRadiusY, - innerTopRightRadiusX, - innerTopRightRadiusY, - innerBottomRightRadiusX, - innerBottomRightRadiusY, - innerBottomLeftRadiusX, - innerBottomLeftRadiusY, - }, - Path.Direction.CW); - - // There is a small gap between mBackgroundColorRenderPath and its - // border. mGapBetweenPaths is used to slightly enlarge the rectangle - // (mInnerClipTempRectForBorderRadius), ensuring the border can be - // drawn on top without the gap. - mBackgroundColorRenderPath.addRoundRect( - mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths, - new float[] { - innerTopLeftRadiusX, - innerTopLeftRadiusY, - innerTopRightRadiusX, - innerTopRightRadiusY, - innerBottomRightRadiusX, - innerBottomRightRadiusY, - innerBottomLeftRadiusX, - innerBottomLeftRadiusY, - }, - Path.Direction.CW); - - mOuterClipPathForBorderRadius.addRoundRect( - mOuterClipTempRectForBorderRadius, - new float[] { - topLeftRadius, - topLeftRadius, - topRightRadius, - topRightRadius, - bottomRightRadius, - bottomRightRadius, - bottomLeftRadius, - bottomLeftRadius - }, - Path.Direction.CW); - - float extraRadiusForOutline = 0; - - if (mBorderWidth != null) { - extraRadiusForOutline = mBorderWidth.get(Spacing.ALL) / 2f; - } - - mPathForBorderRadiusOutline.addRoundRect( - mTempRectForBorderRadiusOutline, - new float[] { - topLeftRadius + extraRadiusForOutline, - topLeftRadius + extraRadiusForOutline, - topRightRadius + extraRadiusForOutline, - topRightRadius + extraRadiusForOutline, - bottomRightRadius + extraRadiusForOutline, - bottomRightRadius + extraRadiusForOutline, - bottomLeftRadius + extraRadiusForOutline, - bottomLeftRadius + extraRadiusForOutline - }, - Path.Direction.CW); - - mCenterDrawPath.addRoundRect( - mTempRectForCenterDrawPath, - new float[] { - Math.max( - topLeftRadius - borderWidth.left * 0.5f, - (borderWidth.left > 0.0f) ? (topLeftRadius / borderWidth.left) : 0.0f), - Math.max( - topLeftRadius - borderWidth.top * 0.5f, - (borderWidth.top > 0.0f) ? (topLeftRadius / borderWidth.top) : 0.0f), - Math.max( - topRightRadius - borderWidth.right * 0.5f, - (borderWidth.right > 0.0f) ? (topRightRadius / borderWidth.right) : 0.0f), - Math.max( - topRightRadius - borderWidth.top * 0.5f, - (borderWidth.top > 0.0f) ? (topRightRadius / borderWidth.top) : 0.0f), - Math.max( - bottomRightRadius - borderWidth.right * 0.5f, - (borderWidth.right > 0.0f) ? (bottomRightRadius / borderWidth.right) : 0.0f), - Math.max( - bottomRightRadius - borderWidth.bottom * 0.5f, - (borderWidth.bottom > 0.0f) ? (bottomRightRadius / borderWidth.bottom) : 0.0f), - Math.max( - bottomLeftRadius - borderWidth.left * 0.5f, - (borderWidth.left > 0.0f) ? (bottomLeftRadius / borderWidth.left) : 0.0f), - Math.max( - bottomLeftRadius - borderWidth.bottom * 0.5f, - (borderWidth.bottom > 0.0f) ? (bottomLeftRadius / borderWidth.bottom) : 0.0f) - }, - Path.Direction.CW); - - /** - * Rounded Multi-Colored Border Algorithm: - * - *

Let O (for outer) = (top, left, bottom, right) be the rectangle that represents the size - * and position of a view V. Since the box-sizing of all React Native views is border-box, any - * border of V will render inside O. - * - *

Let BorderWidth = (borderTop, borderLeft, borderBottom, borderRight). - * - *

Let I (for inner) = O - BorderWidth. - * - *

Then, remembering that O and I are rectangles and that I is inside O, O - I gives us the - * border of V. Therefore, we can use canvas.clipPath to draw V's border. - * - *

canvas.clipPath(O, Region.OP.INTERSECT); - * - *

canvas.clipPath(I, Region.OP.DIFFERENCE); - * - *

canvas.drawRect(O, paint); - * - *

This lets us draw non-rounded single-color borders. - * - *

To extend this algorithm to rounded single-color borders, we: - * - *

1. Curve the corners of O by the (border radii of V) using Path#addRoundRect. - * - *

2. Curve the corners of I by (border radii of V - border widths of V) using - * Path#addRoundRect. - * - *

Let O' = curve(O, border radii of V). - * - *

Let I' = curve(I, border radii of V - border widths of V) - * - *

The rationale behind this decision is the (first sentence of the) following section in the - * CSS Backgrounds and Borders Module Level 3: - * https://www.w3.org/TR/css3-background/#the-border-radius. - * - *

After both O and I have been curved, we can execute the following lines once again to - * render curved single-color borders: - * - *

canvas.clipPath(O, Region.OP.INTERSECT); - * - *

canvas.clipPath(I, Region.OP.DIFFERENCE); - * - *

canvas.drawRect(O, paint); - * - *

To extend this algorithm to rendering multi-colored rounded borders, we render each side - * of the border as its own quadrilateral. Suppose that we were handling the case where all the - * border radii are 0. Then, the four quadrilaterals would be: - * - *

Left: (O.left, O.top), (I.left, I.top), (I.left, I.bottom), (O.left, O.bottom) - * - *

Top: (O.left, O.top), (I.left, I.top), (I.right, I.top), (O.right, O.top) - * - *

Right: (O.right, O.top), (I.right, I.top), (I.right, I.bottom), (O.right, O.bottom) - * - *

Bottom: (O.right, O.bottom), (I.right, I.bottom), (I.left, I.bottom), (O.left, O.bottom) - * - *

Now, lets consider what happens when we render a rounded border (radii != 0). For the sake - * of simplicity, let's focus on the top edge of the Left border: - * - *

Let borderTopLeftRadius = 5. Let borderLeftWidth = 1. Let borderTopWidth = 2. - * - *

We know that O is curved by the ellipse E_O (a = 5, b = 5). We know that I is curved by - * the ellipse E_I (a = 5 - 1, b = 5 - 2). - * - *

Since we have clipping, it should be safe to set the top-left point of the Left - * quadrilateral's top edge to (O.left, O.top). - * - *

But, what should the top-right point be? - * - *

The fact that the border is curved shouldn't change the slope (nor the position) of the - * line connecting the top-left and top-right points of the Left quadrilateral's top edge. - * Therefore, The top-right point should lie somewhere on the line L = (1 - a) * (O.left, O.top) - * + a * (I.left, I.top). - * - *

a != 0, because then the top-left and top-right points would be the same and - * borderLeftWidth = 1. a != 1, because then the top-right point would not touch an edge of the - * ellipse E_I. We want the top-right point to touch an edge of the inner ellipse because the - * border curves with E_I on the top-left corner of V. - * - *

Therefore, it must be the case that a > 1. Two natural locations of the top-right point - * exist: 1. The first intersection of L with E_I. 2. The second intersection of L with E_I. - * - *

We choose the top-right point of the top edge of the Left quadrilateral to be an arbitrary - * intersection of L with E_I. - */ - if (mInnerTopLeftCorner == null) { - mInnerTopLeftCorner = new PointF(); - } - - /** Compute mInnerTopLeftCorner */ - mInnerTopLeftCorner.x = mInnerClipTempRectForBorderRadius.left; - mInnerTopLeftCorner.y = mInnerClipTempRectForBorderRadius.top; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.top, - mInnerClipTempRectForBorderRadius.left + 2 * innerTopLeftRadiusX, - mInnerClipTempRectForBorderRadius.top + 2 * innerTopLeftRadiusY, - - // Line Start - mOuterClipTempRectForBorderRadius.left, - mOuterClipTempRectForBorderRadius.top, - - // Line End - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.top, - - // Result - mInnerTopLeftCorner); - - /** Compute mInnerBottomLeftCorner */ - if (mInnerBottomLeftCorner == null) { - mInnerBottomLeftCorner = new PointF(); - } - - mInnerBottomLeftCorner.x = mInnerClipTempRectForBorderRadius.left; - mInnerBottomLeftCorner.y = mInnerClipTempRectForBorderRadius.bottom; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomLeftRadiusY, - mInnerClipTempRectForBorderRadius.left + 2 * innerBottomLeftRadiusX, - mInnerClipTempRectForBorderRadius.bottom, - - // Line Start - mOuterClipTempRectForBorderRadius.left, - mOuterClipTempRectForBorderRadius.bottom, - - // Line End - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.bottom, - - // Result - mInnerBottomLeftCorner); - - /** Compute mInnerTopRightCorner */ - if (mInnerTopRightCorner == null) { - mInnerTopRightCorner = new PointF(); - } - - mInnerTopRightCorner.x = mInnerClipTempRectForBorderRadius.right; - mInnerTopRightCorner.y = mInnerClipTempRectForBorderRadius.top; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.right - 2 * innerTopRightRadiusX, - mInnerClipTempRectForBorderRadius.top, - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.top + 2 * innerTopRightRadiusY, - - // Line Start - mOuterClipTempRectForBorderRadius.right, - mOuterClipTempRectForBorderRadius.top, - - // Line End - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.top, - - // Result - mInnerTopRightCorner); - - /** Compute mInnerBottomRightCorner */ - if (mInnerBottomRightCorner == null) { - mInnerBottomRightCorner = new PointF(); - } - - mInnerBottomRightCorner.x = mInnerClipTempRectForBorderRadius.right; - mInnerBottomRightCorner.y = mInnerClipTempRectForBorderRadius.bottom; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.right - 2 * innerBottomRightRadiusX, - mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomRightRadiusY, - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.bottom, - - // Line Start - mOuterClipTempRectForBorderRadius.right, - mOuterClipTempRectForBorderRadius.bottom, - - // Line End - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.bottom, - - // Result - mInnerBottomRightCorner); - } - - private static void getEllipseIntersectionWithLine( - double ellipseBoundsLeft, - double ellipseBoundsTop, - double ellipseBoundsRight, - double ellipseBoundsBottom, - double lineStartX, - double lineStartY, - double lineEndX, - double lineEndY, - PointF result) { - final double ellipseCenterX = (ellipseBoundsLeft + ellipseBoundsRight) / 2; - final double ellipseCenterY = (ellipseBoundsTop + ellipseBoundsBottom) / 2; - - /** - * Step 1: - * - *

Translate the line so that the ellipse is at the origin. - * - *

Why? It makes the math easier by changing the ellipse equation from ((x - - * ellipseCenterX)/a)^2 + ((y - ellipseCenterY)/b)^2 = 1 to (x/a)^2 + (y/b)^2 = 1. - */ - lineStartX -= ellipseCenterX; - lineStartY -= ellipseCenterY; - lineEndX -= ellipseCenterX; - lineEndY -= ellipseCenterY; - - /** - * Step 2: - * - *

Ellipse equation: (x/a)^2 + (y/b)^2 = 1 Line equation: y = mx + c - */ - final double a = Math.abs(ellipseBoundsRight - ellipseBoundsLeft) / 2; - final double b = Math.abs(ellipseBoundsBottom - ellipseBoundsTop) / 2; - final double m = (lineEndY - lineStartY) / (lineEndX - lineStartX); - final double c = lineStartY - m * lineStartX; // Just a point on the line - - /** - * Step 3: - * - *

Substitute the Line equation into the Ellipse equation. Solve for x. Eventually, you'll - * have to use the quadratic formula. - * - *

Quadratic formula: Ax^2 + Bx + C = 0 - */ - final double A = (b * b + a * a * m * m); - final double B = 2 * a * a * c * m; - final double C = (a * a * (c * c - b * b)); - - /** - * Step 4: - * - *

Apply Quadratic formula. D = determinant / 2A - */ - final double D = Math.sqrt(-C / A + Math.pow(B / (2 * A), 2)); - final double x2 = -B / (2 * A) - D; - final double y2 = m * x2 + c; - - /** - * Step 5: - * - *

Undo the space transformation in Step 5. - */ - final double x = x2 + ellipseCenterX; - final double y = y2 + ellipseCenterY; - - if (!Double.isNaN(x) && !Double.isNaN(y)) { - result.x = (float) x; - result.y = (float) y; - } - } - - public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { - if (mBorderWidth == null) { - return defaultValue; - } - - final float width = mBorderWidth.getRaw(spacingType); - - if (YogaConstants.isUndefined(width)) { - return defaultValue; - } - - return width; - } - - /** Set type of border */ - private void updatePathEffect() { - // Used for rounded border and rounded background - PathEffect mPathEffectForBorderStyle = - mBorderStyle != null ? BorderStyle.getPathEffect(mBorderStyle, getFullBorderWidth()) : null; - - mPaint.setPathEffect(mPathEffectForBorderStyle); - } - - private void updatePathEffect(int borderWidth) { - PathEffect pathEffectForBorderStyle = null; - if (mBorderStyle != null) { - pathEffectForBorderStyle = BorderStyle.getPathEffect(mBorderStyle, borderWidth); - } - mPaint.setPathEffect(pathEffectForBorderStyle); - } - - /** For rounded borders we use default "borderWidth" property. */ - public float getFullBorderWidth() { - return (mBorderWidth != null && !YogaConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) - ? mBorderWidth.getRaw(Spacing.ALL) - : 0f; - } - +public class ReactViewBackgroundDrawable extends CSSBackgroundDrawable { /** - * Quickly determine if all the set border colors are equal. Bitwise AND all the set colors - * together, then OR them all together. If the AND and the OR are the same, then the colors are - * compatible, so return this color. - * - *

Used to avoid expensive path creation and expensive calls to canvas.drawPath - * - * @return A compatible border color, or zero if the border colors are not compatible. + * @deprecated Please use {@link CSSBackgroundDrawable} instead */ - private static int fastBorderCompatibleColorOrZero( - int borderLeft, - int borderTop, - int borderRight, - int borderBottom, - int colorLeft, - int colorTop, - int colorRight, - int colorBottom) { - int andSmear = - (borderLeft > 0 ? colorLeft : ALL_BITS_SET) - & (borderTop > 0 ? colorTop : ALL_BITS_SET) - & (borderRight > 0 ? colorRight : ALL_BITS_SET) - & (borderBottom > 0 ? colorBottom : ALL_BITS_SET); - int orSmear = - (borderLeft > 0 ? colorLeft : ALL_BITS_UNSET) - | (borderTop > 0 ? colorTop : ALL_BITS_UNSET) - | (borderRight > 0 ? colorRight : ALL_BITS_UNSET) - | (borderBottom > 0 ? colorBottom : ALL_BITS_UNSET); - return andSmear == orSmear ? andSmear : 0; - } - - private void drawRectangularBackgroundWithBorders(Canvas canvas) { - mPaint.setStyle(Paint.Style.FILL); - - int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); - if (Color.alpha(useColor) != 0) { // color is not transparent - mPaint.setColor(useColor); - canvas.drawRect(getBounds(), mPaint); - } - - final RectF borderWidth = getDirectionAwareBorderInsets(); - - final int borderLeft = Math.round(borderWidth.left); - final int borderTop = Math.round(borderWidth.top); - final int borderRight = Math.round(borderWidth.right); - final int borderBottom = Math.round(borderWidth.bottom); - - // maybe draw borders? - if (borderLeft > 0 || borderRight > 0 || borderTop > 0 || borderBottom > 0) { - Rect bounds = getBounds(); - - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - int colorStart = getBorderColor(Spacing.START); - int colorEnd = getBorderColor(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (!isBorderColorDefined(Spacing.START)) { - colorStart = colorLeft; - } - - if (!isBorderColorDefined(Spacing.END)) { - colorEnd = colorRight; - } - - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - colorLeft = directionAwareColorLeft; - colorRight = directionAwareColorRight; - } else { - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); - final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); - final boolean isDirectionAwareColorLeftDefined = - isRTL ? isColorEndDefined : isColorStartDefined; - final boolean isDirectionAwareColorRightDefined = - isRTL ? isColorStartDefined : isColorEndDefined; - - if (isDirectionAwareColorLeftDefined) { - colorLeft = directionAwareColorLeft; - } - - if (isDirectionAwareColorRightDefined) { - colorRight = directionAwareColorRight; - } - } - - int left = bounds.left; - int top = bounds.top; - - // Check for fast path to border drawing. - int fastBorderColor = - fastBorderCompatibleColorOrZero( - borderLeft, - borderTop, - borderRight, - borderBottom, - colorLeft, - colorTop, - colorRight, - colorBottom); - - if (fastBorderColor != 0) { - if (Color.alpha(fastBorderColor) != 0) { - // Border color is not transparent. - int right = bounds.right; - int bottom = bounds.bottom; - - mPaint.setColor(fastBorderColor); - mPaint.setStyle(Paint.Style.STROKE); - if (borderLeft > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.left); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left + width / 2, top); - mPathForSingleBorder.lineTo(left + width / 2, bottom); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderTop > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.top); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left, top + width / 2); - mPathForSingleBorder.lineTo(right, top + width / 2); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderRight > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.right); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(right - width / 2, top); - mPathForSingleBorder.lineTo(right - width / 2, bottom); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderBottom > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.bottom); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left, bottom - width / 2); - mPathForSingleBorder.lineTo(right, bottom - width / 2); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - } - } else { - // If the path drawn previously is of the same color, - // there would be a slight white space between borders - // with anti-alias set to true. - // Therefore we need to disable anti-alias, and - // after drawing is done, we will re-enable it. - - mPaint.setAntiAlias(false); - - int width = bounds.width(); - int height = bounds.height(); - - if (borderLeft > 0) { - final float x1 = left; - final float y1 = top; - final float x2 = left + borderLeft; - final float y2 = top + borderTop; - final float x3 = left + borderLeft; - final float y3 = top + height - borderBottom; - final float x4 = left; - final float y4 = top + height; - - drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderTop > 0) { - final float x1 = left; - final float y1 = top; - final float x2 = left + borderLeft; - final float y2 = top + borderTop; - final float x3 = left + width - borderRight; - final float y3 = top + borderTop; - final float x4 = left + width; - final float y4 = top; - - drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderRight > 0) { - final float x1 = left + width; - final float y1 = top; - final float x2 = left + width; - final float y2 = top + height; - final float x3 = left + width - borderRight; - final float y3 = top + height - borderBottom; - final float x4 = left + width - borderRight; - final float y4 = top + borderTop; - - drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderBottom > 0) { - final float x1 = left; - final float y1 = top + height; - final float x2 = left + width; - final float y2 = top + height; - final float x3 = left + width - borderRight; - final float y3 = top + height - borderBottom; - final float x4 = left + borderLeft; - final float y4 = top + height - borderBottom; - - drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); - } - - // re-enable anti alias - mPaint.setAntiAlias(true); - } - } - } - - private void drawQuadrilateral( - Canvas canvas, - int fillColor, - float x1, - float y1, - float x2, - float y2, - float x3, - float y3, - float x4, - float y4) { - if (fillColor == Color.TRANSPARENT) { - return; - } - - if (mPathForBorder == null) { - mPathForBorder = new Path(); - } - - mPaint.setColor(fillColor); - mPathForBorder.reset(); - mPathForBorder.moveTo(x1, y1); - mPathForBorder.lineTo(x2, y2); - mPathForBorder.lineTo(x3, y3); - mPathForBorder.lineTo(x4, y4); - mPathForBorder.lineTo(x1, y1); - canvas.drawPath(mPathForBorder, mPaint); - } - - private int getBorderWidth(int position) { - if (mBorderWidth == null) { - return 0; - } - - final float width = mBorderWidth.get(position); - return YogaConstants.isUndefined(width) ? -1 : Math.round(width); - } - - private static int colorFromAlphaAndRGBComponents(float alpha, float rgb) { - int rgbComponent = 0x00FFFFFF & (int) rgb; - int alphaComponent = 0xFF000000 & ((int) alpha) << 24; - - return rgbComponent | alphaComponent; - } - - private boolean isBorderColorDefined(int position) { - final float rgb = mBorderRGB != null ? mBorderRGB.get(position) : YogaConstants.UNDEFINED; - final float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : YogaConstants.UNDEFINED; - return !YogaConstants.isUndefined(rgb) && !YogaConstants.isUndefined(alpha); - } - - public int getBorderColor(int position) { - float rgb = mBorderRGB != null ? mBorderRGB.get(position) : DEFAULT_BORDER_RGB; - float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : DEFAULT_BORDER_ALPHA; - - return ReactViewBackgroundDrawable.colorFromAlphaAndRGBComponents(alpha, rgb); - } - - public RectF getDirectionAwareBorderInsets() { - final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); - final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); - final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); - float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); - float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); - - if (mBorderWidth != null) { - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float borderStartWidth = mBorderWidth.getRaw(Spacing.START); - float borderEndWidth = mBorderWidth.getRaw(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (YogaConstants.isUndefined(borderStartWidth)) { - borderStartWidth = borderLeftWidth; - } - - if (YogaConstants.isUndefined(borderEndWidth)) { - borderEndWidth = borderRightWidth; - } - - final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; - final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; - - borderLeftWidth = directionAwareBorderLeftWidth; - borderRightWidth = directionAwareBorderRightWidth; - } else { - final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; - final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; - - if (!YogaConstants.isUndefined(directionAwareBorderLeftWidth)) { - borderLeftWidth = directionAwareBorderLeftWidth; - } - - if (!YogaConstants.isUndefined(directionAwareBorderRightWidth)) { - borderRightWidth = directionAwareBorderRightWidth; - } - } - } - - return new RectF(borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth); + public ReactViewBackgroundDrawable(Context context) { + super(context); } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.kt index b5ed351fe4d7..cee90dce0096 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.kt @@ -7,7 +7,6 @@ package com.facebook.react.views.view -import android.graphics.PixelFormat import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -16,27 +15,6 @@ import org.robolectric.RobolectricTestRunner /** Based on Fresco's DrawableUtilsTest (https://github.com/facebook/fresco). */ @RunWith(RobolectricTestRunner::class) class ColorUtilTest { - @Test - fun testMultiplyColorAlpha() { - assertEquals(0x00123456U.toInt(), ColorUtil.multiplyColorAlpha(0xC0123456U.toInt(), 0)) - assertEquals(0x07123456U.toInt(), ColorUtil.multiplyColorAlpha(0xC0123456U.toInt(), 10)) - assertEquals(0x96123456U.toInt(), ColorUtil.multiplyColorAlpha(0xC0123456U.toInt(), 200)) - assertEquals(0xC0123456U.toInt(), ColorUtil.multiplyColorAlpha(0xC0123456U.toInt(), 255)) - } - - @Test - fun testGetOpacityFromColor() { - assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00000000)) - assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00123456)) - assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00FFFFFF)) - assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0000000.toInt())) - assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0123456.toInt())) - assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0FFFFFF.toInt())) - assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFF000000.toInt())) - assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFF123456.toInt())) - assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFFFFFFFF.toInt())) - } - @Test fun testNormalize() { assertEquals(0x800B1621U.toInt(), ColorUtil.normalize(11.0, 22.0, 33.0, 0.5))