diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 707f71a9f3f9e8..137fc5c93c38de 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 (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 (Landroid/content/Context;)V public fun borderBoxPath ()Landroid/graphics/Path; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowDrawable.kt new file mode 100644 index 00000000000000..dd184f771ef6e9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowDrawable.kt @@ -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() +}