-
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.
Summary: Pull Request resolved: #45337 tsia. Some things to consider when reviewing: * Made a new drawable for inset shadows * The drawable in this class is the same size as the view with some padding. The padding is needed for 2 reasons * Blur near edges looks good * Blur artifacts can appear inside the view if the clear region barely exits the bounds of the view * We draw the clear shape with another drawable, which solely exists so that we can get the border box path for the adjust border. We just use this path to clip out the shadow Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D59300215 fbshipit-source-id: 30acc7aafd82122aa278a42d06418bb1079ca71f
- Loading branch information
1 parent
0b20ea9
commit 65a3259
Showing
4 changed files
with
204 additions
and
58 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
55 changes: 55 additions & 0 deletions
55
...ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BoxShadowBorderRadius.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,55 @@ | ||
/* | ||
* 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 com.facebook.react.uimanager.LengthPercentage | ||
import com.facebook.react.uimanager.LengthPercentageType | ||
import com.facebook.react.uimanager.style.BorderRadiusProp | ||
import com.facebook.react.uimanager.style.BorderRadiusStyle | ||
|
||
internal fun getShadowBorderRadii( | ||
spread: Float, | ||
backgroundBorderRadii: BorderRadiusStyle, | ||
width: Float, | ||
height: Float, | ||
): BorderRadiusStyle { | ||
val adjustedBorderRadii = BorderRadiusStyle() | ||
val borderRadiusProps = BorderRadiusProp.values() | ||
|
||
borderRadiusProps.forEach { borderRadiusProp -> | ||
val borderRadius = backgroundBorderRadii.get(borderRadiusProp) | ||
adjustedBorderRadii.set( | ||
borderRadiusProp, | ||
if (borderRadius == null) null | ||
else adjustedBorderRadius(spread, borderRadius, width, height)) | ||
} | ||
|
||
return adjustedBorderRadii | ||
} | ||
|
||
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape | ||
private fun adjustedBorderRadius( | ||
spread: Float, | ||
backgroundBorderRadius: LengthPercentage?, | ||
width: Float, | ||
height: Float, | ||
): LengthPercentage? { | ||
if (backgroundBorderRadius == null) { | ||
return null | ||
} | ||
var adjustment = spread | ||
val backgroundBorderRadiusValue = backgroundBorderRadius.resolve(width, height) | ||
|
||
if (backgroundBorderRadiusValue < Math.abs(spread)) { | ||
val r = backgroundBorderRadiusValue / Math.abs(spread) | ||
val p = Math.pow(r - 1.0, 3.0) | ||
adjustment *= 1.0f + p.toFloat() | ||
} | ||
|
||
return LengthPercentage(backgroundBorderRadiusValue + adjustment, LengthPercentageType.POINT) | ||
} |
132 changes: 132 additions & 0 deletions
132
...eactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.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,132 @@ | ||
/* | ||
* 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.Paint | ||
import android.graphics.Rect | ||
import android.graphics.RenderNode | ||
import android.graphics.drawable.Drawable | ||
import androidx.annotation.RequiresApi | ||
import com.facebook.common.logging.FLog | ||
import com.facebook.react.common.annotations.UnstableReactNativeAPI | ||
import com.facebook.react.uimanager.FilterHelper | ||
import com.facebook.react.uimanager.PixelUtil | ||
import kotlin.math.roundToInt | ||
|
||
private const val TAG = "InsetBoxShadowDrawable" | ||
|
||
// "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 inner box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */ | ||
@RequiresApi(31) | ||
@OptIn(UnstableReactNativeAPI::class) | ||
internal class InsetBoxShadowDrawable( | ||
private val context: Context, | ||
private val background: CSSBackgroundDrawable, | ||
private val shadowColor: Int, | ||
private val offsetX: Float, | ||
private val offsetY: Float, | ||
private val blurRadius: Float, | ||
private val spread: Float, | ||
) : Drawable() { | ||
private val renderNode = | ||
RenderNode(TAG).apply { | ||
clipToBounds = false | ||
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE)) | ||
} | ||
|
||
private val clearRegionDrawable = CSSBackgroundDrawable(context) | ||
|
||
private val shadowPaint = Paint().apply { color = shadowColor } | ||
|
||
override fun setAlpha(alpha: Int) { | ||
renderNode.alpha = alpha / 255f | ||
} | ||
|
||
override fun setColorFilter(colorFilter: ColorFilter?): Unit = Unit | ||
|
||
override fun getOpacity(): Int = (renderNode.alpha * 255).roundToInt() | ||
|
||
override fun draw(canvas: Canvas) { | ||
if (!canvas.isHardwareAccelerated) { | ||
FLog.w(TAG, "InsetBoxShadowDrawable requires a hardware accelerated canvas") | ||
return | ||
} | ||
// We need the actual size the blur will increase the shadow by so we can | ||
// properly pad. This is not simply the input as Android has it's own | ||
// distinct blur algorithm | ||
val adjustedBlurRadius = | ||
FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE).roundToInt() | ||
val padding = 2 * adjustedBlurRadius | ||
|
||
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0) | ||
val clearRegionBounds = Rect() | ||
background.getPaddingBoxRect().round(clearRegionBounds) | ||
clearRegionBounds.inset(spreadExtent, spreadExtent) | ||
clearRegionBounds.offset( | ||
PixelUtil.toPixelFromDIP(offsetX).roundToInt() + padding / 2, | ||
PixelUtil.toPixelFromDIP(offsetY).roundToInt() + padding / 2) | ||
val clearRegionBorderRadii = | ||
getShadowBorderRadii( | ||
-spreadExtent.toFloat(), | ||
background.borderRadius, | ||
clearRegionBounds.width().toFloat(), | ||
clearRegionBounds.height().toFloat()) | ||
|
||
if (shadowPaint.colorFilter != colorFilter || | ||
clearRegionDrawable.layoutDirection != layoutDirection || | ||
clearRegionDrawable.borderRadius != clearRegionBorderRadii || | ||
clearRegionDrawable.bounds != clearRegionBounds) { | ||
canvas.save() | ||
|
||
shadowPaint.colorFilter = colorFilter | ||
clearRegionDrawable.bounds = clearRegionBounds | ||
clearRegionDrawable.layoutDirection = layoutDirection | ||
clearRegionDrawable.borderRadius = clearRegionBorderRadii | ||
|
||
with(renderNode) { | ||
// We pad by the blur radius so that the edges of the blur look good and | ||
// the blur artifacts can bleed into the view if needed | ||
setPosition(Rect(bounds).apply { inset(-padding, -padding) }) | ||
beginRecording().let { canvas -> | ||
val borderBoxPath = clearRegionDrawable.getBorderBoxPath() | ||
if (borderBoxPath != null) { | ||
canvas.clipOutPath(borderBoxPath) | ||
} else { | ||
canvas.clipOutRect(clearRegionDrawable.borderBoxRect) | ||
} | ||
|
||
canvas.drawPaint(shadowPaint) | ||
endRecording() | ||
} | ||
} | ||
|
||
// We actually draw the render node into our canvas and clip out the | ||
// padding | ||
with(canvas) { | ||
val paddingBoxPath = background.getPaddingBoxPath() | ||
if (paddingBoxPath != null) { | ||
canvas.clipPath(paddingBoxPath) | ||
} else { | ||
canvas.clipRect(background.getPaddingBoxRect()) | ||
} | ||
// This positions the render node properly since we padded it | ||
canvas.translate(padding / 2f, padding / 2f) | ||
drawRenderNode(renderNode) | ||
} | ||
|
||
canvas.restore() | ||
} | ||
} | ||
} |
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