diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 55acb4820df3bb..f5b67fcf57410e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5433,6 +5433,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 : android/graphics/drawable/Drawable { + 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..a440aab49669db --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowDrawable.kt @@ -0,0 +1,100 @@ +/* + * 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 android.graphics.drawable.Drawable +import androidx.annotation.RequiresApi +import com.facebook.common.logging.FLog +import com.facebook.react.uimanager.FilterHelper +import com.facebook.react.uimanager.PixelUtil +import kotlin.math.roundToInt + +private const val TAG = "BoxShadowDrawable" + +// "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 BLUR_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, +) : Drawable() { + + private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor } + + private val renderNode = + RenderNode("BoxShadowDrawable").apply { + clipToBounds = false + setRenderEffect(createBlurEffect()) + } + + override fun draw(canvas: Canvas) { + if (!canvas.isHardwareAccelerated) { + FLog.w(TAG, "BoxShadowDrawable requires a hardware accelerated canvas") + return + } + + val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0) + val shadowShapeFrame = Rect(bounds).apply { inset(-spreadExtent, -spreadExtent) } + val shadowShapeBounds = Rect(0, 0, shadowShapeFrame.width(), shadowShapeFrame.height()) + + if (shadowShapeDrawable.bounds != shadowShapeBounds || + shadowShapeDrawable.layoutDirection != layoutDirection || + shadowShapeDrawable.borderRadius != background.borderRadius || + shadowShapeDrawable.colorFilter != colorFilter) { + shadowShapeDrawable.bounds = shadowShapeBounds + shadowShapeDrawable.layoutDirection = layoutDirection + shadowShapeDrawable.borderRadius = background.borderRadius + shadowShapeDrawable.colorFilter = colorFilter + + with(renderNode) { + setPosition( + Rect(shadowShapeFrame).apply { + offset( + PixelUtil.toPixelFromDIP(offsetX).roundToInt(), + PixelUtil.toPixelFromDIP(offsetY).roundToInt()) + }) + + beginRecording().let { canvas -> + shadowShapeDrawable.draw(canvas) + endRecording() + } + } + } + + with(canvas) { + clipOutPath(background.borderBoxPath()) + drawRenderNode(renderNode) + } + } + + override fun setAlpha(alpha: Int) { + renderNode.alpha = alpha / 255f + } + + override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit + + private fun createBlurEffect(): RenderEffect? = + FilterHelper.createBlurEffect(blur * BLUR_RADIUS_SIGMA_SCALE) + + override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt() +}