Skip to content

Commit 5dd5c6e

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Introduce "BoxShadowDrawable" for Android box shadows (facebook#43722)
Summary: Pull Request resolved: facebook#43722 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 ad772ea commit 5dd5c6e

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

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

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

5380+
public final class com/facebook/react/uimanager/drawable/BoxShadowDrawable : android/graphics/drawable/Drawable {
5381+
public fun <init> (Landroid/content/Context;Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable;IFFFF)V
5382+
public fun draw (Landroid/graphics/Canvas;)V
5383+
public fun getOpacity ()I
5384+
public fun setAlpha (I)V
5385+
public fun setColorFilter (Landroid/graphics/ColorFilter;)V
5386+
}
5387+
53805388
public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : android/graphics/drawable/Drawable {
53815389
public fun <init> (Landroid/content/Context;)V
53825390
public fun borderBoxPath ()Landroid/graphics/Path;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 android.graphics.drawable.Drawable
17+
import androidx.annotation.RequiresApi
18+
import com.facebook.react.uimanager.FilterHelper
19+
import com.facebook.react.uimanager.PixelUtil
20+
import kotlin.math.roundToInt
21+
22+
// "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal
23+
// to half the blur radius"
24+
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
25+
private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f
26+
27+
/** Draws an outer-box shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
28+
@RequiresApi(31)
29+
public class BoxShadowDrawable(
30+
context: Context,
31+
private val background: CSSBackgroundDrawable,
32+
private val shadowColor: Int,
33+
private val offsetX: Float,
34+
private val offsetY: Float,
35+
private val blur: Float,
36+
private val spread: Float,
37+
) : Drawable() {
38+
39+
private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor }
40+
41+
private val renderNode =
42+
RenderNode("BoxShadowDrawable").apply {
43+
clipToBounds = false
44+
setRenderEffect(createBlurEffect())
45+
}
46+
47+
override fun draw(canvas: Canvas) {
48+
if (!canvas.isHardwareAccelerated) {
49+
return
50+
}
51+
52+
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
53+
val shadowShapeFrame = Rect(bounds).apply { inset(-spreadExtent, -spreadExtent) }
54+
val shadowShapeBounds = Rect(0, 0, shadowShapeFrame.width(), shadowShapeFrame.height())
55+
56+
if (shadowShapeDrawable.bounds != shadowShapeBounds ||
57+
shadowShapeDrawable.layoutDirection != layoutDirection ||
58+
shadowShapeDrawable.borderRadius != background.borderRadius) {
59+
shadowShapeDrawable.bounds = shadowShapeBounds
60+
shadowShapeDrawable.layoutDirection = layoutDirection
61+
shadowShapeDrawable.borderRadius = background.borderRadius
62+
63+
with(renderNode) {
64+
setPosition(
65+
Rect(shadowShapeFrame).apply {
66+
offset(
67+
PixelUtil.toPixelFromDIP(offsetX).roundToInt(),
68+
PixelUtil.toPixelFromDIP(offsetY).roundToInt())
69+
})
70+
71+
discardDisplayList()
72+
beginRecording().let { canvas ->
73+
shadowShapeDrawable.draw(canvas)
74+
endRecording()
75+
}
76+
}
77+
}
78+
79+
with(canvas) {
80+
clipOutPath(background.borderBoxPath())
81+
drawRenderNode(renderNode)
82+
}
83+
}
84+
85+
override fun setAlpha(alpha: Int) {
86+
renderNode.alpha = alpha / 255f
87+
}
88+
89+
override fun setColorFilter(colorFilter: ColorFilter?) {
90+
val chainedEffect = colorFilter?.let { RenderEffect.createColorFilterEffect(it) }
91+
renderNode.setRenderEffect(createBlurEffect(chainedEffect))
92+
}
93+
94+
private fun createBlurEffect(chainedEffect: RenderEffect? = null): RenderEffect? =
95+
if (blur <= 0f) chainedEffect
96+
else FilterHelper.createBlurEffect(blur * BLUR_RADIUS_SIGMA_SCALE, chainedEffect)
97+
98+
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
99+
}

0 commit comments

Comments
 (0)