forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: 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 Differential Revision: D59300215
- Loading branch information
1 parent
99dcf1e
commit f5ca5f5
Showing
4 changed files
with
297 additions
and
1 deletion.
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
53 changes: 53 additions & 0 deletions
53
...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,53 @@ | ||
/* | ||
* 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 -> | ||
adjustedBorderRadii.set( | ||
borderRadiusProp, | ||
adjustedBorderRadius(spread, backgroundBorderRadii.get(borderRadiusProp), 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) | ||
} |
134 changes: 134 additions & 0 deletions
134
...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,134 @@ | ||
/* | ||
* 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.RenderNode | ||
import android.graphics.drawable.Drawable | ||
import androidx.annotation.RequiresApi | ||
import com.facebook.common.logging.FLog | ||
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) | ||
internal class InsetBoxShadowDrawable( | ||
private val context: Context, | ||
private val background: CSSBackgroundDrawable, | ||
shadowColor: Int, | ||
private val offsetX: Float, | ||
private val offsetY: Float, | ||
private val blurRadius: Float, | ||
private val spread: Float, | ||
) : Drawable() { | ||
private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor } | ||
|
||
private val renderNode = | ||
RenderNode(TAG).apply { | ||
clipToBounds = false | ||
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE)) | ||
} | ||
|
||
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 | ||
} | ||
|
||
if (shadowShapeDrawable.bounds != bounds || | ||
shadowShapeDrawable.layoutDirection != layoutDirection || | ||
shadowShapeDrawable.borderRadius != background.borderRadius || | ||
shadowShapeDrawable.colorFilter != colorFilter) { | ||
canvas.save() | ||
|
||
// 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() | ||
// 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 | ||
val shadowShapeBounds = | ||
Rect(bounds).apply { inset(-2 * adjustedBlurRadius, -2 * adjustedBlurRadius) } | ||
shadowShapeDrawable.bounds = shadowShapeBounds | ||
shadowShapeDrawable.layoutDirection = layoutDirection | ||
shadowShapeDrawable.colorFilter = colorFilter | ||
|
||
// We create a new drawable that represents the clear region that we clip | ||
// out of the shadow. The shadow itself is just a colored rect before this | ||
with(renderNode) { | ||
setPosition(Rect(bounds).apply { inset(-2 * adjustedBlurRadius, -2 * adjustedBlurRadius) }) | ||
val spreadExtent = PixelUtil.toPixelFromDIP(spread).roundToInt().coerceAtLeast(0) | ||
val clearRegionBounds = Rect(bounds).apply { inset(spreadExtent, spreadExtent) } | ||
val clearRegionBorderRadii = | ||
getShadowBorderRadii( | ||
-spreadExtent.toFloat(), | ||
background.borderRadius, | ||
bounds.width().toFloat(), | ||
bounds.height().toFloat()) | ||
val clearRegionDrawable = | ||
CSSBackgroundDrawable(context).apply { | ||
borderRadius = clearRegionBorderRadii | ||
bounds = | ||
Rect(clearRegionBounds).apply { | ||
offset( | ||
PixelUtil.toPixelFromDIP(offsetX).roundToInt() + adjustedBlurRadius, | ||
PixelUtil.toPixelFromDIP(offsetY).roundToInt() + adjustedBlurRadius) | ||
} | ||
} | ||
|
||
beginRecording().let { canvas -> | ||
val borderBoxPath = clearRegionDrawable.getBorderBoxPath() | ||
if (borderBoxPath != null) { | ||
canvas.clipOutPath(borderBoxPath) | ||
} else { | ||
canvas.clipOutRect(clearRegionDrawable.borderBoxRect) | ||
} | ||
|
||
shadowShapeDrawable.draw(canvas) | ||
endRecording() | ||
} | ||
} | ||
|
||
// We actually draw the render node into our canvas and clip out the | ||
// padding | ||
with(canvas) { | ||
val borderBoxPath = background.getBorderBoxPath() | ||
if (borderBoxPath != null) { | ||
canvas.clipPath(borderBoxPath) | ||
} else { | ||
canvas.clipRect(bounds) | ||
} | ||
// This positions the render node properly since we padded it | ||
canvas.translate(adjustedBlurRadius.toFloat(), adjustedBlurRadius.toFloat()) | ||
drawRenderNode(renderNode) | ||
} | ||
|
||
canvas.restore() | ||
} | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
...actAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.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,109 @@ | ||
/* | ||
* 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.RenderNode | ||
import android.graphics.drawable.Drawable | ||
import androidx.annotation.RequiresApi | ||
import com.facebook.common.logging.FLog | ||
import com.facebook.react.uimanager.FilterHelper | ||
import com.facebook.react.uimanager.PixelUtil | ||
import kotlin.math.roundToInt | ||
|
||
private const val TAG = "OutsetBoxShadowDrawable" | ||
|
||
// "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) | ||
internal class OutsetBoxShadowDrawable( | ||
context: Context, | ||
private val background: CSSBackgroundDrawable, | ||
shadowColor: Int, | ||
private val offsetX: Float, | ||
private val offsetY: Float, | ||
private val blurRadius: Float, | ||
private val spread: Float, | ||
) : Drawable() { | ||
private val shadowShapeDrawable = CSSBackgroundDrawable(context).apply { color = shadowColor } | ||
|
||
private val renderNode = | ||
RenderNode(TAG).apply { | ||
clipToBounds = false | ||
setRenderEffect(FilterHelper.createBlurEffect(blurRadius * BLUR_RADIUS_SIGMA_SCALE)) | ||
} | ||
|
||
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, "OutsetBoxShadowDrawable requires a hardware accelerated canvas") | ||
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.colorFilter != colorFilter) { | ||
canvas.save() | ||
shadowShapeDrawable.bounds = shadowShapeBounds | ||
shadowShapeDrawable.layoutDirection = layoutDirection | ||
shadowShapeDrawable.borderRadius = | ||
getShadowBorderRadii( | ||
spreadExtent.toFloat(), | ||
background.borderRadius, | ||
bounds.width().toFloat(), | ||
bounds.height().toFloat()) | ||
shadowShapeDrawable.colorFilter = colorFilter | ||
|
||
with(renderNode) { | ||
setPosition( | ||
Rect(shadowShapeFrame).apply { | ||
offset( | ||
PixelUtil.toPixelFromDIP(offsetX).roundToInt(), | ||
PixelUtil.toPixelFromDIP(offsetY).roundToInt()) | ||
}) | ||
|
||
beginRecording().let { canvas -> | ||
shadowShapeDrawable.draw(canvas) | ||
endRecording() | ||
} | ||
} | ||
} | ||
|
||
with(canvas) { | ||
val borderBoxPath = background.getBorderBoxPath() | ||
if (borderBoxPath != null) { | ||
clipOutPath(borderBoxPath) | ||
} else { | ||
clipOutRect(background.getBorderBoxRect()) | ||
} | ||
|
||
drawRenderNode(renderNode) | ||
} | ||
|
||
canvas.restore() | ||
} | ||
} |