/* * 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.BlurMaskFilter import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.Path import android.graphics.RectF import android.graphics.drawable.Drawable import androidx.annotation.RequiresApi import com.facebook.react.uimanager.FilterHelper import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.PixelUtil.pxToDp import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.ComputedBorderRadius import com.facebook.react.uimanager.style.CornerRadii import kotlin.math.roundToInt internal const val MIN_OUTSET_BOX_SHADOW_SDK_VERSION = 28 // "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(MIN_OUTSET_BOX_SHADOW_SDK_VERSION) internal class OutsetBoxShadowDrawable( private val context: Context, borderRadius: BorderRadiusStyle? = null, private val shadowColor: Int, private val offsetX: Float, private val offsetY: Float, private val blurRadius: Float, private val spread: Float, ) : Drawable() { public var borderRadius = borderRadius set(value) { if (value != field) { field = value invalidateSelf() } } private val shadowPaint = Paint().apply { color = shadowColor if (blurRadius > 0) { maskFilter = BlurMaskFilter( FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE), BlurMaskFilter.Blur.NORMAL) } } override fun setAlpha(alpha: Int) { shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt() invalidateSelf() } override fun setColorFilter(colorFilter: ColorFilter?) { shadowPaint.colorFilter = colorFilter invalidateSelf() } override fun getOpacity(): Int = ((shadowPaint.alpha / 255f) / (Color.alpha(shadowColor) / 255f) * 255f).roundToInt() override fun draw(canvas: Canvas) { val resolutionWidth = bounds.width().toFloat().pxToDp() val resolutionHeight = bounds.height().toFloat().pxToDp() val computedBorderRadii = borderRadius?.resolve(layoutDirection, context, resolutionWidth, resolutionHeight)?.let { ComputedBorderRadius( topLeft = CornerRadii(it.topLeft.horizontal.dpToPx(), it.topLeft.vertical.dpToPx()), topRight = CornerRadii(it.topRight.horizontal.dpToPx(), it.topRight.vertical.dpToPx()), bottomLeft = CornerRadii(it.bottomLeft.horizontal.dpToPx(), it.bottomLeft.vertical.dpToPx()), bottomRight = CornerRadii(it.bottomRight.horizontal.dpToPx(), it.bottomRight.vertical.dpToPx()), ) } val spreadExtent = spread.dpToPx() val shadowRect = RectF(bounds).apply { inset(-spreadExtent, -spreadExtent) offset(offsetX.dpToPx(), offsetY.dpToPx()) } canvas.save().let { saveCount -> if (computedBorderRadii?.hasRoundedBorders() == true) { drawShadowRoundRect(canvas, shadowRect, spreadExtent, computedBorderRadii) } else { drawShadowRect(canvas, shadowRect) } canvas.restoreToCount(saveCount) } } private fun drawShadowRoundRect( canvas: Canvas, shadowRect: RectF, spreadExtent: Float, computedBorderRadii: ComputedBorderRadius ) { // We inset the clip slightly, to avoid Skia artifacts with antialiased // clipping. This inset is only visible when no background is present. // https://neugierig.org/software/chromium/notes/2010/07/clipping.html val subpixelInsetBounds = RectF(bounds).apply { inset(0.4f, 0.4f) } canvas.clipOutPath( Path().apply { addRoundRect( subpixelInsetBounds, floatArrayOf( computedBorderRadii.topLeft.horizontal, computedBorderRadii.topLeft.vertical, computedBorderRadii.topRight.horizontal, computedBorderRadii.topRight.vertical, computedBorderRadii.bottomRight.horizontal, computedBorderRadii.bottomRight.vertical, computedBorderRadii.bottomLeft.horizontal, computedBorderRadii.bottomLeft.vertical), Path.Direction.CW) }) canvas.drawPath( Path().apply { addRoundRect( shadowRect, floatArrayOf( adjustRadiusForSpread(computedBorderRadii.topLeft.horizontal, spreadExtent), adjustRadiusForSpread(computedBorderRadii.topLeft.vertical, spreadExtent), adjustRadiusForSpread(computedBorderRadii.topRight.horizontal, spreadExtent), adjustRadiusForSpread(computedBorderRadii.topRight.vertical, spreadExtent), adjustRadiusForSpread(computedBorderRadii.bottomRight.horizontal, spreadExtent), adjustRadiusForSpread(computedBorderRadii.bottomRight.vertical, spreadExtent), adjustRadiusForSpread(computedBorderRadii.bottomLeft.horizontal, spreadExtent), adjustRadiusForSpread(computedBorderRadii.bottomLeft.vertical, spreadExtent)), Path.Direction.CW) }, shadowPaint) } private fun drawShadowRect(canvas: Canvas, shadowRect: RectF) { canvas.clipOutRect(bounds) canvas.drawRect(shadowRect, shadowPaint) } }