Skip to content

Commit 0fc42d5

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Introduce "BoxShadowDrawable" for Android box shadows
Summary: This change adds a drawable, when when drawn on the bounds of a border-box sized view, will draw a spec compliant box-shadow outside the box. This is reliant on Android `RenderNode` and `RenderEffect` APIs provided by API 31. Inset box shadows can also be added using a similar method, but this is not done yet. The code which manages this is in flux, but the underlying drawable should be good. Will add some tests once it's more wired up. Changelog: [Internal] Differential Revision: D55561465
1 parent c41fb54 commit 0fc42d5

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5395,6 +5395,14 @@ public abstract interface class com/facebook/react/uimanager/debug/NotThreadSafe
53955395
public abstract fun onViewHierarchyUpdateFinished ()V
53965396
}
53975397

5398+
public final class com/facebook/react/uimanager/drawable/BoxShadowDrawable : com/facebook/react/uimanager/drawable/DecorationDrawable {
5399+
public fun <init> (Landroid/content/Context;Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable;IFFFF)V
5400+
public fun draw (Landroid/graphics/Canvas;)V
5401+
public fun getOpacity ()I
5402+
public fun setAlpha (I)V
5403+
public fun setColorFilter (Landroid/graphics/ColorFilter;)V
5404+
}
5405+
53985406
public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : android/graphics/drawable/Drawable {
53995407
public fun <init> (Landroid/content/Context;)V
54005408
public fun borderBoxPath ()Landroid/graphics/Path;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.Rect
14+
import android.graphics.RenderEffect
15+
import android.graphics.RenderNode
16+
import androidx.annotation.RequiresApi
17+
import com.facebook.react.uimanager.FilterHelper
18+
import com.facebook.react.uimanager.PixelUtil
19+
import kotlin.math.roundToInt
20+
21+
// "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal
22+
// to half the blur radius"
23+
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
24+
private const val SHADOW_RADIUS_SIGMA_SCALE = 0.5f
25+
26+
/** Draws an outer-box shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
27+
@RequiresApi(31)
28+
public class BoxShadowDrawable(
29+
context: Context,
30+
private val background: CSSBackgroundDrawable,
31+
private val shadowColor: Int,
32+
private val offsetX: Float,
33+
private val offsetY: Float,
34+
private val blur: Float,
35+
private val spread: Float,
36+
) : DecorationDrawable() {
37+
38+
private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor }
39+
40+
private val renderNode =
41+
RenderNode("box-shadow").apply {
42+
setClipToBounds(false)
43+
setRenderEffect(createBlurEffect())
44+
}
45+
46+
override fun draw(canvas: Canvas) {
47+
val spreadPx = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
48+
val boundsWithSpread = Rect(bounds).apply { inset(-spreadPx, -spreadPx) }
49+
50+
with(shadowShapeDrawable) {
51+
setBounds(0, 0, boundsWithSpread.width(), boundsWithSpread.height())
52+
setRadius(background.fullBorderRadius)
53+
for (location in CSSBackgroundDrawable.BorderRadiusLocation.values()) {
54+
setRadius(background.getBorderRadius(location), location.ordinal)
55+
}
56+
}
57+
58+
with(renderNode) {
59+
val offsetBounds =
60+
Rect(boundsWithSpread).apply {
61+
offset(
62+
PixelUtil.toPixelFromDIP(offsetX).roundToInt(),
63+
PixelUtil.toPixelFromDIP(offsetY).roundToInt())
64+
}
65+
66+
setPosition(offsetBounds)
67+
discardDisplayList()
68+
beginRecording().let { nodeCanvas -> shadowShapeDrawable.draw(nodeCanvas) }
69+
endRecording()
70+
}
71+
72+
with(canvas) {
73+
clipOutPath(background.borderBoxPath())
74+
drawRenderNode(renderNode)
75+
}
76+
}
77+
78+
override fun setAlpha(alpha: Int) {
79+
renderNode.alpha = alpha / 255f
80+
}
81+
82+
override fun setColorFilter(colorFilter: ColorFilter?) {
83+
val chainedEffect = colorFilter?.let { RenderEffect.createColorFilterEffect(it) }
84+
renderNode.setRenderEffect(createBlurEffect(chainedEffect))
85+
}
86+
87+
private fun createBlurEffect(chainedEffect: RenderEffect? = null): RenderEffect? =
88+
if (blur <= 0f) chainedEffect
89+
else FilterHelper.createBlurEffect(blur * SHADOW_RADIUS_SIGMA_SCALE, chainedEffect)
90+
91+
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
92+
}

0 commit comments

Comments
 (0)