From a8f58db702fe4b39acb848583ded72fd21845294 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 30 Mar 2024 18:40:53 -0700 Subject: [PATCH] Move background drawing code from "react/views/view" to "react/uimanager/drawable" Summary: This lets us use BG drawing code in rules which view depends on. I also removed some lib usage. The original class is still present, subclassing the class in its new location, but is marked deprecated. Next diffs in stack clean up some of our own now deprecated usage. Changelog: [Internal] Differential Revision: D55565035 --- .../ReactAndroid/api/ReactAndroid.api | 92 +- .../drawable/CSSBackgroundDrawable.java | 1458 +++++++++++++++++ .../facebook/react/views/view/ColorUtil.java | 38 - .../view/ReactViewBackgroundDrawable.java | 1428 +--------------- .../react/views/view/ColorUtilTest.kt | 22 - 5 files changed, 1512 insertions(+), 1526 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java 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))