Skip to content

Commit

Permalink
Introduce "BoxShadowDrawable" for Android box shadows (facebook#43722)
Browse files Browse the repository at this point in the history
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]

Reviewed By: joevilches

Differential Revision: D55561465
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Apr 1, 2024
1 parent 21bc677 commit a163191
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 0 deletions.
8 changes: 8 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -5373,6 +5373,14 @@ public abstract interface class com/facebook/react/uimanager/debug/NotThreadSafe
public abstract fun onViewHierarchyUpdateFinished ()V
}

public final class com/facebook/react/uimanager/drawable/BoxShadowDrawable : com/facebook/react/uimanager/drawable/DecorationDrawable {
public fun <init> (Landroid/content/Context;Lcom/facebook/react/uimanager/drawable/CSSBackgroundDrawable;IFFFF)V
public fun draw (Landroid/graphics/Canvas;)V
public fun getOpacity ()I
public fun setAlpha (I)V
public fun setColorFilter (Landroid/graphics/ColorFilter;)V
}

public class com/facebook/react/uimanager/drawable/CSSBackgroundDrawable : android/graphics/drawable/Drawable {
public fun <init> (Landroid/content/Context;)V
public fun borderBoxPath ()Landroid/graphics/Path;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.uimanager.drawable

import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Rect
import android.graphics.RenderEffect
import android.graphics.RenderNode
import androidx.annotation.RequiresApi
import com.facebook.react.uimanager.FilterHelper
import com.facebook.react.uimanager.PixelUtil
import kotlin.math.ceil
import kotlin.math.roundToInt

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

/** Draws an outer-box shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
@RequiresApi(31)
public class BoxShadowDrawable(
context: Context,
private val background: CSSBackgroundDrawable,
private val shadowColor: Int,
private val offsetX: Float,
private val offsetY: Float,
private val blur: Float,
private val spread: Float,
) : DecorationDrawable() {

private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor }

private val renderNode =
RenderNode("box-shadow").apply {
clipToBounds = false
setRenderEffect(createBlurEffect())
}

override fun draw(canvas: Canvas) {
val spreadPx = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
val boundsWithSpread = Rect(bounds).apply { inset(-spreadPx, -spreadPx) }

with(shadowShapeDrawable) {
setBounds(0, 0, boundsWithSpread.width(), boundsWithSpread.height())
setRadius(background.fullBorderRadius)
for (location in CSSBackgroundDrawable.BorderRadiusLocation.values()) {
setRadius(background.getBorderRadius(location), location.ordinal)
}
}

with(renderNode) {
val offsetBounds =
Rect(boundsWithSpread).apply {
offset(
PixelUtil.toPixelFromDIP(offsetX).roundToInt(),
PixelUtil.toPixelFromDIP(offsetY).roundToInt())
}

setPosition(offsetBounds)

// Clip to the extent of the blur
// https://drafts.fxtf.org/filter-effects/#funcdef-filter-blur
val blurExtent =
ceil(1.88f * SHADOW_RADIUS_SIGMA_SCALE * PixelUtil.toPixelFromDIP(blur)).toInt()
setClipRect(
Rect(0, 0, boundsWithSpread.width(), boundsWithSpread.height()).apply {
inset(-blurExtent, -blurExtent)
})

discardDisplayList()
beginRecording().let { nodeCanvas -> shadowShapeDrawable.draw(nodeCanvas) }
endRecording()
}

with(canvas) {
clipOutPath(background.borderBoxPath())
drawRenderNode(renderNode)
}
}

override fun setAlpha(alpha: Int) {
renderNode.alpha = alpha / 255f
}

override fun setColorFilter(colorFilter: ColorFilter?) {
val chainedEffect = colorFilter?.let { RenderEffect.createColorFilterEffect(it) }
renderNode.setRenderEffect(createBlurEffect(chainedEffect))
}

private fun createBlurEffect(chainedEffect: RenderEffect? = null): RenderEffect? =
if (blur <= 0f) chainedEffect
else FilterHelper.createBlurEffect(blur * SHADOW_RADIUS_SIGMA_SCALE, chainedEffect)

override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt()
}

0 comments on commit a163191

Please sign in to comment.