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") + } + } +}