Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RenderScript Impl of Box Shadow #43988

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
RenderScript Impl of Box Shadow (#43988)
Summary:
Pull Request resolved: #43988

The supported path for blur composition effects on API 31+ is RenderNode, but that is pretty new.

This adds a RendderScript path, deprecated for newer versions of Android.

We still need API 26+ for clipOutPath (we need to clip original BG out of shadow, e.g. for transparent BG), but this should be supported in many more places.

It is not perfect. RenderScript's built-in blur implementation has a maximum physical pixel radius of 25px. On an xxhdpi device, this means shadows stop getting larger after ~8 CSS pixels. I also think this path can end up with  blurry bitmaps if scale transform enlarged, unlike RenderNode path.

I think this is probably an acceptable degradation path for older devices, and probably not work home-rolling more complex RenderScript shaders. We should potentially add an OS API level check on the JS side with a dev time warning.

We don't have RenderScript path for blur filter yet, which I imagine might end up stealing some of this code later.

Changelog: [Internal]

Differential Revision: D55896447
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Apr 9, 2024
commit 35b798b3f7b7f445c44d535853ca0a0395205af8
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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.
*/

// Suppress RenderScript deprecation warnings since we only use where replacement API not yet
// available
@file:Suppress("DEPRECATION")

package com.facebook.react.uimanager.drawable

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import androidx.annotation.DeprecatedSinceApi
import androidx.annotation.RequiresApi
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 BLUR_RADIUS_SIGMA_SCALE = 0.5f

// Max supported blur radius of ScriptInstrinsicBlur
private const val MAX_RADIUS = 25f

/**
* Draws an outer-box shadow using RenderScript for older Android versions
* https://www.w3.org/TR/css-backgrounds-3/#shadow-shape
*/
@RequiresApi(26)
@DeprecatedSinceApi(31)
public class BoxShadowRenderscriptDrawable(
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 renderScript = RenderScript.create(context)
private val blurScript =
ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)).apply {
setRadius(
(radiusFromSigma(PixelUtil.toPixelFromDIP(blur) * BLUR_RADIUS_SIGMA_SCALE))
.coerceAtMost(MAX_RADIUS))
}

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

private var inputBitmap: Bitmap? = null
private var outputBitmap: Bitmap? = null

override fun draw(canvas: Canvas) {
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0)
val shadowShapeFrame = Rect(bounds).apply { inset(-spreadExtent, -spreadExtent) }
val blurExtent = ceil(PixelUtil.toPixelFromDIP(blur)).coerceAtMost(MAX_RADIUS).roundToInt() * 2

val blurBounds =
Rect(
0,
0,
shadowShapeFrame.width() + blurExtent * 2,
shadowShapeFrame.height() + blurExtent * 2)

val shadowShapeBounds = Rect(blurBounds).apply { inset(blurExtent, blurExtent) }

val input =
inputBitmap
?: Bitmap.createBitmap(blurBounds.width(), blurBounds.height(), Bitmap.Config.ARGB_8888)
inputBitmap = input
if (input.height != blurBounds.height() || input.width != blurBounds.width()) {
input.reconfigure(blurBounds.width(), blurBounds.height(), input.config)
}

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

val output =
outputBitmap
?: Bitmap.createBitmap(
blurBounds.width(), blurBounds.height(), Bitmap.Config.ARGB_8888)
outputBitmap = output
if (output.height != blurBounds.height() || output.width != blurBounds.width()) {
output.reconfigure(blurBounds.width(), blurBounds.height(), output.config)
}

shadowShapeDrawable.draw(Canvas(input))

val inputAllocation = Allocation.createFromBitmap(renderScript, input)
val outputAllocation = Allocation.createFromBitmap(renderScript, output)

blurScript.setInput(inputAllocation)
blurScript.forEach(outputAllocation)
outputAllocation.copyTo(output)
}

with(canvas) {
clipOutPath(background.borderBoxPath())
drawBitmap(
checkNotNull(outputBitmap),
blurBounds,
Rect(blurBounds).apply {
offset(
PixelUtil.toPixelFromDIP(offsetX).roundToInt() - blurExtent - spreadExtent,
PixelUtil.toPixelFromDIP(offsetY).roundToInt() - blurExtent - spreadExtent)
},
Paint())
}
}

private fun radiusFromSigma(sigma: Float): Float {
// RenderScript converts radius to (0.4 * sigma) + 0.6
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/rs/toolkit/Blur.cpp;l=105;bpv=0;bpt=0
return ((sigma - 0.6f) / 0.4f).coerceAtLeast(0f)
}

override fun setAlpha(alpha: Int) {
shadowShapeDrawable.alpha = alpha
}

override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit

override fun getOpacity(): Int = shadowShapeDrawable.opacity
}
Loading