mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
f905f90468
commit
d9f684b1cf
+48
-19
@@ -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<BoxShadow>): Unit {
|
||||
if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) {
|
||||
return
|
||||
}
|
||||
|
||||
val outerShadows = mutableListOf<OutsetBoxShadowDrawable>()
|
||||
val innerShadows = mutableListOf<InsetBoxShadowDrawable>()
|
||||
|
||||
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
|
||||
|
||||
+1
-1
@@ -25,5 +25,5 @@ internal fun adjustRadiusForSpread(
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
return (radius + abs(spread) * spreadMultiplier).coerceAtLeast(0f)
|
||||
return (radius + spread * spreadMultiplier).coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
+136
-120
@@ -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)
|
||||
}
|
||||
|
||||
+4
-2
@@ -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)
|
||||
|
||||
+105
@@ -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<Float?>(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user