Rename rememberLayerBackdrop with rememberBackdrop, add Modifier.contentBackdrop

This commit is contained in:
Kyant0 committed 2025-09-15 22:37:15 +08:00
1 parent 7697b3bf97
commit 2700c40514
13 files changed
+362 -177

No files matched your search

@@ -1,21 +1,48 @@
package com.kyant.backdrop
import androidx.compose.ui.Modifier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
fun Modifier.backdrop(backdrop: Backdrop): Modifier {
val element = backdrop.element
return if (element != null) {
this then element
} else {
this
private val DefaultDrawLayer: ContentDrawScope.() -> Unit = { drawContent() }
@Composable
fun rememberBackdrop(
graphicsLayer: GraphicsLayer = rememberGraphicsLayer(),
drawLayer: ContentDrawScope.() -> Unit = DefaultDrawLayer
): Backdrop {
return remember(graphicsLayer, drawLayer) {
Backdrop(
graphicsLayer = graphicsLayer,
drawLayer = drawLayer
)
}
}
interface Backdrop {
@Stable
class Backdrop internal constructor(
internal val graphicsLayer: GraphicsLayer,
internal val drawLayer: ContentDrawScope.() -> Unit
) {
val element: Modifier.Element?
internal var backdropPosition: Offset by mutableStateOf(Offset.Zero)
fun DrawScope.drawBackdrop(coordinates: LayoutCoordinates)
internal fun DrawScope.drawBackdrop(coordinates: LayoutCoordinates) {
val position = coordinates.positionInWindow() - backdropPosition
translate(-position.x, -position.y) {
drawLayer(graphicsLayer)
}
}
}
@@ -5,6 +5,11 @@ import android.graphics.RuntimeShader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
@@ -40,3 +45,27 @@ interface BackdropEffectScope : Density {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun obtainRuntimeShader(key: String, string: String): RuntimeShader
}
internal abstract class BackdropEffectScopeImpl : BackdropEffectScope {
private val runtimeShaders = mutableMapOf<String, RuntimeShader>()
override var density: Float by mutableFloatStateOf(1f)
override var fontScale: Float by mutableFloatStateOf(1f)
override var size: Size by mutableStateOf(Size.Unspecified)
override var layoutDirection: LayoutDirection by mutableStateOf(LayoutDirection.Ltr)
override var renderEffect: RenderEffect? = null
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun obtainRuntimeShader(key: String, string: String): RuntimeShader {
return runtimeShaders.getOrPut(key) { RuntimeShader(string) }
}
fun applyDrawScope(scope: CacheDrawScope) {
density = scope.density
fontScale = scope.fontScale
size = scope.size
layoutDirection = scope.layoutDirection
renderEffect = null
}
}
@@ -0,0 +1,61 @@
package com.kyant.backdrop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.GlobalPositionAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.platform.InspectorInfo
fun Modifier.backdrop(backdrop: Backdrop): Modifier =
this then BackdropElement(backdrop)
private class BackdropElement(val backdrop: Backdrop) : ModifierNodeElement<BackdropNode>() {
override fun create(): BackdropNode {
return BackdropNode(backdrop = backdrop)
}
override fun update(node: BackdropNode) {
node.backdrop = backdrop
node.invalidateDraw()
}
override fun InspectorInfo.inspectableProperties() {
name = "layerBackdrop"
properties["backdrop"] = backdrop
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BackdropElement) return false
if (backdrop != other.backdrop) return false
return true
}
override fun hashCode(): Int {
return backdrop.hashCode()
}
}
private class BackdropNode(var backdrop: Backdrop) :
DrawModifierNode, GlobalPositionAwareModifierNode, Modifier.Node() {
override val shouldAutoInvalidate: Boolean = false
override fun ContentDrawScope.draw() {
drawContent()
backdrop.graphicsLayer.record { backdrop.drawLayer.invoke(this@draw) }
}
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
if (coordinates.isAttached) {
backdrop.backdropPosition = coordinates.positionInWindow()
}
}
}
@@ -0,0 +1,186 @@
package com.kyant.backdrop
import android.os.Build
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.CacheDrawModifierNode
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.requireGraphicsContext
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.highlight.HighlightElement
import com.kyant.backdrop.shadow.Shadow
import com.kyant.backdrop.shadow.ShadowElement
fun Modifier.contentBackdrop(
shapeProvider: () -> Shape,
highlight: (() -> Highlight?)? = DefaultHighlight,
shadow: (() -> Shadow?)? = DefaultShadow,
onDrawBehind: (DrawScope.() -> Unit)? = null,
onDrawSurface: (DrawScope.() -> Unit)? = null,
effects: BackdropEffectScope.() -> Unit
): Modifier {
val shapeProvider = CachedBackdropShapeProvider(shapeProvider)
return this
.then(
if (shadow != null) {
ShadowElement(
shapeProvider = shapeProvider,
shadow = shadow
)
} else {
Modifier
}
)
.then(
if (highlight != null) {
HighlightElement(
shapeProvider = shapeProvider,
highlight = highlight
)
} else {
Modifier
}
)
.then(
ContentBackdropElement(
shapeProvider = shapeProvider,
onDrawBehind = onDrawBehind,
onDrawSurface = onDrawSurface,
effects = effects
)
)
}
private class ContentBackdropElement(
val shapeProvider: BackdropShapeProvider,
val onDrawBehind: (DrawScope.() -> Unit)?,
val onDrawSurface: (DrawScope.() -> Unit)?,
val effects: BackdropEffectScope.() -> Unit
) : ModifierNodeElement<ContentBackdropNode>() {
override fun create(): ContentBackdropNode {
return ContentBackdropNode(
shapeProvider = shapeProvider,
onDrawBehind = onDrawBehind,
onDrawSurface = onDrawSurface,
effects = effects
)
}
override fun update(node: ContentBackdropNode) {
node.shapeProvider = shapeProvider
node.onDrawBehind = onDrawBehind
node.onDrawSurface = onDrawSurface
node.effects = effects
node.drawNode.invalidateDrawCache()
}
override fun InspectorInfo.inspectableProperties() {
name = "contentBackdrop"
properties["shapeProvider"] = shapeProvider
properties["onDrawBehind"] = onDrawBehind
properties["onDrawSurface"] = onDrawSurface
properties["effects"] = effects
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ContentBackdropElement) return false
if (shapeProvider != other.shapeProvider) return false
if (onDrawBehind != other.onDrawBehind) return false
if (onDrawSurface != other.onDrawSurface) return false
if (effects != other.effects) return false
return true
}
override fun hashCode(): Int {
var result = shapeProvider.hashCode()
result = 31 * result + (onDrawBehind?.hashCode() ?: 0)
result = 31 * result + (onDrawSurface?.hashCode() ?: 0)
result = 31 * result + effects.hashCode()
return result
}
}
private class ContentBackdropNode(
var shapeProvider: BackdropShapeProvider,
var onDrawBehind: (DrawScope.() -> Unit)?,
var onDrawSurface: (DrawScope.() -> Unit)?,
var effects: BackdropEffectScope.() -> Unit
) : LayoutModifierNode, DelegatingNode() {
override val shouldAutoInvalidate: Boolean = false
private var graphicsLayer: GraphicsLayer? = null
private val effectScope =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
object : BackdropEffectScopeImpl() {
override val shape: Shape get() = shapeProvider.innerShape
}
} else {
null
}
val drawNode = delegate(CacheDrawModifierNode {
if (effectScope != null) {
effectScope.applyDrawScope(this)
effects(effectScope)
graphicsLayer?.renderEffect = effectScope.renderEffect?.asComposeRenderEffect()
}
onDrawWithContent {
onDrawBehind?.invoke(this)
graphicsLayer?.let { layer ->
layer.record { this@onDrawWithContent.drawContent() }
drawLayer(layer)
}
onDrawSurface?.invoke(this)
}
})
private val layerBlock: GraphicsLayerScope.() -> Unit = {
clip = true
shape = shapeProvider.shape
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.placeWithLayer(IntOffset.Zero, layerBlock = layerBlock)
}
}
override fun onAttach() {
val graphicsContext = requireGraphicsContext()
graphicsLayer = graphicsContext.createGraphicsLayer()
}
override fun onDetach() {
val graphicsContext = requireGraphicsContext()
graphicsLayer?.let { layer ->
graphicsContext.releaseGraphicsLayer(layer)
graphicsLayer = null
}
}
}
@@ -1,16 +1,11 @@
package com.kyant.backdrop
import android.graphics.RenderEffect
import android.graphics.RuntimeShader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.CacheDrawModifierNode
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asComposeRenderEffect
@@ -29,14 +24,13 @@ import androidx.compose.ui.node.requireGraphicsContext
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.highlight.HighlightElement
import com.kyant.backdrop.shadow.Shadow
import com.kyant.backdrop.shadow.ShadowElement
private val DefaultHighlight = { Highlight() }
private val DefaultShadow = { Shadow() }
internal val DefaultHighlight = { Highlight() }
internal val DefaultShadow = { Shadow() }
private val DefaultOnDrawBackdrop: DrawScope.(DrawScope.() -> Unit) -> Unit = { it() }
fun Modifier.drawBackdrop(
@@ -75,10 +69,10 @@ fun Modifier.drawBackdrop(
DrawBackdropElement(
backdrop = backdrop,
shapeProvider = shapeProvider,
effects = effects,
onDrawBehind = onDrawBehind,
onDrawBackdrop = onDrawBackdrop,
onDrawSurface = onDrawSurface
onDrawSurface = onDrawSurface,
effects = effects
)
)
}
@@ -86,30 +80,30 @@ fun Modifier.drawBackdrop(
private class DrawBackdropElement(
val backdrop: Backdrop,
val shapeProvider: BackdropShapeProvider,
val effects: BackdropEffectScope.() -> Unit,
val onDrawBehind: (DrawScope.() -> Unit)?,
val onDrawBackdrop: DrawScope.(drawBackdrop: DrawScope.() -> Unit) -> Unit,
val onDrawSurface: (DrawScope.() -> Unit)?
val onDrawSurface: (DrawScope.() -> Unit)?,
val effects: BackdropEffectScope.() -> Unit
) : ModifierNodeElement<DrawBackdropNode>() {
override fun create(): DrawBackdropNode {
return DrawBackdropNode(
backdrop = backdrop,
shapeProvider = shapeProvider,
effects = effects,
onDrawBehind = onDrawBehind,
onDrawBackdrop = onDrawBackdrop,
onDrawSurface = onDrawSurface
onDrawSurface = onDrawSurface,
effects = effects
)
}
override fun update(node: DrawBackdropNode) {
node.backdrop = backdrop
node.shapeProvider = shapeProvider
node.effects = effects
node.onDrawBehind = onDrawBehind
node.onDrawBackdrop = onDrawBackdrop
node.onDrawSurface = onDrawSurface
node.effects = effects
node.drawNode.invalidateDrawCache()
}
@@ -117,10 +111,10 @@ private class DrawBackdropElement(
name = "drawBackdrop"
properties["backdrop"] = backdrop
properties["shapeProvider"] = shapeProvider
properties["effects"] = effects
properties["onDrawBehind"] = onDrawBehind
properties["onDrawBackdrop"] = onDrawBackdrop
properties["onDrawSurface"] = onDrawSurface
properties["effects"] = effects
}
override fun equals(other: Any?): Boolean {
@@ -129,10 +123,10 @@ private class DrawBackdropElement(
if (backdrop != other.backdrop) return false
if (shapeProvider != other.shapeProvider) return false
if (effects != other.effects) return false
if (onDrawBehind != other.onDrawBehind) return false
if (onDrawBackdrop != other.onDrawBackdrop) return false
if (onDrawSurface != other.onDrawSurface) return false
if (effects != other.effects) return false
return true
}
@@ -140,10 +134,10 @@ private class DrawBackdropElement(
override fun hashCode(): Int {
var result = backdrop.hashCode()
result = 31 * result + shapeProvider.hashCode()
result = 31 * result + effects.hashCode()
result = 31 * result + (onDrawBehind?.hashCode() ?: 0)
result = 31 * result + onDrawBackdrop.hashCode()
result = 31 * result + (onDrawSurface?.hashCode() ?: 0)
result = 31 * result + effects.hashCode()
return result
}
}
@@ -151,10 +145,10 @@ private class DrawBackdropElement(
private class DrawBackdropNode(
var backdrop: Backdrop,
var shapeProvider: BackdropShapeProvider,
var effects: BackdropEffectScope.() -> Unit,
var onDrawBehind: (DrawScope.() -> Unit)?,
var onDrawBackdrop: DrawScope.(drawBackdrop: DrawScope.() -> Unit) -> Unit,
var onDrawSurface: (DrawScope.() -> Unit)?
var onDrawSurface: (DrawScope.() -> Unit)?,
var effects: BackdropEffectScope.() -> Unit
) : LayoutModifierNode, GlobalPositionAwareModifierNode, DelegatingNode() {
override val shouldAutoInvalidate: Boolean = false
@@ -164,21 +158,9 @@ private class DrawBackdropNode(
private val effectScope =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
object : BackdropEffectScope {
object : BackdropEffectScopeImpl() {
private val runtimeShaders = mutableMapOf<String, RuntimeShader>()
override var density: Float by mutableFloatStateOf(1f)
override var fontScale: Float by mutableFloatStateOf(1f)
override var size: Size by mutableStateOf(Size.Unspecified)
override var layoutDirection: LayoutDirection by mutableStateOf(LayoutDirection.Ltr)
override val shape: Shape get() = shapeProvider.innerShape
override var renderEffect: RenderEffect? = null
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun obtainRuntimeShader(key: String, string: String): RuntimeShader {
return runtimeShaders.getOrPut(key) { RuntimeShader(string) }
}
}
} else {
null
@@ -186,14 +168,8 @@ private class DrawBackdropNode(
val drawNode = delegate(CacheDrawModifierNode {
if (effectScope != null) {
effectScope.density = density
effectScope.fontScale = fontScale
effectScope.size = size
effectScope.layoutDirection = layoutDirection
effectScope.renderEffect = null
effectScope.applyDrawScope(this)
effects(effectScope)
graphicsLayer?.renderEffect = effectScope.renderEffect?.asComposeRenderEffect()
}
@@ -1,116 +0,0 @@
package com.kyant.backdrop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.GlobalPositionAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
@Composable
fun rememberLayerBackdrop(backgroundColor: Color?): LayerBackdrop {
val graphicsLayer = rememberGraphicsLayer()
return remember(graphicsLayer, backgroundColor) {
LayerBackdrop(
graphicsLayer = graphicsLayer,
backgroundColor = backgroundColor
)
}
}
@Stable
class LayerBackdrop(
val graphicsLayer: GraphicsLayer,
val backgroundColor: Color?
) : Backdrop {
override val element: Modifier.Element?
get() = LayerBackdropElement(this)
var backdropPosition: Offset by mutableStateOf(Offset.Zero)
override fun DrawScope.drawBackdrop(coordinates: LayoutCoordinates) {
val position = coordinates.positionInWindow() - backdropPosition
translate(-position.x, -position.y) {
if (backgroundColor != null && backgroundColor.isSpecified) {
drawContext.canvas.nativeCanvas.drawColor(backgroundColor.toArgb())
}
drawLayer(graphicsLayer)
}
}
}
private class LayerBackdropElement(
val backdrop: LayerBackdrop
) : ModifierNodeElement<LayerBackdropNode>() {
override fun create(): LayerBackdropNode {
return LayerBackdropNode(backdrop = backdrop)
}
override fun update(node: LayerBackdropNode) {
node.update(backdrop = backdrop)
}
override fun InspectorInfo.inspectableProperties() {
name = "layerBackdrop"
properties["backdrop"] = backdrop
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LayerBackdropElement) return false
if (backdrop != other.backdrop) return false
return true
}
override fun hashCode(): Int {
return backdrop.hashCode()
}
}
private class LayerBackdropNode(
var backdrop: LayerBackdrop
) : DrawModifierNode, GlobalPositionAwareModifierNode, Modifier.Node() {
override val shouldAutoInvalidate: Boolean = false
override fun ContentDrawScope.draw() {
drawContent()
backdrop.graphicsLayer.record {
backdrop.backgroundColor?.let { drawRect(it) }
this@draw.drawContent()
}
}
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
if (coordinates.isAttached) {
backdrop.backdropPosition = coordinates.positionInWindow()
}
}
fun update(backdrop: LayerBackdrop) {
this.backdrop = backdrop
}
}