From d9f684b1cf7fb6824e25e1e948bfb30dec450464 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sat, 17 Aug 2024 09:21:06 -0700 Subject: [PATCH] Revamp InsetBoxShadowDrawable (#46074) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46074 This brings over the changes made to OutsetBoxShadowDrawable. Namely 1. Removing reliance on CSSBackgroundDrawable for drawing paths 2. Using BlurMaskFilter instead of RenderEffect 3. Removing RenderNode usage This should make the implementation, more reliable less memory intensive for large boxes, and compatible down to Android API 29. I changed previous gating to allow outset shadows for 28+, and inset for 29+. Changelog: [Android][Changed] - Revamp InsetBoxShadowDrawable Reviewed By: joevilches Differential Revision: D61348615 fbshipit-source-id: 97b63b5dce65224ca54b76c5318c219973fc09fa --- .../uimanager/BackgroundStyleApplicator.kt | 67 +++-- .../drawable/BoxShadowBorderRadius.kt | 2 +- .../drawable/CompositeBackgroundDrawable.kt | 4 + .../drawable/InsetBoxShadowDrawable.kt | 256 ++++++++++-------- .../drawable/OutsetBoxShadowDrawable.kt | 6 +- .../react/uimanager/style/BorderInsets.kt | 105 +++++++ 6 files changed, 298 insertions(+), 142 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderInsets.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 3365481f1e4..fd4e4520d34 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -14,14 +14,17 @@ import android.graphics.drawable.Drawable import android.os.Build import android.view.View import androidx.annotation.ColorInt -import androidx.annotation.RequiresApi -import com.facebook.common.logging.FLog import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.common.ViewUtil import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable +import com.facebook.react.uimanager.drawable.MIN_INSET_BOX_SHADOW_SDK_VERSION +import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable +import com.facebook.react.uimanager.style.BorderInsets import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.BorderStyle @@ -51,9 +54,21 @@ public object BackgroundStyleApplicator { public fun getBackgroundColor(view: View): Int? = getCSSBackground(view)?.color @JvmStatic - public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit = - ensureCSSBackground(view) - .setBorderWidth(edge.toSpacingType(), PixelUtil.toPixelFromDIP(width ?: Float.NaN)) + public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit { + ensureCSSBackground(view) + .setBorderWidth(edge.toSpacingType(), PixelUtil.toPixelFromDIP(width ?: Float.NaN)) + + if (Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { + val composite = ensureCompositeBackgroundDrawable(view) + composite.borderInsets = composite.borderInsets ?: BorderInsets() + composite.borderInsets?.setBorderWidth(edge, width) + + for (shadow in composite.innerShadows) { + (shadow as InsetBoxShadowDrawable).borderInsets = composite.borderInsets + shadow.invalidateSelf() + } + } + } @JvmStatic public fun getBorderWidth(view: View, edge: LogicalEdge): Float? { @@ -77,12 +92,25 @@ public object BackgroundStyleApplicator { radius: LengthPercentage? ): Unit { ensureCSSBackground(view).setBorderRadius(corner, radius) + val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view) - if (Build.VERSION.SDK_INT >= 31) { - getCompositeBackgroundDrawable(view)?.outerShadows?.forEach { shadow -> - val outsetShadow = shadow as OutsetBoxShadowDrawable - outsetShadow.borderRadius = outsetShadow.borderRadius ?: BorderRadiusStyle() - outsetShadow.borderRadius?.set(corner, radius) + if (Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) { + for (shadow in compositeBackgroundDrawable.outerShadows) { + if (shadow is OutsetBoxShadowDrawable) { + shadow.borderRadius = shadow.borderRadius ?: BorderRadiusStyle() + shadow.borderRadius?.set(corner, radius) + shadow.invalidateSelf() + } + } + } + + if (Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { + for (shadow in compositeBackgroundDrawable.innerShadows) { + if (shadow is InsetBoxShadowDrawable) { + shadow.borderRadius = shadow.borderRadius ?: BorderRadiusStyle() + shadow.borderRadius?.set(corner, radius) + shadow.invalidateSelf() + } } } } @@ -100,11 +128,16 @@ public object BackgroundStyleApplicator { public fun getBorderStyle(view: View): BorderStyle? = getCSSBackground(view)?.borderStyle @JvmStatic - @RequiresApi(31) public fun setBoxShadow(view: View, shadows: List): Unit { + if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { + return + } + val outerShadows = mutableListOf() val innerShadows = mutableListOf() + val borderInsets = ensureCompositeBackgroundDrawable(view).borderInsets + for (boxShadow in shadows) { val offsetX = boxShadow.offsetX val offsetY = boxShadow.offsetY @@ -113,17 +146,18 @@ public object BackgroundStyleApplicator { val spreadDistance = boxShadow.spreadDistance ?: 0f val inset = boxShadow.inset ?: false - if (inset) { + if (inset && Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) { innerShadows.add( InsetBoxShadowDrawable( context = view.context, - background = ensureCSSBackground(view), + borderRadius = ensureCSSBackground(view).borderRadius, + borderInsets = borderInsets, shadowColor = color, offsetX = offsetX, offsetY = offsetY, blurRadius = blurRadius, spread = spreadDistance)) - } else { + } else if (!inset && Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) { outerShadows.add( OutsetBoxShadowDrawable( context = view.context, @@ -143,11 +177,6 @@ public object BackgroundStyleApplicator { @JvmStatic public fun setBoxShadow(view: View, shadows: ReadableArray?): Unit { - if (Build.VERSION.SDK_INT < 31) { - FLog.w("BackgroundStyleApplicator", "\"boxShadow\" requires Android 12 or later") - return - } - if (shadows == null) { BackgroundStyleApplicator.setBoxShadow(view, emptyList()) return diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowBorderRadius.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowBorderRadius.kt index 98365f7a86f..0e4fbe0c283 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowBorderRadius.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowBorderRadius.kt @@ -25,5 +25,5 @@ internal fun adjustRadiusForSpread( } else { 1f } - return (radius + abs(spread) * spreadMultiplier).coerceAtLeast(0f) + return (radius + spread * spreadMultiplier).coerceAtLeast(0f) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt index cdf83e51fad..1da46a7d6b2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt @@ -10,6 +10,7 @@ package com.facebook.react.uimanager.drawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.uimanager.style.BorderInsets /** * CompositeBackgroundDrawable can overlay multiple different layers, shadows, and native effects @@ -52,6 +53,9 @@ internal class CompositeBackgroundDrawable( *innerShadows.asReversed().toTypedArray()) .toTypedArray()) { + // Holder value for currently set insets + public var borderInsets: BorderInsets? = null + init { // We want to overlay drawables, instead of placing future drawables within the content area of // previous ones. E.g. an EditText style may set padding on a TextInput, but we don't want to diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.kt index 0a3d4fe12e7..1f7aa5e3f01 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.kt @@ -8,171 +8,187 @@ package com.facebook.react.uimanager.drawable import android.content.Context +import android.graphics.BlurMaskFilter import android.graphics.Canvas +import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RenderNode +import android.graphics.Path +import android.graphics.RectF import android.graphics.drawable.Drawable import androidx.annotation.RequiresApi -import com.facebook.common.logging.FLog -import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.uimanager.FilterHelper -import com.facebook.react.uimanager.LengthPercentage -import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.style.BorderInsets import com.facebook.react.uimanager.style.BorderRadiusStyle +import com.facebook.react.uimanager.style.ComputedBorderRadius +import com.facebook.react.uimanager.style.CornerRadii import kotlin.math.roundToInt -private const val TAG = "InsetBoxShadowDrawable" +internal const val MIN_INSET_BOX_SHADOW_SDK_VERSION = 29 // "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal // to half the blur radius" // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f +private val ZERO_RADII = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) + /** Draws an inner box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */ -@RequiresApi(31) -@OptIn(UnstableReactNativeAPI::class) +@RequiresApi(MIN_INSET_BOX_SHADOW_SDK_VERSION) internal class InsetBoxShadowDrawable( private val context: Context, - private val background: CSSBackgroundDrawable, + borderRadius: BorderRadiusStyle? = null, + borderInsets: BorderInsets? = null, private val shadowColor: Int, private val offsetX: Float, private val offsetY: Float, private val blurRadius: Float, private val spread: Float, ) : Drawable() { - private val renderNode = - RenderNode(TAG).apply { - clipToBounds = false - setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE)) + public var borderRadius = borderRadius + set(value) { + if (value != field) { + field = value + invalidateSelf() } + } - private val clearRegionDrawable = CSSBackgroundDrawable(context) + public var borderInsets = borderInsets + set(value) { + if (value != field) { + field = value + invalidateSelf() + } + } - private val shadowPaint = Paint().apply { color = shadowColor } + private val shadowPaint = + Paint().apply { + color = shadowColor + if (blurRadius > 0) { + maskFilter = + BlurMaskFilter( + FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE), + BlurMaskFilter.Blur.NORMAL) + } + } override fun setAlpha(alpha: Int) { - renderNode.alpha = alpha / 255f + shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt() + invalidateSelf() } - override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit + override fun setColorFilter(colorFilter: ColorFilter?) { + shadowPaint.colorFilter = colorFilter + invalidateSelf() + } - override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt() + override fun getOpacity(): Int = + ((shadowPaint.alpha / 255f) / (Color.alpha(shadowColor) / 255f) * 255f).roundToInt() override fun draw(canvas: Canvas) { - if (!canvas.isHardwareAccelerated) { - FLog.w(TAG, "InsetBoxShadowDrawable requires a hardware accelerated canvas") - return - } - // We need the actual size the blur will increase the shadow by so we can - // properly pad. This is not simply the input as Android has it's own - // distinct blur algorithm - val adjustedBlurRadius = - FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE).roundToInt() - val padding = 2 * adjustedBlurRadius + val computedBorderRadii = computeBorderRadii() + val computedBorderInsets = computeBorderInsets() - val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0) - val clearRegionBounds = Rect() - background.getPaddingBoxRect().round(clearRegionBounds) - clearRegionBounds.inset(spreadExtent, spreadExtent) - clearRegionBounds.offset( - PixelUtil.toPixelFromDIP(offsetX).roundToInt() + padding / 2, - PixelUtil.toPixelFromDIP(offsetY).roundToInt() + padding / 2, - ) - val clearRegionBorderRadii = getClearRegionBorderRadii(spreadExtent, background) + val paddingBoxRect = + RectF( + bounds.left + (computedBorderInsets?.left ?: 0f), + bounds.top + (computedBorderInsets?.top ?: 0f), + bounds.right - (computedBorderInsets?.right ?: 0f), + bounds.bottom - (computedBorderInsets?.bottom ?: 0f)) + val paddingBoxRadii = + computedBorderRadii?.let { + floatArrayOf( + innerRadius(it.topLeft.horizontal, computedBorderInsets?.left), + innerRadius(it.topLeft.vertical, computedBorderInsets?.top), + innerRadius(it.topRight.horizontal, computedBorderInsets?.right), + innerRadius(it.topRight.vertical, computedBorderInsets?.top), + innerRadius(it.bottomRight.horizontal, computedBorderInsets?.right), + innerRadius(it.bottomRight.vertical, computedBorderInsets?.bottom), + innerRadius(it.bottomLeft.horizontal, computedBorderInsets?.left), + innerRadius(it.bottomLeft.vertical, computedBorderInsets?.bottom)) + } - if (!renderNode.hasDisplayList() || - shadowPaint.colorFilter != colorFilter || - clearRegionDrawable.layoutDirection != layoutDirection || - clearRegionDrawable.borderRadius != clearRegionBorderRadii || - clearRegionDrawable.bounds != clearRegionBounds) { + val x = PixelUtil.toPixelFromDIP(offsetX) + val y = PixelUtil.toPixelFromDIP(offsetY) + val spreadExtent = PixelUtil.toPixelFromDIP(spread) + val innerRect = + RectF(paddingBoxRect).apply { + inset(spreadExtent, spreadExtent) + offset(x, y) + } - shadowPaint.colorFilter = colorFilter - clearRegionDrawable.bounds = clearRegionBounds - clearRegionDrawable.layoutDirection = layoutDirection - clearRegionDrawable.borderRadius = clearRegionBorderRadii - - with(renderNode) { - // We pad by the blur radius so that the edges of the blur look good and - // the blur artifacts can bleed into the view if needed - setPosition(Rect(bounds).apply { inset(-padding, -padding) }) - beginRecording().let { renderNodeCanvas -> - val borderBoxPath = clearRegionDrawable.getBorderBoxPath() - if (borderBoxPath != null) { - renderNodeCanvas.clipOutPath(borderBoxPath) - } else { - renderNodeCanvas.clipOutRect(clearRegionDrawable.borderBoxRect) + // Graciously stolen from Blink + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/box_painter_base.cc;l=338;drc=0a301506035e13015ea5c8dd39164d0d5954fa60 + val blurExtent = FilterHelper.sigmaToRadius(blurRadius) + val outerRect = + RectF(innerRect).apply { + inset(-blurExtent, -blurExtent) + if (spreadExtent < 0) { + inset(spreadExtent, spreadExtent) } - - renderNodeCanvas.drawPaint(shadowPaint) - endRecording() + union(RectF(this).apply { offset(-x, -y) }) } + + canvas.save().let { saveCount -> + if (paddingBoxRadii != null) { + canvas.clipPath( + Path().apply { addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW) }) + + val innerRadii = + paddingBoxRadii.map { adjustRadiusForSpread(it, -spreadExtent) }.toFloatArray() + + canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint) + } else { + canvas.clipRect(paddingBoxRect) + canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint) } - // We actually draw the render node into our canvas and clip out the - // padding - with(canvas) { - save() - - val paddingBoxPath = background.getPaddingBoxPath() - if (paddingBoxPath != null) { - canvas.clipPath(paddingBoxPath) - } else { - canvas.clipRect(background.getPaddingBoxRect()) - } - // This positions the render node properly since we padded it - canvas.translate(padding / 2f, padding / 2f) - drawRenderNode(renderNode) - restore() - } + canvas.restoreToCount(saveCount) } } - // TODO: Remove usage of unsafe `CSSBackgroundDrawable.getComputedBorderRadius` - @Suppress("DEPRECATION") - private fun getClearRegionBorderRadii( - spread: Int, - background: CSSBackgroundDrawable, - ): BorderRadiusStyle { - // Accessing this is super duper unsafe and only works because the CSSBackgroundDrawable renders - // first - val computedBorderRadii = background.computedBorderRadius - val borderWidth = background.getDirectionAwareBorderInsets() + private fun computeBorderRadii(): ComputedBorderRadius? { + val resolvedBorderRadii = + borderRadius?.resolve( + layoutDirection, + context, + PixelUtil.toDIPFromPixel(bounds.width().toFloat()), + PixelUtil.toDIPFromPixel(bounds.height().toFloat())) - val topLeftRadius = computedBorderRadii.topLeft.toPixelFromDIP() - val topRightRadius = computedBorderRadii.topRight.toPixelFromDIP() - val bottomLeftRadius = computedBorderRadii.bottomLeft.toPixelFromDIP() - val bottomRightRadius = computedBorderRadii.bottomRight.toPixelFromDIP() - - val innerTopLeftRadius = - background.getInnerBorderRadius(topLeftRadius.horizontal, borderWidth.left) - val innerTopRightRadius = - background.getInnerBorderRadius(topRightRadius.horizontal, borderWidth.right) - val innerBottomRightRadius = - background.getInnerBorderRadius(bottomRightRadius.horizontal, borderWidth.right) - val innerBottomLeftRadius = - background.getInnerBorderRadius(bottomLeftRadius.horizontal, borderWidth.left) - - val spreadWithDirection = -spread.toFloat() - return BorderRadiusStyle( - topLeft = - LengthPercentage( - adjustRadiusForSpread(innerTopLeftRadius, spreadWithDirection), - LengthPercentageType.POINT), - topRight = - LengthPercentage( - adjustRadiusForSpread(innerTopRightRadius, spreadWithDirection), - LengthPercentageType.POINT), - bottomLeft = - LengthPercentage( - adjustRadiusForSpread(innerBottomLeftRadius, spreadWithDirection), - LengthPercentageType.POINT), - bottomRight = - LengthPercentage( - adjustRadiusForSpread(innerBottomRightRadius, spreadWithDirection), - LengthPercentageType.POINT), - ) + return if (resolvedBorderRadii?.hasRoundedBorders() == true) { + ComputedBorderRadius( + topLeft = + CornerRadii( + PixelUtil.toPixelFromDIP(resolvedBorderRadii.topLeft.horizontal), + PixelUtil.toPixelFromDIP(resolvedBorderRadii.topLeft.vertical)), + topRight = + CornerRadii( + PixelUtil.toPixelFromDIP(resolvedBorderRadii.topRight.horizontal), + PixelUtil.toPixelFromDIP(resolvedBorderRadii.topRight.vertical)), + bottomLeft = + CornerRadii( + PixelUtil.toPixelFromDIP(resolvedBorderRadii.bottomLeft.horizontal), + PixelUtil.toPixelFromDIP(resolvedBorderRadii.bottomLeft.vertical)), + bottomRight = + CornerRadii( + PixelUtil.toPixelFromDIP(resolvedBorderRadii.bottomRight.horizontal), + PixelUtil.toPixelFromDIP(resolvedBorderRadii.bottomRight.vertical)), + ) + } else { + null + } } + + private fun computeBorderInsets(): RectF? = + borderInsets?.resolve(layoutDirection, context)?.let { + RectF( + PixelUtil.toPixelFromDIP(it.left), + PixelUtil.toPixelFromDIP(it.top), + PixelUtil.toPixelFromDIP(it.right), + PixelUtil.toPixelFromDIP(it.bottom)) + } + + private fun innerRadius(radius: Float, borderInset: Float?): Float = + (radius - (borderInset ?: 0f)).coerceAtLeast(0f) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.kt index 9365cc46b60..cfa798a8d1c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.kt @@ -24,13 +24,15 @@ import com.facebook.react.uimanager.style.ComputedBorderRadius import com.facebook.react.uimanager.style.CornerRadii import kotlin.math.roundToInt +internal const val MIN_OUTSET_BOX_SHADOW_SDK_VERSION = 28 + // "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal // to half the blur radius" // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f /** Draws an outer box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */ -@RequiresApi(28) +@RequiresApi(MIN_OUTSET_BOX_SHADOW_SDK_VERSION) internal class OutsetBoxShadowDrawable( private val context: Context, borderRadius: BorderRadiusStyle? = null, @@ -97,7 +99,7 @@ internal class OutsetBoxShadowDrawable( ) } - val spreadExtent = PixelUtil.toPixelFromDIP(spread).coerceAtLeast(0f) + val spreadExtent = PixelUtil.toPixelFromDIP(spread) val shadowRect = RectF(bounds).apply { inset(-spreadExtent, -spreadExtent) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderInsets.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderInsets.kt new file mode 100644 index 00000000000..4134b5b3f4c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderInsets.kt @@ -0,0 +1,105 @@ +/* + * 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.graphics.RectF +import android.util.LayoutDirection +import com.facebook.react.modules.i18nmanager.I18nUtil + +/** Represents the insets from border pox to padding box (i.e. border widths) */ +internal class BorderInsets { + private val edgeInsets = arrayOfNulls(LogicalEdge.values().size) + + public fun setBorderWidth(edge: LogicalEdge, width: Float?) { + edgeInsets[edge.ordinal] = width + } + + public fun resolve( + layoutDirection: Int, + context: Context, + ): RectF { + return when (layoutDirection) { + LayoutDirection.LTR -> + RectF( + edgeInsets[LogicalEdge.START.ordinal] + ?: edgeInsets[LogicalEdge.LEFT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_START.ordinal] + ?: edgeInsets[LogicalEdge.TOP.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.END.ordinal] + ?: edgeInsets[LogicalEdge.RIGHT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_END.ordinal] + ?: edgeInsets[LogicalEdge.BOTTOM.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f) + LayoutDirection.RTL -> + if (I18nUtil.instance.doLeftAndRightSwapInRTL(context)) { + RectF( + edgeInsets[LogicalEdge.END.ordinal] + ?: edgeInsets[LogicalEdge.RIGHT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_START.ordinal] + ?: edgeInsets[LogicalEdge.TOP.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.START.ordinal] + ?: edgeInsets[LogicalEdge.LEFT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_END.ordinal] + ?: edgeInsets[LogicalEdge.BOTTOM.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f) + } else { + RectF( + edgeInsets[LogicalEdge.END.ordinal] + ?: edgeInsets[LogicalEdge.LEFT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_START.ordinal] + ?: edgeInsets[LogicalEdge.TOP.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.START.ordinal] + ?: edgeInsets[LogicalEdge.RIGHT.ordinal] + ?: edgeInsets[LogicalEdge.HORIZONTAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f, + edgeInsets[LogicalEdge.BLOCK_END.ordinal] + ?: edgeInsets[LogicalEdge.BOTTOM.ordinal] + ?: edgeInsets[LogicalEdge.BLOCK.ordinal] + ?: edgeInsets[LogicalEdge.VERTICAL.ordinal] + ?: edgeInsets[LogicalEdge.ALL.ordinal] + ?: 0f) + } + else -> throw IllegalArgumentException("Expected resolved layout direction") + } + } +}