/* * 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.BorderInsets 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_INSET_BOX_SHADOW_SDK_VERSION = 29 // "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 private val ZERO_RADII = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) /** Draws an inner box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */ @RequiresApi(MIN_INSET_BOX_SHADOW_SDK_VERSION) internal class InsetBoxShadowDrawable( private val context: Context, borderRadius: BorderRadiusStyle? = null, borderInsets: BorderInsets? = 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() } } public var borderInsets = borderInsets 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 computedBorderRadii = computeBorderRadii() val computedBorderInsets = computeBorderInsets() val paddingBoxRect = RectF( bounds.left + (computedBorderInsets?.left ?: 0f), bounds.top + (computedBorderInsets?.top ?: 0f), bounds.right - (computedBorderInsets?.right ?: 0f), bounds.bottom - (computedBorderInsets?.bottom ?: 0f)) val paddingBoxRadii = computedBorderRadii?.let { floatArrayOf( innerRadius(it.topLeft.horizontal, computedBorderInsets?.left), innerRadius(it.topLeft.vertical, computedBorderInsets?.top), innerRadius(it.topRight.horizontal, computedBorderInsets?.right), innerRadius(it.topRight.vertical, computedBorderInsets?.top), innerRadius(it.bottomRight.horizontal, computedBorderInsets?.right), innerRadius(it.bottomRight.vertical, computedBorderInsets?.bottom), innerRadius(it.bottomLeft.horizontal, computedBorderInsets?.left), innerRadius(it.bottomLeft.vertical, computedBorderInsets?.bottom)) } val x = offsetX.dpToPx() val y = offsetY.dpToPx() val spreadExtent = spread.dpToPx() val innerRect = RectF(paddingBoxRect).apply { inset(spreadExtent, spreadExtent) offset(x, y) } // Graciously stolen from Blink // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/box_painter_base.cc;l=338;drc=0a301506035e13015ea5c8dd39164d0d5954fa60 val blurExtent = FilterHelper.sigmaToRadius(blurRadius) val outerRect = RectF(innerRect).apply { inset(-blurExtent, -blurExtent) if (spreadExtent < 0) { inset(spreadExtent, spreadExtent) } union(RectF(this).apply { offset(-x, -y) }) } canvas.save().let { saveCount -> if (paddingBoxRadii != null) { canvas.clipPath( Path().apply { addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW) }) val innerRadii = paddingBoxRadii.map { adjustRadiusForSpread(it, -spreadExtent) }.toFloatArray() canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint) } else { canvas.clipRect(paddingBoxRect) canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint) } canvas.restoreToCount(saveCount) } } private fun computeBorderRadii(): ComputedBorderRadius? { val resolvedBorderRadii = borderRadius?.resolve( layoutDirection, context, bounds.width().toFloat().pxToDp(), bounds.height().toFloat().pxToDp()) return if (resolvedBorderRadii?.hasRoundedBorders() == true) { ComputedBorderRadius( topLeft = CornerRadii( resolvedBorderRadii.topLeft.horizontal.dpToPx(), resolvedBorderRadii.topLeft.vertical.dpToPx()), topRight = CornerRadii( resolvedBorderRadii.topRight.horizontal.dpToPx(), resolvedBorderRadii.topRight.vertical.dpToPx()), bottomLeft = CornerRadii( resolvedBorderRadii.bottomLeft.horizontal.dpToPx(), resolvedBorderRadii.bottomLeft.vertical.dpToPx()), bottomRight = CornerRadii( resolvedBorderRadii.bottomRight.horizontal.dpToPx(), resolvedBorderRadii.bottomRight.vertical.dpToPx()), ) } else { null } } private fun computeBorderInsets(): RectF? = borderInsets?.resolve(layoutDirection, context)?.let { RectF(it.left.dpToPx(), it.top.dpToPx(), it.right.dpToPx(), it.bottom.dpToPx()) } private fun innerRadius(radius: Float, borderInset: Float?): Float = (radius - (borderInset ?: 0f)).coerceAtLeast(0f) }