Skip to content

Commit bb12629

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 Reviewed By: joevilches
1 parent 92a9572 commit bb12629

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

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

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

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

0 commit comments

Comments
 (0)