Skip to content

Commit 65a3259

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Inset box shadow impl (facebook#45337)
Summary: Pull Request resolved: facebook#45337 tsia. Some things to consider when reviewing: * Made a new drawable for inset shadows * The drawable in this class is the same size as the view with some padding. The padding is needed for 2 reasons * Blur near edges looks good * Blur artifacts can appear inside the view if the clear region barely exits the bounds of the view * We draw the clear shape with another drawable, which solely exists so that we can get the border box path for the adjust border. We just use this path to clip out the shadow Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D59300215 fbshipit-source-id: 30acc7aafd82122aa278a42d06418bb1079ca71f
1 parent 0b20ea9 commit 65a3259

File tree

4 files changed

+204
-58
lines changed

4 files changed

+204
-58
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ internal object FilterHelper {
388388
}
389389
}
390390

391-
private fun sigmaToRadius(sigma: Float): Float {
391+
internal fun sigmaToRadius(sigma: Float): Float {
392392
// Android takes blur amount as a radius while web takes a sigma. This value
393393
// is used under the hood to convert between them on Android
394394
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/jni/RenderEffect.cpp
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import com.facebook.react.uimanager.LengthPercentage
11+
import com.facebook.react.uimanager.LengthPercentageType
12+
import com.facebook.react.uimanager.style.BorderRadiusProp
13+
import com.facebook.react.uimanager.style.BorderRadiusStyle
14+
15+
internal fun getShadowBorderRadii(
16+
spread: Float,
17+
backgroundBorderRadii: BorderRadiusStyle,
18+
width: Float,
19+
height: Float,
20+
): BorderRadiusStyle {
21+
val adjustedBorderRadii = BorderRadiusStyle()
22+
val borderRadiusProps = BorderRadiusProp.values()
23+
24+
borderRadiusProps.forEach { borderRadiusProp ->
25+
val borderRadius = backgroundBorderRadii.get(borderRadiusProp)
26+
adjustedBorderRadii.set(
27+
borderRadiusProp,
28+
if (borderRadius == null) null
29+
else adjustedBorderRadius(spread, borderRadius, width, height))
30+
}
31+
32+
return adjustedBorderRadii
33+
}
34+
35+
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape
36+
private fun adjustedBorderRadius(
37+
spread: Float,
38+
backgroundBorderRadius: LengthPercentage?,
39+
width: Float,
40+
height: Float,
41+
): LengthPercentage? {
42+
if (backgroundBorderRadius == null) {
43+
return null
44+
}
45+
var adjustment = spread
46+
val backgroundBorderRadiusValue = backgroundBorderRadius.resolve(width, height)
47+
48+
if (backgroundBorderRadiusValue < Math.abs(spread)) {
49+
val r = backgroundBorderRadiusValue / Math.abs(spread)
50+
val p = Math.pow(r - 1.0, 3.0)
51+
adjustment *= 1.0f + p.toFloat()
52+
}
53+
54+
return LengthPercentage(backgroundBorderRadiusValue + adjustment, LengthPercentageType.POINT)
55+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.content.Context
11+
import android.graphics.Canvas
12+
import android.graphics.ColorFilter
13+
import android.graphics.Paint
14+
import android.graphics.Rect
15+
import android.graphics.RenderNode
16+
import android.graphics.drawable.Drawable
17+
import androidx.annotation.RequiresApi
18+
import com.facebook.common.logging.FLog
19+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
20+
import com.facebook.react.uimanager.FilterHelper
21+
import com.facebook.react.uimanager.PixelUtil
22+
import kotlin.math.roundToInt
23+
24+
private const val TAG = "InsetBoxShadowDrawable"
25+
26+
// "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal
27+
// to half the blur radius"
28+
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
29+
private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f
30+
31+
/** Draws an inner box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
32+
@RequiresApi(31)
33+
@OptIn(UnstableReactNativeAPI::class)
34+
internal class InsetBoxShadowDrawable(
35+
private val context: Context,
36+
private val background: CSSBackgroundDrawable,
37+
private val shadowColor: Int,
38+
private val offsetX: Float,
39+
private val offsetY: Float,
40+
private val blurRadius: Float,
41+
private val spread: Float,
42+
) : Drawable() {
43+
private val renderNode =
44+
RenderNode(TAG).apply {
45+
clipToBounds = false
46+
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE))
47+
}
48+
49+
private val clearRegionDrawable = CSSBackgroundDrawable(context)
50+
51+
private val shadowPaint = Paint().apply { color = shadowColor }
52+
53+
override fun setAlpha(alpha: Int) {
54+
renderNode.alpha = alpha / 255f
55+
}
56+
57+
override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit
58+
59+
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
60+
61+
override fun draw(canvas: Canvas) {
62+
if (!canvas.isHardwareAccelerated) {
63+
FLog.w(TAG, "InsetBoxShadowDrawable requires a hardware accelerated canvas")
64+
return
65+
}
66+
// We need the actual size the blur will increase the shadow by so we can
67+
// properly pad. This is not simply the input as Android has it's own
68+
// distinct blur algorithm
69+
val adjustedBlurRadius =
70+
FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE).roundToInt()
71+
val padding = 2 * adjustedBlurRadius
72+
73+
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
74+
val clearRegionBounds = Rect()
75+
background.getPaddingBoxRect().round(clearRegionBounds)
76+
clearRegionBounds.inset(spreadExtent, spreadExtent)
77+
clearRegionBounds.offset(
78+
PixelUtil.toPixelFromDIP(offsetX).roundToInt() + padding / 2,
79+
PixelUtil.toPixelFromDIP(offsetY).roundToInt() + padding / 2)
80+
val clearRegionBorderRadii =
81+
getShadowBorderRadii(
82+
-spreadExtent.toFloat(),
83+
background.borderRadius,
84+
clearRegionBounds.width().toFloat(),
85+
clearRegionBounds.height().toFloat())
86+
87+
if (shadowPaint.colorFilter != colorFilter ||
88+
clearRegionDrawable.layoutDirection != layoutDirection ||
89+
clearRegionDrawable.borderRadius != clearRegionBorderRadii ||
90+
clearRegionDrawable.bounds != clearRegionBounds) {
91+
canvas.save()
92+
93+
shadowPaint.colorFilter = colorFilter
94+
clearRegionDrawable.bounds = clearRegionBounds
95+
clearRegionDrawable.layoutDirection = layoutDirection
96+
clearRegionDrawable.borderRadius = clearRegionBorderRadii
97+
98+
with(renderNode) {
99+
// We pad by the blur radius so that the edges of the blur look good and
100+
// the blur artifacts can bleed into the view if needed
101+
setPosition(Rect(bounds).apply { inset(-padding, -padding) })
102+
beginRecording().let { canvas ->
103+
val borderBoxPath = clearRegionDrawable.getBorderBoxPath()
104+
if (borderBoxPath != null) {
105+
canvas.clipOutPath(borderBoxPath)
106+
} else {
107+
canvas.clipOutRect(clearRegionDrawable.borderBoxRect)
108+
}
109+
110+
canvas.drawPaint(shadowPaint)
111+
endRecording()
112+
}
113+
}
114+
115+
// We actually draw the render node into our canvas and clip out the
116+
// padding
117+
with(canvas) {
118+
val paddingBoxPath = background.getPaddingBoxPath()
119+
if (paddingBoxPath != null) {
120+
canvas.clipPath(paddingBoxPath)
121+
} else {
122+
canvas.clipRect(background.getPaddingBoxRect())
123+
}
124+
// This positions the render node properly since we padded it
125+
canvas.translate(padding / 2f, padding / 2f)
126+
drawRenderNode(renderNode)
127+
}
128+
129+
canvas.restore()
130+
}
131+
}
132+
}
Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,47 @@ import androidx.annotation.RequiresApi
1717
import com.facebook.common.logging.FLog
1818
import com.facebook.react.common.annotations.UnstableReactNativeAPI
1919
import com.facebook.react.uimanager.FilterHelper
20-
import com.facebook.react.uimanager.LengthPercentage
21-
import com.facebook.react.uimanager.LengthPercentageType
2220
import com.facebook.react.uimanager.PixelUtil
23-
import com.facebook.react.uimanager.style.BorderRadiusProp
24-
import com.facebook.react.uimanager.style.BorderRadiusStyle
2521
import kotlin.math.roundToInt
2622

27-
private const val TAG = "BoxShadowDrawable"
23+
private const val TAG = "OutsetBoxShadowDrawable"
2824

2925
// "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal
3026
// to half the blur radius"
3127
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
3228
private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f
3329

34-
/** Draws an outer-box shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
30+
/** Draws an outer box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
3531
@RequiresApi(31)
3632
@OptIn(UnstableReactNativeAPI::class)
37-
internal class BoxShadowDrawable(
33+
internal class OutsetBoxShadowDrawable(
3834
context: Context,
3935
private val background: CSSBackgroundDrawable,
4036
shadowColor: Int,
4137
private val offsetX: Float,
4238
private val offsetY: Float,
43-
blurRadius: Float,
39+
private val blurRadius: Float,
4440
private val spread: Float,
4541
) : Drawable() {
46-
4742
private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor }
4843

4944
private val renderNode =
50-
RenderNode("BoxShadowDrawable").apply {
45+
RenderNode(TAG).apply {
5146
clipToBounds = false
5247
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE))
5348
}
5449

50+
override fun setAlpha(alpha: Int) {
51+
renderNode.alpha = alpha / 255f
52+
}
53+
54+
override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit
55+
56+
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
57+
5558
override fun draw(canvas: Canvas) {
5659
if (!canvas.isHardwareAccelerated) {
57-
FLog.w(TAG, "BoxShadowDrawable requires a hardware accelerated canvas")
60+
FLog.w(TAG, "OutsetBoxShadowDrawable requires a hardware accelerated canvas")
5861
return
5962
}
6063

@@ -72,6 +75,7 @@ internal class BoxShadowDrawable(
7275
shadowShapeDrawable.layoutDirection != layoutDirection ||
7376
shadowShapeDrawable.borderRadius != borderRadii ||
7477
shadowShapeDrawable.colorFilter != colorFilter) {
78+
canvas.save()
7579
shadowShapeDrawable.bounds = shadowShapeBounds
7680
shadowShapeDrawable.layoutDirection = layoutDirection
7781
shadowShapeDrawable.borderRadius = borderRadii
@@ -102,52 +106,7 @@ internal class BoxShadowDrawable(
102106

103107
drawRenderNode(renderNode)
104108
}
105-
}
106-
107-
override fun setAlpha(alpha: Int) {
108-
renderNode.alpha = alpha / 255f
109-
}
110-
111-
override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit
112-
113-
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
114-
115-
private fun getShadowBorderRadii(
116-
spread: Float,
117-
backgroundBorderRadii: BorderRadiusStyle,
118-
width: Float,
119-
height: Float,
120-
): BorderRadiusStyle {
121-
val adjustedBorderRadii = BorderRadiusStyle()
122-
val borderRadiusProps = BorderRadiusProp.values()
123-
124-
borderRadiusProps.forEach { borderRadiusProp ->
125-
val borderRadius = backgroundBorderRadii.get(borderRadiusProp)
126-
adjustedBorderRadii.set(
127-
borderRadiusProp,
128-
if (borderRadius == null) null
129-
else adjustedBorderRadius(spread, borderRadius, width, height))
130-
}
131-
132-
return adjustedBorderRadii
133-
}
134-
135-
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape
136-
private fun adjustedBorderRadius(
137-
spread: Float,
138-
backgroundBorderRadius: LengthPercentage,
139-
width: Float,
140-
height: Float,
141-
): LengthPercentage {
142-
var adjustment = spread
143-
val backgroundBorderRadiusValue = backgroundBorderRadius.resolve(width, height)
144-
145-
if (backgroundBorderRadiusValue < Math.abs(spread)) {
146-
val r = backgroundBorderRadiusValue / Math.abs(spread)
147-
val p = Math.pow(r - 1.0, 3.0)
148-
adjustment *= 1.0f + p.toFloat()
149-
}
150109

151-
return LengthPercentage(backgroundBorderRadiusValue + adjustment, LengthPercentageType.POINT)
110+
canvas.restore()
152111
}
153112
}

0 commit comments

Comments
 (0)