Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,63 +21,18 @@ import android.graphics.RenderNode
import android.graphics.Shader
import android.os.Build
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.blur.BlurRadiusSpec
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
* Specification for blur radius parameters, capable of creating a hardware [RenderEffect]
* for use with [RenderNode.setBackdropRenderEffect].
*
* @param radiusX The horizontal blur radius.
* @param radiusY The vertical blur radius (defaults to [radiusX]).
* @param tileMode The tile mode for handling edges (defaults to [Shader.TileMode.CLAMP]).
*/
data class BlurRadiusSpec(val radiusX: Dp, val radiusY: Dp = radiusX, val tileMode: Shader.TileMode = Shader.TileMode.CLAMP) {
/**
* Creates an Android [RenderEffect] configured with this specification.
*/
fun createRenderEffect(density: Density): RenderEffect? {
val rxPx = with(density) { radiusX.toPx() }
val ryPx = with(density) { radiusY.toPx() }
return createRenderEffect(rxPx, ryPx, tileMode)
}

companion object {
/**
* Creates a hardware [RenderEffect] blur effect from pixel radii.
*/
fun createRenderEffect(
radiusXPx: Float,
radiusYPx: Float = radiusXPx,
tileMode: Shader.TileMode = Shader.TileMode.CLAMP,
): RenderEffect? {
if (Build.VERSION.SDK_INT >= 31 && (radiusXPx > 0f || radiusYPx > 0f)) {
return RenderEffect.createBlurEffect(
radiusXPx.coerceAtLeast(0.01f),
radiusYPx.coerceAtLeast(0.01f),
tileMode,
)
}
return null
}

/**
* Creates a hardware [RenderEffect] blur effect from a [Dp] radius.
*/
fun createRenderEffect(radius: Dp, density: Density, tileMode: Shader.TileMode = Shader.TileMode.CLAMP): RenderEffect? {
val px = with(density) { radius.toPx() }
return createRenderEffect(px, px, tileMode)
}
}
}

/**
* Applies an in-window backdrop [RenderEffect] to content drawn behind this composable in the window.
*
Expand Down Expand Up @@ -110,26 +65,51 @@ fun Modifier.backdropRenderEffect(
)

/**
* Draws the content behind this composable blurred with the specified [radius],
* Overload of [backdropRenderEffect] accepting Compose's [androidx.compose.ui.graphics.RenderEffect].
*/
fun Modifier.backdropRenderEffect(
renderEffect: androidx.compose.ui.graphics.RenderEffect?,
shape: Shape = RectangleShape,
tint: Color = Color.Unspecified,
elevation: Dp = 0.dp,
outerShadowOnly: Boolean = true,
fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent,
): Modifier = backdropRenderEffect(
renderEffect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
renderEffect?.asAndroidRenderEffect()
} else {
null
},
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)

/**
* Draws the content behind this composable blurred according to the provided [spec],
* clipped to [shape], beneath this composable's own content.
*
* @param radius The blur radius to apply to the backdrop.
* Uses Compose's built-in [BlurRadiusSpec] to configure uniform or spatially-varying blur
* radii (such as [BlurRadiusSpec.uniform], [BlurRadiusSpec.verticalGradient], etc.).
*
* @param spec The [BlurRadiusSpec] defining the blur radius or gradient.
* @param shape The shape of the frosted-glass region.
* @param tint An optional translucent color overlay drawn over the blurred backdrop.
* @param elevation Optional elevation shadow cast by this component.
* @param outerShadowOnly If true, clips out the shadow cast beneath the outline area.
* @param fallbackColor An optional fallback background color for platforms earlier than Android 17.
*/
fun Modifier.backdropBlur(
radius: Dp,
spec: BlurRadiusSpec,
shape: Shape = RectangleShape,
tint: Color = Color.Unspecified,
elevation: Dp = 0.dp,
outerShadowOnly: Boolean = true,
fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent,
): Modifier = backdropBlur(
radiusX = radius,
radiusY = radius,
): Modifier = this then BackdropBlurElement(
spec = spec,
shape = shape,
tint = tint,
elevation = elevation,
Expand All @@ -138,26 +118,25 @@ fun Modifier.backdropBlur(
)

/**
* Overload of [backdropBlur] allowing independent horizontal and vertical blur radii.
* Draws the content behind this composable blurred with the specified uniform [radius],
* clipped to [shape], beneath this composable's own content.
*
* @param radiusX The horizontal blur radius.
* @param radiusY The vertical blur radius.
* @param radius The blur radius to apply to the backdrop.
* @param shape The shape of the frosted-glass region.
* @param tint An optional translucent color overlay drawn over the blurred backdrop.
* @param elevation Optional elevation shadow cast by this component.
* @param outerShadowOnly If true, clips out the shadow cast beneath the outline area.
* @param fallbackColor An optional fallback background color for platforms earlier than Android 17.
*/
fun Modifier.backdropBlur(
radiusX: Dp,
radiusY: Dp,
radius: Dp,
shape: Shape = RectangleShape,
tint: Color = Color.Unspecified,
elevation: Dp = 0.dp,
outerShadowOnly: Boolean = true,
fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent,
): Modifier = backdropBlur(
spec = BlurRadiusSpec(radiusX, radiusY),
spec = BlurRadiusSpec.uniform(radius),
shape = shape,
tint = tint,
elevation = elevation,
Expand All @@ -166,23 +145,44 @@ fun Modifier.backdropBlur(
)

/**
* Overload of [backdropBlur] configured via a [BlurRadiusSpec].
* Overload of [backdropBlur] allowing independent horizontal and vertical blur radii.
*
* @param radiusX The horizontal blur radius.
* @param radiusY The vertical blur radius.
* @param shape The shape of the frosted-glass region.
* @param tint An optional translucent color overlay drawn over the blurred backdrop.
* @param elevation Optional elevation shadow cast by this component.
* @param outerShadowOnly If true, clips out the shadow cast beneath the outline area.
* @param fallbackColor An optional fallback background color for platforms earlier than Android 17.
*/
fun Modifier.backdropBlur(
spec: BlurRadiusSpec,
radiusX: Dp,
radiusY: Dp,
shape: Shape = RectangleShape,
tint: Color = Color.Unspecified,
elevation: Dp = 0.dp,
outerShadowOnly: Boolean = true,
fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent,
): Modifier = this then BackdropBlurElement(
spec = spec,
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)
): Modifier = if (radiusX == radiusY) {
backdropBlur(
spec = BlurRadiusSpec.uniform(radiusX),
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)
} else {
this then BackdropEllipticalBlurElement(
radiusX = radiusX,
radiusY = radiusY,
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)
}

private data class BackdropRenderEffectElement(
val renderEffect: RenderEffect?,
Expand Down Expand Up @@ -262,6 +262,49 @@ private data class BackdropBlurElement(
}
}

private data class BackdropEllipticalBlurElement(
val radiusX: Dp,
val radiusY: Dp,
val shape: Shape,
val tint: Color,
val elevation: Dp,
val outerShadowOnly: Boolean,
val fallbackColor: Color,
) : ModifierNodeElement<BackdropEllipticalBlurNode>() {
override fun create(): BackdropEllipticalBlurNode = BackdropEllipticalBlurNode(
radiusX = radiusX,
radiusY = radiusY,
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)

override fun update(node: BackdropEllipticalBlurNode) {
node.update(
radiusX = radiusX,
radiusY = radiusY,
shape = shape,
tint = tint,
elevation = elevation,
outerShadowOnly = outerShadowOnly,
fallbackColor = fallbackColor,
)
}

override fun InspectorInfo.inspectableProperties() {
name = "backdropBlur"
properties["radiusX"] = radiusX
properties["radiusY"] = radiusY
properties["shape"] = shape
properties["tint"] = tint
properties["elevation"] = elevation
properties["outerShadowOnly"] = outerShadowOnly
properties["fallbackColor"] = fallbackColor
}
}

private class BackdropRenderEffectNode(
var renderEffect: RenderEffect?,
shape: Shape,
Expand Down Expand Up @@ -317,13 +360,23 @@ private class BackdropBlurNode(

private var cachedEffect: RenderEffect? = null
private var cachedDensity: Float = -1f
private var cachedSize: Size = Size.Unspecified
private var cachedSpec: BlurRadiusSpec? = null

override fun resolveRenderEffect(density: Density): RenderEffect? {
override fun resolveRenderEffect(density: Density, size: Size): RenderEffect? {
val currentDensity = density.density
if (cachedEffect == null || cachedDensity != currentDensity || cachedSpec != spec) {
cachedEffect = spec.createRenderEffect(density)
if (cachedEffect == null || cachedDensity != currentDensity || cachedSize != size || cachedSpec != spec) {
cachedEffect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && size.width > 0f && size.height > 0f) {
try {
spec.createRenderEffect(size, density).asAndroidRenderEffect()
} catch (_: Throwable) {
null
}
} else {
null
}
cachedDensity = currentDensity
cachedSize = size
cachedSpec = spec
}
return cachedEffect
Expand Down Expand Up @@ -361,3 +414,77 @@ private class BackdropBlurNode(
}
}
}

private class BackdropEllipticalBlurNode(
var radiusX: Dp,
var radiusY: Dp,
shape: Shape,
tint: Color,
elevation: Dp,
outerShadowOnly: Boolean,
fallbackColor: Color,
) : BaseBackdropNode(shape, tint, elevation, outerShadowOnly, fallbackColor) {

private var cachedEffect: RenderEffect? = null
private var cachedDensity: Float = -1f
private var cachedRadiusX: Dp = 0.dp
private var cachedRadiusY: Dp = 0.dp

override fun resolveRenderEffect(density: Density): RenderEffect? {
val currentDensity = density.density
if (cachedEffect == null || cachedDensity != currentDensity || cachedRadiusX != radiusX || cachedRadiusY != radiusY) {
val rxPx = with(density) { radiusX.toPx() }
val ryPx = with(density) { radiusY.toPx() }
cachedEffect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && (rxPx > 0f || ryPx > 0f)) {
RenderEffect.createBlurEffect(
rxPx.coerceAtLeast(0.01f),
ryPx.coerceAtLeast(0.01f),
Shader.TileMode.CLAMP,
)
} else {
null
}
cachedDensity = currentDensity
cachedRadiusX = radiusX
cachedRadiusY = radiusY
}
return cachedEffect
}

fun update(radiusX: Dp, radiusY: Dp, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) {
var changed = false
if (this.radiusX != radiusX) {
this.radiusX = radiusX
cachedEffect = null
changed = true
}
if (this.radiusY != radiusY) {
this.radiusY = radiusY
cachedEffect = null
changed = true
}
if (this.shape != shape) {
this.shape = shape
changed = true
}
if (this.tint != tint) {
this.tint = tint
changed = true
}
if (this.elevation != elevation) {
this.elevation = elevation
changed = true
}
if (this.outerShadowOnly != outerShadowOnly) {
this.outerShadowOnly = outerShadowOnly
changed = true
}
if (this.fallbackColor != fallbackColor) {
this.fallbackColor = fallbackColor
changed = true
}
if (changed) {
markDirty()
}
}
}
Loading
Loading