mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Workaround gap between border and outer box shadow (#45797)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45797 Android borders are drawn using a path generated by `addRoundRect()` inset by half the border width, using the full border width as stoke width. The edges of the ellipsis drawn for rounded borders do not line up with the math used to trace the bounding border-box path. In a relatively similar hack to elsewhere in border drawing code for gap between content and the border, we inset the clipOut path, as if its bounding rectangle were about half a subpixel smaller, to mininally overlap the border on these edges. We then place the outer box shadows under the border in z-ordering, so that the minimal extra insetting is only visible with transparent backgrounds. Changelog: [Internal] Reviewed By: joevilches Differential Revision: D60389685 fbshipit-source-id: 8c449cc3eee1a3e4100f06fd87f27ae341e02eac
This commit is contained in:
committed by
Facebook GitHub Bot
parent
32d040380f
commit
be3936762f
+22
-16
@@ -91,16 +91,19 @@ public object BackgroundStyleApplicator {
|
||||
@JvmStatic
|
||||
@RequiresApi(31)
|
||||
public fun setBoxShadow(view: View, shadows: List<BoxShadow>): Unit {
|
||||
val shadowDrawables =
|
||||
shadows.map { boxShadow ->
|
||||
val offsetX = boxShadow.offsetX
|
||||
val offsetY = boxShadow.offsetY
|
||||
val color = boxShadow.color ?: Color.BLACK
|
||||
val blurRadius = boxShadow.blurRadius ?: 0f
|
||||
val spreadDistance = boxShadow.spreadDistance ?: 0f
|
||||
val inset = boxShadow.inset ?: false
|
||||
val outerShadows = mutableListOf<OutsetBoxShadowDrawable>()
|
||||
val innerShadows = mutableListOf<InsetBoxShadowDrawable>()
|
||||
|
||||
if (inset) {
|
||||
for (boxShadow in shadows) {
|
||||
val offsetX = boxShadow.offsetX
|
||||
val offsetY = boxShadow.offsetY
|
||||
val color = boxShadow.color ?: Color.BLACK
|
||||
val blurRadius = boxShadow.blurRadius ?: 0f
|
||||
val spreadDistance = boxShadow.spreadDistance ?: 0f
|
||||
val inset = boxShadow.inset ?: false
|
||||
|
||||
if (inset) {
|
||||
innerShadows.add(
|
||||
InsetBoxShadowDrawable(
|
||||
context = view.context,
|
||||
background = ensureCSSBackground(view),
|
||||
@@ -108,8 +111,9 @@ public object BackgroundStyleApplicator {
|
||||
offsetX = offsetX,
|
||||
offsetY = offsetY,
|
||||
blurRadius = blurRadius,
|
||||
spread = spreadDistance)
|
||||
} else {
|
||||
spread = spreadDistance))
|
||||
} else {
|
||||
outerShadows.add(
|
||||
OutsetBoxShadowDrawable(
|
||||
context = view.context,
|
||||
background = ensureCSSBackground(view),
|
||||
@@ -117,11 +121,13 @@ public object BackgroundStyleApplicator {
|
||||
offsetX = offsetX,
|
||||
offsetY = offsetY,
|
||||
blurRadius = blurRadius,
|
||||
spread = spreadDistance)
|
||||
}
|
||||
}
|
||||
spread = spreadDistance))
|
||||
}
|
||||
}
|
||||
|
||||
view.background = ensureCompositeBackgroundDrawable(view).withNewShadows(shadowDrawables)
|
||||
view.background =
|
||||
ensureCompositeBackgroundDrawable(view)
|
||||
.withNewShadows(outerShadows = outerShadows, innerShadows = innerShadows)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -165,7 +171,7 @@ public object BackgroundStyleApplicator {
|
||||
return view.background as CompositeBackgroundDrawable
|
||||
}
|
||||
|
||||
val compositeDrawable = CompositeBackgroundDrawable(view.background, null, emptyList(), null)
|
||||
val compositeDrawable = CompositeBackgroundDrawable(originalBackground = view.background)
|
||||
view.background = compositeDrawable
|
||||
return compositeDrawable
|
||||
}
|
||||
|
||||
+17
-43
@@ -7,49 +7,23 @@
|
||||
|
||||
package com.facebook.react.uimanager.drawable
|
||||
|
||||
import com.facebook.react.uimanager.LengthPercentage
|
||||
import com.facebook.react.uimanager.LengthPercentageType
|
||||
import com.facebook.react.uimanager.style.BorderRadiusProp
|
||||
import com.facebook.react.uimanager.style.BorderRadiusStyle
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
internal fun getShadowBorderRadii(
|
||||
/**
|
||||
* Manipulates a corner radius of the shadow-shape according to the radius of the original
|
||||
* background, and the applied spread (negative, if inset). See algorithm at
|
||||
* https://drafts.csswg.org/css-backgrounds/#shadow-shape
|
||||
*/
|
||||
internal fun adjustRadiusForSpread(
|
||||
radius: Float,
|
||||
spread: Float,
|
||||
backgroundBorderRadii: BorderRadiusStyle,
|
||||
width: Float,
|
||||
height: Float,
|
||||
): BorderRadiusStyle {
|
||||
val adjustedBorderRadii = BorderRadiusStyle()
|
||||
val borderRadiusProps = BorderRadiusProp.values()
|
||||
|
||||
borderRadiusProps.forEach { borderRadiusProp ->
|
||||
val borderRadius = backgroundBorderRadii.get(borderRadiusProp)
|
||||
adjustedBorderRadii.set(
|
||||
borderRadiusProp,
|
||||
if (borderRadius == null) null
|
||||
else adjustedBorderRadius(spread, borderRadius, width, height))
|
||||
}
|
||||
|
||||
return adjustedBorderRadii
|
||||
}
|
||||
|
||||
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape
|
||||
private fun adjustedBorderRadius(
|
||||
spread: Float,
|
||||
backgroundBorderRadius: LengthPercentage?,
|
||||
width: Float,
|
||||
height: Float,
|
||||
): LengthPercentage? {
|
||||
if (backgroundBorderRadius == null) {
|
||||
return null
|
||||
}
|
||||
var adjustment = spread
|
||||
val backgroundBorderRadiusValue = backgroundBorderRadius.resolve(width, height)
|
||||
|
||||
if (backgroundBorderRadiusValue < Math.abs(spread)) {
|
||||
val r = backgroundBorderRadiusValue / Math.abs(spread)
|
||||
val p = Math.pow(r - 1.0, 3.0)
|
||||
adjustment *= 1.0f + p.toFloat()
|
||||
}
|
||||
|
||||
return LengthPercentage(backgroundBorderRadiusValue + adjustment, LengthPercentageType.POINT)
|
||||
): Float {
|
||||
val spreadMultiplier =
|
||||
if (radius < abs(spread)) {
|
||||
1 + (radius / abs(spread) - 1).pow(3)
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
return (radius + abs(spread) * spreadMultiplier).coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
+18
-8
@@ -23,6 +23,9 @@ internal class CompositeBackgroundDrawable(
|
||||
*/
|
||||
public val originalBackground: Drawable? = null,
|
||||
|
||||
/** Non-inset box shadows */
|
||||
public val outerShadows: List<Drawable> = emptyList(),
|
||||
|
||||
/**
|
||||
* CSS background layer and border rendering
|
||||
*
|
||||
@@ -31,8 +34,8 @@ internal class CompositeBackgroundDrawable(
|
||||
*/
|
||||
public val cssBackground: CSSBackgroundDrawable? = null,
|
||||
|
||||
/** Inner and outer box shadows */
|
||||
public val shadows: List<Drawable> = emptyList(),
|
||||
/** Inset box-shadows */
|
||||
public val innerShadows: List<Drawable> = emptyList(),
|
||||
|
||||
/** Native riplple effect (e.g. used by TouchableNativeFeedback) */
|
||||
public val nativeRipple: Drawable? = null
|
||||
@@ -40,11 +43,12 @@ internal class CompositeBackgroundDrawable(
|
||||
LayerDrawable(
|
||||
listOfNotNull(
|
||||
originalBackground,
|
||||
cssBackground,
|
||||
// z-ordering of user-provided shadow-list is opposite direction of LayerDrawable
|
||||
// z-ordering
|
||||
// https://drafts.csswg.org/css-backgrounds/#shadow-layers
|
||||
*shadows.asReversed().toTypedArray(),
|
||||
*outerShadows.asReversed().toTypedArray(),
|
||||
cssBackground,
|
||||
*innerShadows.asReversed().toTypedArray(),
|
||||
nativeRipple)
|
||||
.toTypedArray()) {
|
||||
|
||||
@@ -58,14 +62,20 @@ internal class CompositeBackgroundDrawable(
|
||||
public fun withNewCssBackground(
|
||||
cssBackground: CSSBackgroundDrawable?
|
||||
): CompositeBackgroundDrawable {
|
||||
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, nativeRipple)
|
||||
return CompositeBackgroundDrawable(
|
||||
originalBackground, outerShadows, cssBackground, innerShadows, nativeRipple)
|
||||
}
|
||||
|
||||
public fun withNewShadows(newShadows: List<Drawable>): CompositeBackgroundDrawable {
|
||||
return CompositeBackgroundDrawable(originalBackground, cssBackground, newShadows, nativeRipple)
|
||||
public fun withNewShadows(
|
||||
outerShadows: List<Drawable>,
|
||||
innerShadows: List<Drawable>
|
||||
): CompositeBackgroundDrawable {
|
||||
return CompositeBackgroundDrawable(
|
||||
originalBackground, outerShadows, cssBackground, innerShadows, nativeRipple)
|
||||
}
|
||||
|
||||
public fun withNewNativeRipple(newRipple: Drawable?): CompositeBackgroundDrawable {
|
||||
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, newRipple)
|
||||
return CompositeBackgroundDrawable(
|
||||
originalBackground, outerShadows, cssBackground, innerShadows, newRipple)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-27
@@ -21,7 +21,6 @@ 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.BorderRadiusProp
|
||||
import com.facebook.react.uimanager.style.BorderRadiusStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -82,8 +81,7 @@ internal class InsetBoxShadowDrawable(
|
||||
PixelUtil.toPixelFromDIP(offsetX).roundToInt() + padding / 2,
|
||||
PixelUtil.toPixelFromDIP(offsetY).roundToInt() + padding / 2,
|
||||
)
|
||||
val clearRegionBorderRadii =
|
||||
getClearRegionBorderRadii(spreadExtent, background, clearRegionBounds)
|
||||
val clearRegionBorderRadii = getClearRegionBorderRadii(spreadExtent, background)
|
||||
|
||||
if (shadowPaint.colorFilter != colorFilter ||
|
||||
clearRegionDrawable.layoutDirection != layoutDirection ||
|
||||
@@ -134,7 +132,6 @@ internal class InsetBoxShadowDrawable(
|
||||
private fun getClearRegionBorderRadii(
|
||||
spread: Int,
|
||||
background: CSSBackgroundDrawable,
|
||||
clearRegionBounds: Rect,
|
||||
): BorderRadiusStyle {
|
||||
val computedBorderRadii = background.computedBorderRadius
|
||||
val borderWidth = background.getDirectionAwareBorderInsets()
|
||||
@@ -150,29 +147,24 @@ internal class InsetBoxShadowDrawable(
|
||||
background.getInnerBorderRadius(bottomRightRadius, borderWidth.right)
|
||||
val innerBottomLeftRadius = background.getInnerBorderRadius(bottomLeftRadius, borderWidth.left)
|
||||
|
||||
val innerBorderRadii = BorderRadiusStyle()
|
||||
innerBorderRadii.set(
|
||||
BorderRadiusProp.BORDER_TOP_LEFT_RADIUS,
|
||||
LengthPercentage(innerTopLeftRadius, LengthPercentageType.POINT),
|
||||
)
|
||||
innerBorderRadii.set(
|
||||
BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS,
|
||||
LengthPercentage(innerTopRightRadius, LengthPercentageType.POINT),
|
||||
)
|
||||
innerBorderRadii.set(
|
||||
BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS,
|
||||
LengthPercentage(innerBottomRightRadius, LengthPercentageType.POINT),
|
||||
)
|
||||
innerBorderRadii.set(
|
||||
BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS,
|
||||
LengthPercentage(innerBottomLeftRadius, LengthPercentageType.POINT),
|
||||
)
|
||||
|
||||
return getShadowBorderRadii(
|
||||
-spread.toFloat(),
|
||||
innerBorderRadii,
|
||||
clearRegionBounds.width().toFloat(),
|
||||
clearRegionBounds.height().toFloat(),
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+56
-13
@@ -10,14 +10,20 @@ package com.facebook.react.uimanager.drawable
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.RenderNode
|
||||
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.BorderRadiusStyle
|
||||
import com.facebook.react.uimanager.style.ComputedBorderRadius
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "OutsetBoxShadowDrawable"
|
||||
@@ -31,7 +37,7 @@ private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f
|
||||
@RequiresApi(31)
|
||||
@OptIn(UnstableReactNativeAPI::class)
|
||||
internal class OutsetBoxShadowDrawable(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
private val background: CSSBackgroundDrawable,
|
||||
shadowColor: Int,
|
||||
private val offsetX: Float,
|
||||
@@ -47,6 +53,8 @@ internal class OutsetBoxShadowDrawable(
|
||||
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE))
|
||||
}
|
||||
|
||||
private val shadowClipOutPath: Path = Path()
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
renderNode.alpha = alpha / 255f
|
||||
}
|
||||
@@ -64,22 +72,58 @@ internal class OutsetBoxShadowDrawable(
|
||||
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
|
||||
val shadowShapeFrame = Rect(bounds).apply { inset(-spreadExtent, -spreadExtent) }
|
||||
val shadowShapeBounds = Rect(0, 0, shadowShapeFrame.width(), shadowShapeFrame.height())
|
||||
val borderRadii =
|
||||
getShadowBorderRadii(
|
||||
spreadExtent.toFloat(),
|
||||
background.borderRadius,
|
||||
bounds.width().toFloat(),
|
||||
bounds.height().toFloat())
|
||||
|
||||
val resolutionWidth = bounds.width().toFloat()
|
||||
val resolutionHeight = bounds.height().toFloat()
|
||||
val computedBorderRadii =
|
||||
background.borderRadius.resolve(layoutDirection, context, resolutionWidth, resolutionHeight)
|
||||
val shadowBorderRadii =
|
||||
ComputedBorderRadius(
|
||||
topLeft = adjustRadiusForSpread(computedBorderRadii.topLeft, spreadExtent.toFloat()),
|
||||
topRight = adjustRadiusForSpread(computedBorderRadii.topRight, spreadExtent.toFloat()),
|
||||
bottomRight =
|
||||
adjustRadiusForSpread(computedBorderRadii.bottomRight, spreadExtent.toFloat()),
|
||||
bottomLeft =
|
||||
adjustRadiusForSpread(computedBorderRadii.bottomLeft, spreadExtent.toFloat()),
|
||||
)
|
||||
|
||||
if (shadowShapeDrawable.bounds != shadowShapeBounds ||
|
||||
shadowShapeDrawable.layoutDirection != layoutDirection ||
|
||||
shadowShapeDrawable.borderRadius != borderRadii ||
|
||||
shadowShapeDrawable.computedBorderRadius != shadowBorderRadii ||
|
||||
shadowShapeDrawable.colorFilter != colorFilter) {
|
||||
shadowShapeDrawable.bounds = shadowShapeBounds
|
||||
shadowShapeDrawable.layoutDirection = layoutDirection
|
||||
shadowShapeDrawable.borderRadius = borderRadii
|
||||
shadowShapeDrawable.borderRadius =
|
||||
BorderRadiusStyle(
|
||||
topLeft = LengthPercentage(shadowBorderRadii.topLeft, LengthPercentageType.POINT),
|
||||
topRight = LengthPercentage(shadowBorderRadii.topRight, LengthPercentageType.POINT),
|
||||
bottomLeft =
|
||||
LengthPercentage(shadowBorderRadii.bottomLeft, LengthPercentageType.POINT),
|
||||
bottomRight =
|
||||
LengthPercentage(shadowBorderRadii.bottomRight, LengthPercentageType.POINT))
|
||||
shadowShapeDrawable.colorFilter = colorFilter
|
||||
|
||||
// We remove the portion of the shadow which overlaps the background border box, to avoid
|
||||
// showing the shadow shape e.g. behind a transparent background. There may be a subpixel gap
|
||||
// between the border box path, and the edge of border rendering, so we slightly inflate the
|
||||
// shadow to cover it, placing it below the borders.
|
||||
shadowClipOutPath.rewind()
|
||||
if (background.hasRoundedBorders()) {
|
||||
val subpixelInsetBounds = RectF(bounds).apply { inset(0.4f, 0.4f) }
|
||||
shadowClipOutPath.addRoundRect(
|
||||
subpixelInsetBounds,
|
||||
floatArrayOf(
|
||||
computedBorderRadii.topLeft,
|
||||
computedBorderRadii.topLeft,
|
||||
computedBorderRadii.topRight,
|
||||
computedBorderRadii.topRight,
|
||||
computedBorderRadii.bottomRight,
|
||||
computedBorderRadii.bottomRight,
|
||||
computedBorderRadii.bottomLeft,
|
||||
computedBorderRadii.bottomLeft),
|
||||
Path.Direction.CW)
|
||||
}
|
||||
|
||||
with(renderNode) {
|
||||
setPosition(
|
||||
Rect(shadowShapeFrame).apply {
|
||||
@@ -98,11 +142,10 @@ internal class OutsetBoxShadowDrawable(
|
||||
with(canvas) {
|
||||
save()
|
||||
|
||||
val borderBoxPath = background.getBorderBoxPath()
|
||||
if (borderBoxPath != null) {
|
||||
clipOutPath(borderBoxPath)
|
||||
if (background.hasRoundedBorders()) {
|
||||
clipOutPath(shadowClipOutPath)
|
||||
} else {
|
||||
clipOutRect(background.getBorderBoxRect())
|
||||
clipOutRect(bounds)
|
||||
}
|
||||
|
||||
drawRenderNode(renderNode)
|
||||
|
||||
Reference in New Issue
Block a user