-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce "BoxShadowDrawable" for Android box shadows (#43722)
Summary: Pull Request resolved: #43722 This change adds a drawable, when when drawn on the bounds of a border-box sized view, will draw a spec compliant box-shadow outside the box. This is reliant on Android `RenderNode` and `RenderEffect` APIs provided by API 31. Inset box shadows can also be added using a similar method, but this is not done yet. The code which manages this is in flux, but the underlying drawable should be good. Will add some tests once it's more wired up. Changelog: [Internal] Differential Revision: D55561465
- Loading branch information
1 parent
db575ec
commit 42bcbea
Showing
2 changed files
with
107 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
...ive/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowDrawable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* 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.react.uimanager.FilterHelper | ||
import com.facebook.react.uimanager.PixelUtil | ||
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 | ||
|
||
/** 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) { | ||
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.bounds = shadowShapeBounds | ||
shadowShapeDrawable.layoutDirection = layoutDirection | ||
shadowShapeDrawable.borderRadius = background.borderRadius | ||
|
||
with(renderNode) { | ||
setPosition( | ||
Rect(shadowShapeFrame).apply { | ||
offset( | ||
PixelUtil.toPixelFromDIP(offsetX).roundToInt(), | ||
PixelUtil.toPixelFromDIP(offsetY).roundToInt()) | ||
}) | ||
|
||
discardDisplayList() | ||
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?) { | ||
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 * BLUR_RADIUS_SIGMA_SCALE, chainedEffect) | ||
|
||
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt() | ||
} |