diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index bd561e1a1a87..b951d7d8675b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5442,6 +5442,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 } @@ -7653,8 +7700,6 @@ public final class com/facebook/react/views/unimplementedview/ReactUnimplemented public final class com/facebook/react/views/view/ColorUtil { public static final field INSTANCE Lcom/facebook/react/views/view/ColorUtil; - public static final fun getOpacityFromColor (I)I - public static final fun multiplyColorAlpha (II)I public static final fun normalize (DDDD)I } @@ -7687,49 +7732,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 { @@ -7850,4 +7854,3 @@ public final class com/facebook/react/views/view/ViewGroupClickEvent : com/faceb public fun canCoalesce ()Z public fun getEventName ()Ljava/lang/String; } - diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt index a451c9f5f3b9..f0fbf919f31f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt @@ -8,7 +8,6 @@ package com.facebook.react.uimanager import android.annotation.TargetApi -import android.graphics.ColorFilter import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.RenderEffect @@ -20,54 +19,24 @@ import com.facebook.react.bridge.ReadableArray internal object FilterHelper { @JvmStatic - fun parseFilters(filters: ReadableArray?): RenderEffect? { + public fun parseFilters(filters: ReadableArray?): RenderEffect? { filters ?: return null var chainedEffects: RenderEffect? = null for (i in 0 until filters.size()) { val filter = filters.getMap(i) val filterName = filter.getString("name") ?: continue + val amount = filter.getDouble("amount").toFloat() + chainedEffects = when (filterName) { - "brightness" -> { - val brightnessAmount = filter.getDouble("amount").toFloat() - val brightnessFilter = - ColorMatrixColorFilter(getBrightnessColorMatrix(brightnessAmount)) - chainColorFilterEffect(chainedEffects, brightnessFilter) - } - "contrast" -> { - val contrastAmount = filter.getDouble("amount").toFloat() - val contrastFilter = ColorMatrixColorFilter(getContrastColorMatrix(contrastAmount)) - chainColorFilterEffect(chainedEffects, contrastFilter) - } - "grayscale" -> { - val grayscaleAmount = filter.getDouble("amount").toFloat() - val grayscaleFilter = ColorMatrixColorFilter(getGrayscaleColorMatrix(grayscaleAmount)) - chainColorFilterEffect(chainedEffects, grayscaleFilter) - } - "sepia" -> { - val sepiaAmount = filter.getDouble("amount").toFloat() - val sepiaFilter = ColorMatrixColorFilter(getSepiaColorMatrix(sepiaAmount)) - chainColorFilterEffect(chainedEffects, sepiaFilter) - } - "saturate" -> { - val saturateAmount = filter.getDouble("amount").toFloat() - val saturateFilter = ColorMatrixColorFilter(getSaturateColorMatrix(saturateAmount)) - chainColorFilterEffect(chainedEffects, saturateFilter) - } - "hue-rotate" -> { - val hueRotateAmount = filter.getDouble("amount").toFloat() - val hueRotateFilter = ColorMatrixColorFilter(getHueRotateColorMatrix(hueRotateAmount)) - chainColorFilterEffect(chainedEffects, hueRotateFilter) - } - "invert" -> { - val invertAmount = filter.getDouble("amount").toFloat() - val invertColorFilter = ColorMatrixColorFilter(getInvertColorMatrix(invertAmount)) - chainColorFilterEffect(chainedEffects, invertColorFilter) - } - "blur" -> { - val blurAmount = filter.getDouble("amount").toFloat() - chainBlurFilterEffect(chainedEffects, blurAmount) - } + "brightness" -> createBrightnessEffect(amount, chainedEffects) + "contrast" -> createContrastEffect(amount, chainedEffects) + "grayscale" -> createGrayscaleEffect(amount, chainedEffects) + "sepia" -> createSepiaEffect(amount, chainedEffects) + "saturate" -> createSaturateEffect(amount, chainedEffects) + "hue-rotate" -> createHueRotateEffect(amount, chainedEffects) + "invert" -> createInvertEffect(amount, chainedEffects) + "blur" -> createBlurEffect(amount, chainedEffects) else -> throw IllegalArgumentException("Invalid filter name: $filterName") } } @@ -75,180 +44,210 @@ internal object FilterHelper { } // https://www.w3.org/TR/filter-effects-1/#blurEquivalent - private fun chainBlurFilterEffect(chainedEffects: RenderEffect?, std: Float): RenderEffect { + public fun createBlurEffect(sigma: Float, chainedEffects: RenderEffect? = null): RenderEffect? { + if (sigma <= 0.5) { + return null + } + // Android takes blur amount as a radius while web takes a sigma. This value // is used under the hood to convert between them on Android - // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/utils/Blur.cpp + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/jni/RenderEffect.cpp val sigmaToRadiusRatio = 0.57_735f - val radius = (std - 0.5f) / sigmaToRadiusRatio - val scaledRadius = PixelUtil.toPixelFromDIP(radius) - return if (chainedEffects == null) { - RenderEffect.createBlurEffect(scaledRadius, scaledRadius, Shader.TileMode.DECAL) - } else { - RenderEffect.createBlurEffect( - scaledRadius, scaledRadius, chainedEffects, Shader.TileMode.DECAL) - } - } - - private fun chainColorFilterEffect( - chainedEffects: RenderEffect?, - colorFilter: ColorFilter - ): RenderEffect { + val radius = (PixelUtil.toPixelFromDIP(sigma) - 0.5f) / sigmaToRadiusRatio return if (chainedEffects == null) { - RenderEffect.createColorFilterEffect(colorFilter) + RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.DECAL) } else { - RenderEffect.createColorFilterEffect(colorFilter, chainedEffects) + RenderEffect.createBlurEffect(radius, radius, chainedEffects, Shader.TileMode.DECAL) } } // https://www.w3.org/TR/filter-effects-1/#brightnessEquivalent - private fun getBrightnessColorMatrix(amount: Float): ColorMatrix { + public fun createBrightnessEffect( + amount: Float, + chainedEffects: RenderEffect? = null + ): RenderEffect { val matrix = ColorMatrix() matrix.setScale(amount, amount, amount, 1f) - return matrix + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#contrastEquivalent - private fun getContrastColorMatrix(amount: Float): ColorMatrix { + public fun createContrastEffect( + amount: Float, + chainedEffects: RenderEffect? = null + ): RenderEffect { // Multiply by 255 as Android operates in [0, 255] while the spec operates in [0, 1]. // This really only matters if there is an intercept that needs to be added val intercept = 255 * (-(amount / 2.0f) + 0.5f) - val colorMatrix = - floatArrayOf( - amount, - 0f, - 0f, - 0f, - intercept, - 0f, - amount, - 0f, - 0f, - intercept, - 0f, - 0f, - amount, - 0f, - intercept, - 0f, - 0f, - 0f, - 1f, - 0f) - return ColorMatrix(colorMatrix) + val matrix = + ColorMatrix( + floatArrayOf( + amount, + 0f, + 0f, + 0f, + intercept, + 0f, + amount, + 0f, + 0f, + intercept, + 0f, + 0f, + amount, + 0f, + intercept, + 0f, + 0f, + 0f, + 1f, + 0f)) + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent - private fun getGrayscaleColorMatrix(amount: Float): FloatArray { + public fun createGrayscaleEffect( + amount: Float, + chainedEffects: RenderEffect? = null + ): RenderEffect { val inverseAmount = 1 - amount - return floatArrayOf( - 0.2_126f + 0.7_874f * inverseAmount, - 0.7_152f - 0.7_152f * inverseAmount, - 0.0_722f - 0.0_722f * inverseAmount, - 0f, - 0f, - 0.2_126f - 0.2_126f * inverseAmount, - 0.7_152f + 0.2_848f * inverseAmount, - 0.0_722f - 0.0_722f * inverseAmount, - 0f, - 0f, - 0.2_126f - 0.2_126f * inverseAmount, - 0.7_152f - 0.7_152f * inverseAmount, - 0.0_722f + 0.9_278f * inverseAmount, - 0f, - 0f, - 0f, - 0f, - 0f, - 1f, - 0f) + val matrix = + ColorMatrix( + floatArrayOf( + 0.2_126f + 0.7_874f * inverseAmount, + 0.7_152f - 0.7_152f * inverseAmount, + 0.0_722f - 0.0_722f * inverseAmount, + 0f, + 0f, + 0.2_126f - 0.2_126f * inverseAmount, + 0.7_152f + 0.2_848f * inverseAmount, + 0.0_722f - 0.0_722f * inverseAmount, + 0f, + 0f, + 0.2_126f - 0.2_126f * inverseAmount, + 0.7_152f - 0.7_152f * inverseAmount, + 0.0_722f + 0.9_278f * inverseAmount, + 0f, + 0f, + 0f, + 0f, + 0f, + 1f, + 0f)) + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent - private fun getSepiaColorMatrix(amount: Float): FloatArray { + public fun createSepiaEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect { val inverseAmount = 1 - amount - return floatArrayOf( - 0.393f + 0.607f * inverseAmount, - 0.769f - 0.769f * inverseAmount, - 0.189f - 0.189f * inverseAmount, - 0f, - 0f, - 0.349f - 0.349f * inverseAmount, - 0.686f + 0.314f * inverseAmount, - 0.168f - 0.168f * inverseAmount, - 0f, - 0f, - 0.272f - 0.272f * inverseAmount, - 0.534f - 0.534f * inverseAmount, - 0.131f + 0.869f * inverseAmount, - 0f, - 0f, - 0f, - 0f, - 0f, - 1f, - 0f) + val matrix = + ColorMatrix( + floatArrayOf( + 0.393f + 0.607f * inverseAmount, + 0.769f - 0.769f * inverseAmount, + 0.189f - 0.189f * inverseAmount, + 0f, + 0f, + 0.349f - 0.349f * inverseAmount, + 0.686f + 0.314f * inverseAmount, + 0.168f - 0.168f * inverseAmount, + 0f, + 0f, + 0.272f - 0.272f * inverseAmount, + 0.534f - 0.534f * inverseAmount, + 0.131f + 0.869f * inverseAmount, + 0f, + 0f, + 0f, + 0f, + 0f, + 1f, + 0f)) + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#saturateEquivalent - private fun getSaturateColorMatrix(amount: Float): ColorMatrix { + public fun createSaturateEffect( + amount: Float, + chainedEffects: RenderEffect? = null + ): RenderEffect { val matrix = ColorMatrix() matrix.setSaturation(amount) - return matrix + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#huerotateEquivalent - private fun getHueRotateColorMatrix(amount: Float): FloatArray { + public fun createHueRotateEffect( + amount: Float, + chainedEffects: RenderEffect? = null + ): RenderEffect { val amountRads = Math.toRadians(amount.toDouble()) val cos = Math.cos(amountRads).toFloat() val sin = Math.sin(amountRads).toFloat() - return floatArrayOf( - 0.213f + 0.787f * cos - 0.213f * sin, - 0.715f - 0.715f * cos - 0.715f * sin, - 0.072f - 0.072f * cos + 0.928f * sin, - 0f, - 0f, - 0.213f - 0.213f * cos + 0.143f * sin, - 0.715f + 0.285f * cos + 0.140f * sin, - 0.072f - 0.072f * cos - 0.283f * sin, - 0f, - 0f, - 0.213f - 0.213f * cos - 0.787f * sin, - 0.715f - 0.715f * cos + 0.715f * sin, - 0.072f + 0.928f * cos + 0.072f * sin, - 0f, - 0f, - 0f, - 0f, - 0f, - 1f, - 0f) + val matrix = + ColorMatrix( + floatArrayOf( + 0.213f + 0.787f * cos - 0.213f * sin, + 0.715f - 0.715f * cos - 0.715f * sin, + 0.072f - 0.072f * cos + 0.928f * sin, + 0f, + 0f, + 0.213f - 0.213f * cos + 0.143f * sin, + 0.715f + 0.285f * cos + 0.140f * sin, + 0.072f - 0.072f * cos - 0.283f * sin, + 0f, + 0f, + 0.213f - 0.213f * cos - 0.787f * sin, + 0.715f - 0.715f * cos + 0.715f * sin, + 0.072f + 0.928f * cos + 0.072f * sin, + 0f, + 0f, + 0f, + 0f, + 0f, + 1f, + 0f)) + return createColorMatrixEffect(matrix, chainedEffects) } // https://www.w3.org/TR/filter-effects-1/#invertEquivalent - private fun getInvertColorMatrix(amount: Float): FloatArray { + public fun createInvertEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect { val slope = 1 - 2 * amount val intercept = amount * 255 - return floatArrayOf( - slope, - 0f, - 0f, - 0f, - intercept, - 0f, - slope, - 0f, - 0f, - intercept, - 0f, - 0f, - slope, - 0f, - intercept, - 0f, - 0f, - 0f, - 1f, - 0f) + val matrix = + ColorMatrix( + floatArrayOf( + slope, + 0f, + 0f, + 0f, + intercept, + 0f, + slope, + 0f, + 0f, + intercept, + 0f, + 0f, + slope, + 0f, + intercept, + 0f, + 0f, + 0f, + 1f, + 0f)) + return createColorMatrixEffect(matrix, chainedEffects) + } + + private fun createColorMatrixEffect( + colorMatrix: ColorMatrix, + chainedEffects: RenderEffect? = null + ): RenderEffect { + return if (chainedEffects == null) { + RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(colorMatrix)) + } else { + RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(colorMatrix), chainedEffects) + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FloatUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FloatUtil.kt index e5ad28d758a3..732c728122d0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FloatUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FloatUtil.kt @@ -18,4 +18,15 @@ public object FloatUtil { java.lang.Float.isNaN(f1) && java.lang.Float.isNaN(f2) } else abs(f2 - f1) < EPSILON } + + @JvmStatic + public fun floatsEqual(f1: Float?, f2: Float?): Boolean { + if (f1 == null) { + return f2 == null + } else if (f2 == null) { + return false + } + + return floatsEqual(f1, f2) + } } 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..21094fc73d5a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java @@ -0,0 +1,1336 @@ +/* + * 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 com.facebook.react.uimanager.style.BorderRadiusProp; +import com.facebook.react.uimanager.style.BorderRadiusStyle; +import com.facebook.react.uimanager.style.ComputedBorderRadius; +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; + + /* 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 BorderRadiusStyle mBorderRadius = new BorderRadiusStyle(); + private final Context mContext; + private int mLayoutDirection; + + public CSSBackgroundDrawable(Context context) { + mContext = context; + } + + @Override + public void draw(Canvas canvas) { + updatePathEffect(); + if (!hasRoundedBorders()) { + drawRectangularBackgroundWithBorders(canvas); + } else { + drawRoundedBackgroundWithBorders(canvas); + } + } + + public boolean hasRoundedBorders() { + return mBorderRadius.hasRoundedBorders(); + } + + @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 (hasRoundedBorders()) { + 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(); + } + } + + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ + public void setRadius(float radius) { + @Nullable Float boxedRadius = Float.isNaN(radius) ? null : Float.valueOf(radius); + setBorderRadius(BorderRadiusProp.BORDER_RADIUS, boxedRadius); + } + + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ + public void setRadius(float radius, int position) { + @Nullable Float boxedRadius = Float.isNaN(radius) ? null : Float.valueOf(radius); + setBorderRadius(BorderRadiusProp.values()[position], boxedRadius); + } + + public void setBorderRadius(BorderRadiusProp property, @Nullable Float radius) { + if (!FloatUtil.floatsEqual(mBorderRadius.getUniform(), radius)) { + mBorderRadius.set(property, radius); + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setBorderRadius(BorderRadiusStyle radius) { + mBorderRadius = radius; + } + + public BorderRadiusStyle getBorderRadius() { + return mBorderRadius; + } + + 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; + + ComputedBorderRadius radius = mBorderRadius.resolve(mLayoutDirection, mContext); + float topLeftRadius = radius.getTopLeft(); + float topRightRadius = radius.getTopRight(); + float bottomLeftRadius = radius.getBottomLeft(); + float bottomRightRadius = radius.getBottomRight(); + + 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/uimanager/style/BorderRadiusStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt new file mode 100644 index 000000000000..0683b01b073c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderRadiusStyle.kt @@ -0,0 +1,131 @@ +/* + * 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.style + +import android.content.Context +import android.util.LayoutDirection +import com.facebook.react.modules.i18nmanager.I18nUtil + +/** Represents the collection of possible border radius style properties. */ +public enum class BorderRadiusProp { + BORDER_TOP_LEFT_RADIUS, + BORDER_TOP_RIGHT_RADIUS, + BORDER_BOTTOM_RIGHT_RADIUS, + BORDER_BOTTOM_LEFT_RADIUS, + BORDER_TOP_START_RADIUS, + BORDER_TOP_END_RADIUS, + BORDER_BOTTOM_START_RADIUS, + BORDER_BOTTOM_END_RADIUS, + BORDER_END_END_RADIUS, + BORDER_END_START_RADIUS, + BORDER_START_END_RADIUS, + BORDER_START_START_RADIUS, + BORDER_RADIUS, +} + +/** Represents all logical properties and shorthands for border radius. */ +public data class BorderRadiusStyle( + var uniform: Float? = null, + var topLeft: Float? = null, + var topRight: Float? = null, + var bottomLeft: Float? = null, + var bottomRight: Float? = null, + var topStart: Float? = null, + var topEnd: Float? = null, + var bottomStart: Float? = null, + var bottomEnd: Float? = null, + var startStart: Float? = null, + var startEnd: Float? = null, + var endStart: Float? = null, + var endEnd: Float? = null +) { + public constructor(properties: List>) : this() { + properties.forEach { (k, v) -> set(k, v) } + } + + public fun set(property: BorderRadiusProp, value: Float?) { + when (property) { + BorderRadiusProp.BORDER_RADIUS -> uniform = value + BorderRadiusProp.BORDER_TOP_LEFT_RADIUS -> topLeft = value + BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS -> topRight = value + BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS -> bottomLeft = value + BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS -> bottomRight = value + BorderRadiusProp.BORDER_TOP_START_RADIUS -> topStart = value + BorderRadiusProp.BORDER_TOP_END_RADIUS -> topEnd = value + BorderRadiusProp.BORDER_BOTTOM_START_RADIUS -> bottomStart = value + BorderRadiusProp.BORDER_BOTTOM_END_RADIUS -> bottomEnd = value + BorderRadiusProp.BORDER_START_START_RADIUS -> startStart = value + BorderRadiusProp.BORDER_START_END_RADIUS -> startEnd = value + BorderRadiusProp.BORDER_END_START_RADIUS -> endStart = value + BorderRadiusProp.BORDER_END_END_RADIUS -> endEnd = value + } + } + + public fun get(property: BorderRadiusProp): Float? { + return when (property) { + BorderRadiusProp.BORDER_RADIUS -> uniform + BorderRadiusProp.BORDER_TOP_LEFT_RADIUS -> topLeft + BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS -> topRight + BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS -> bottomLeft + BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS -> bottomRight + BorderRadiusProp.BORDER_TOP_START_RADIUS -> topStart + BorderRadiusProp.BORDER_TOP_END_RADIUS -> topEnd + BorderRadiusProp.BORDER_BOTTOM_START_RADIUS -> bottomStart + BorderRadiusProp.BORDER_BOTTOM_END_RADIUS -> bottomEnd + BorderRadiusProp.BORDER_START_START_RADIUS -> startStart + BorderRadiusProp.BORDER_START_END_RADIUS -> startEnd + BorderRadiusProp.BORDER_END_START_RADIUS -> endStart + BorderRadiusProp.BORDER_END_END_RADIUS -> endEnd + } + } + + public fun hasRoundedBorders(): Boolean { + return ((uniform ?: 0f) > 0f) || + ((topLeft ?: 0f) > 0f) || + ((topRight ?: 0f) > 0f) || + ((bottomLeft ?: 0f) > 0f) || + ((bottomRight ?: 0f) > 0f) || + ((topStart ?: 0f) > 0f) || + ((topEnd ?: 0f) > 0f) || + ((bottomStart ?: 0f) > 0f) || + ((bottomEnd ?: 0f) > 0f) || + ((startStart ?: 0f) > 0f) || + ((startEnd ?: 0f) > 0f) || + ((endStart ?: 0f) > 0f) || + ((endEnd ?: 0f) > 0f) + } + + public fun resolve( + layoutDirection: Int, + context: Context, + ): ComputedBorderRadius { + return when (layoutDirection) { + LayoutDirection.LTR -> + ComputedBorderRadius( + topLeft = startStart ?: topStart ?: topLeft ?: uniform ?: 0f, + topRight = endStart ?: topEnd ?: topRight ?: uniform ?: 0f, + bottomLeft = startEnd ?: bottomStart ?: bottomLeft ?: uniform ?: 0f, + bottomRight = endEnd ?: bottomEnd ?: bottomRight ?: uniform ?: 0f) + LayoutDirection.RTL -> + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(context)) { + ComputedBorderRadius( + topLeft = endStart ?: topEnd ?: topRight ?: uniform ?: 0f, + topRight = startStart ?: topStart ?: topLeft ?: uniform ?: 0f, + bottomLeft = endEnd ?: bottomStart ?: bottomRight ?: uniform ?: 0f, + bottomRight = startEnd ?: bottomEnd ?: bottomLeft ?: uniform ?: 0f) + } else { + ComputedBorderRadius( + topLeft = endStart ?: topEnd ?: topLeft ?: uniform ?: 0f, + topRight = startStart ?: topStart ?: topRight ?: uniform ?: 0f, + bottomLeft = endEnd ?: bottomStart ?: bottomLeft ?: uniform ?: 0f, + bottomRight = startEnd ?: bottomEnd ?: bottomRight ?: uniform ?: 0f) + } + else -> throw IllegalArgumentException("Expected resolved layout direction") + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt new file mode 100644 index 000000000000..181bc4ecba09 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ComputedBorderRadius.kt @@ -0,0 +1,20 @@ +/* + * 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.style + +/** Phsysical edge lengths (in DIPs) for a border-radius. */ +public data class ComputedBorderRadius( + val topLeft: Float, + val topRight: Float, + val bottomLeft: Float, + val bottomRight: Float, +) { + public fun hasRoundedBorders(): Boolean { + return topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.kt index d0f1644d106b..8361293a021a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.kt @@ -7,7 +7,6 @@ package com.facebook.react.views.view -import android.graphics.PixelFormat import kotlin.math.roundToInt /** @@ -17,43 +16,6 @@ import kotlin.math.roundToInt */ public object ColorUtil { - /** - * Multiplies the color with the given alpha. - * - * @param color color to be multiplied - * @param alpha value between 0 and 255 - * @return multiplied color - */ - @JvmStatic - public fun multiplyColorAlpha(color: Int, alpha: Int): Int { - if (alpha == 255) { - return color - } else if (alpha == 0) { - return color and 0x00FFFFFF - } - - val scaledAlpha = alpha + (alpha shr 7) // make it 0..256 - val colorAlpha = color ushr 24 - val multipliedAlpha = (colorAlpha * scaledAlpha) shr 8 - return (multipliedAlpha shl 24) or (color and 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 - */ - @JvmStatic - public fun getOpacityFromColor(color: Int): Int { - val colorAlpha = color ushr 24 - return when (colorAlpha) { - 255 -> PixelFormat.OPAQUE - 0 -> PixelFormat.TRANSPARENT - else -> 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/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index c24a5b359192..d0bff356bb3b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -52,7 +52,9 @@ import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.common.UIManagerType; import com.facebook.react.uimanager.common.ViewUtil; -import com.facebook.yoga.YogaConstants; +import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable; +import com.facebook.react.uimanager.style.BorderRadiusProp; +import com.facebook.react.uimanager.style.ComputedBorderRadius; /** * Backing for a React View. Has support for borders, but since borders aren't common, lazy @@ -315,16 +317,27 @@ public void setBorderColor(int position, float rgb, float alpha) { getOrCreateReactViewBackground().setBorderColor(position, rgb, alpha); } + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ public void setBorderRadius(float borderRadius) { - ReactViewBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); + CSSBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); backgroundDrawable.setRadius(borderRadius); } + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ public void setBorderRadius(float borderRadius, int position) { - ReactViewBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); + CSSBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); backgroundDrawable.setRadius(borderRadius, position); } + public void setBorderRadius(BorderRadiusProp property, @Nullable Float borderRadius) { + CSSBackgroundDrawable backgroundDrawable = getOrCreateReactViewBackground(); + backgroundDrawable.setBorderRadius(property, borderRadius); + } + public void setBorderStyle(@Nullable String style) { getOrCreateReactViewBackground().setBorderStyle(style); } @@ -914,95 +927,10 @@ private void dispatchOverflowDraw(Canvas canvas) { bottom -= borderWidth.bottom; } - final float borderRadius = mReactBackgroundDrawable.getFullBorderRadius(); - float topLeftBorderRadius = - mReactBackgroundDrawable.getBorderRadiusOrDefaultTo( - borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_LEFT); - float topRightBorderRadius = - mReactBackgroundDrawable.getBorderRadiusOrDefaultTo( - borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_RIGHT); - float bottomLeftBorderRadius = - mReactBackgroundDrawable.getBorderRadiusOrDefaultTo( - borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_LEFT); - float bottomRightBorderRadius = - mReactBackgroundDrawable.getBorderRadiusOrDefaultTo( - borderRadius, ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_RIGHT); - - final boolean isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL; - float topStartBorderRadius = - mReactBackgroundDrawable.getBorderRadius( - ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_START); - float topEndBorderRadius = - mReactBackgroundDrawable.getBorderRadius( - ReactViewBackgroundDrawable.BorderRadiusLocation.TOP_END); - float bottomStartBorderRadius = - mReactBackgroundDrawable.getBorderRadius( - ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_START); - float bottomEndBorderRadius = - mReactBackgroundDrawable.getBorderRadius( - ReactViewBackgroundDrawable.BorderRadiusLocation.BOTTOM_END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(getContext())) { - if (YogaConstants.isUndefined(topStartBorderRadius)) { - topStartBorderRadius = topLeftBorderRadius; - } - - if (YogaConstants.isUndefined(topEndBorderRadius)) { - topEndBorderRadius = topRightBorderRadius; - } - - if (YogaConstants.isUndefined(bottomStartBorderRadius)) { - bottomStartBorderRadius = bottomLeftBorderRadius; - } - - if (YogaConstants.isUndefined(bottomEndBorderRadius)) { - bottomEndBorderRadius = bottomRightBorderRadius; - } - - final float directionAwareTopLeftRadius = - isRTL ? topEndBorderRadius : topStartBorderRadius; - final float directionAwareTopRightRadius = - isRTL ? topStartBorderRadius : topEndBorderRadius; - final float directionAwareBottomLeftRadius = - isRTL ? bottomEndBorderRadius : bottomStartBorderRadius; - final float directionAwareBottomRightRadius = - isRTL ? bottomStartBorderRadius : bottomEndBorderRadius; - - topLeftBorderRadius = directionAwareTopLeftRadius; - topRightBorderRadius = directionAwareTopRightRadius; - bottomLeftBorderRadius = directionAwareBottomLeftRadius; - bottomRightBorderRadius = directionAwareBottomRightRadius; - } else { - final float directionAwareTopLeftRadius = - isRTL ? topEndBorderRadius : topStartBorderRadius; - final float directionAwareTopRightRadius = - isRTL ? topStartBorderRadius : topEndBorderRadius; - final float directionAwareBottomLeftRadius = - isRTL ? bottomEndBorderRadius : bottomStartBorderRadius; - final float directionAwareBottomRightRadius = - isRTL ? bottomStartBorderRadius : bottomEndBorderRadius; - - if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) { - topLeftBorderRadius = directionAwareTopLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) { - topRightBorderRadius = directionAwareTopRightRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) { - bottomLeftBorderRadius = directionAwareBottomLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) { - bottomRightBorderRadius = directionAwareBottomRightRadius; - } - } + final ComputedBorderRadius borderRadius = + mReactBackgroundDrawable.getBorderRadius().resolve(mLayoutDirection, getContext()); - if (topLeftBorderRadius > 0 - || topRightBorderRadius > 0 - || bottomRightBorderRadius > 0 - || bottomLeftBorderRadius > 0) { + if (borderRadius.hasRoundedBorders()) { if (mPath == null) { mPath = new Path(); } @@ -1011,14 +939,14 @@ private void dispatchOverflowDraw(Canvas canvas) { mPath.addRoundRect( new RectF(left, top, right, bottom), new float[] { - Math.max(topLeftBorderRadius - borderWidth.left, 0), - Math.max(topLeftBorderRadius - borderWidth.top, 0), - Math.max(topRightBorderRadius - borderWidth.right, 0), - Math.max(topRightBorderRadius - borderWidth.top, 0), - Math.max(bottomRightBorderRadius - borderWidth.right, 0), - Math.max(bottomRightBorderRadius - borderWidth.bottom, 0), - Math.max(bottomLeftBorderRadius - borderWidth.left, 0), - Math.max(bottomLeftBorderRadius - borderWidth.bottom, 0), + Math.max(borderRadius.getTopLeft() - borderWidth.left, 0), + Math.max(borderRadius.getTopLeft() - borderWidth.top, 0), + Math.max(borderRadius.getBottomRight() - borderWidth.right, 0), + Math.max(borderRadius.getBottomRight() - borderWidth.top, 0), + Math.max(borderRadius.getBottomRight() - borderWidth.right, 0), + Math.max(borderRadius.getBottomRight() - borderWidth.bottom, 0), + Math.max(borderRadius.getBottomLeft() - borderWidth.left, 0), + Math.max(borderRadius.getBottomLeft() - borderWidth.bottom, 0), }, Path.Direction.CW); canvas.clipPath(mPath); 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))