Add transformBlock param to liquidGlass modifiers

This commit is contained in:
Kyant0 committed 2025-08-21 09:51:51 +08:00
1 parent d1953995f9
commit 5c0514fe5b
7 files changed
+171 -112

No files matched your search

+12 -1
View File
@@ -90,7 +90,7 @@ See [here](./glassmusic/src/main/java/com/kyant/glassmusic/BottomTabs.kt#L123) f
### Tips
The following case is not supported:
- The following case is not supported:
```kotlin
LiquidGlassProvider(providerState) {
@@ -107,6 +107,17 @@ Box {
}
```
- Apply the scale correctly:
```kotlin
Modifier
.scale(scaleX, scaleY)
.liquidGlass(
// ...
transformBlock = { scale(1f / scaleX, 1f / scaleY, Offset.Zero) }
)
```
## Comparisons
Android device: Google Pixel 4 XL (the smallest width is adjusted to 440 dp to match the density of the iOS device)
@@ -21,11 +21,17 @@ internal class GlassShapeElement(
) : ModifierNodeElement<GlassShapeNode>() {
override fun create(): GlassShapeNode {
return GlassShapeNode(style, compositingStrategy)
return GlassShapeNode(
style = style,
compositingStrategy = compositingStrategy
)
}
override fun update(node: GlassShapeNode) {
node.update(style, compositingStrategy)
node.update(
style = style,
compositingStrategy = compositingStrategy
)
}
override fun InspectorInfo.inspectableProperties() {
@@ -72,7 +78,10 @@ internal class GlassShapeNode(
compositingStrategy = this@GlassShapeNode.compositingStrategy
}
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
@@ -9,21 +9,6 @@ import com.kyant.liquidglass.shadow.GlassShadow
/**
* The style of the liquid glass.
*
* @param shape
* The shape of the glass.
*
* @param innerRefraction
* The inner refraction effect of the liquid glass.
*
* @param material
* The material properties of the liquid glass.
*
* @param highlight
* The highlight effect applied to the liquid glass.
*
* @param shadow
* The shadow effect applied to the liquid glass.
*/
@Immutable
data class GlassStyle(
@@ -18,7 +18,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.asAndroidColorFilter
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.DrawTransform
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.LayoutCoordinates
@@ -44,24 +46,28 @@ import kotlinx.coroutines.launch
fun Modifier.liquidGlass(
state: LiquidGlassProviderState,
style: GlassStyle,
compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen
compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
transformBlock: (DrawTransform.() -> Unit)? = null
): Modifier =
this.liquidGlass(
state = state,
luminanceSampler = null,
compositingStrategy = compositingStrategy,
transformBlock = transformBlock,
style = { style }
)
fun Modifier.liquidGlass(
state: LiquidGlassProviderState,
compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
transformBlock: (DrawTransform.() -> Unit)? = null,
style: () -> GlassStyle
): Modifier =
this.liquidGlass(
state = state,
luminanceSampler = null,
compositingStrategy = compositingStrategy,
transformBlock = transformBlock,
style = style
)
@@ -70,12 +76,14 @@ fun Modifier.liquidGlass(
state: LiquidGlassProviderState,
style: GlassStyle,
compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
transformBlock: (DrawTransform.() -> Unit)? = null,
luminanceSampler: LuminanceSampler? = null
): Modifier =
this.liquidGlass(
state = state,
luminanceSampler = luminanceSampler,
compositingStrategy = compositingStrategy,
transformBlock = transformBlock,
style = { style }
)
@@ -84,6 +92,7 @@ fun Modifier.liquidGlass(
state: LiquidGlassProviderState,
luminanceSampler: LuminanceSampler? = null,
compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
transformBlock: (DrawTransform.() -> Unit)? = null,
style: () -> GlassStyle
): Modifier =
this
@@ -103,7 +112,8 @@ fun Modifier.liquidGlass(
LiquidGlassElement(
state = state,
style = style,
luminanceSampler = luminanceSampler
luminanceSampler = luminanceSampler,
transformBlock = transformBlock
) then GlassBrushElement(
style = style
) then GlassHighlightElement(
@@ -118,14 +128,16 @@ fun Modifier.liquidGlass(
private class LiquidGlassElement(
val state: LiquidGlassProviderState,
val style: () -> GlassStyle,
val luminanceSampler: LuminanceSampler?
val luminanceSampler: LuminanceSampler?,
val transformBlock: (DrawTransform.() -> Unit)?
) : ModifierNodeElement<LiquidGlassNode>() {
override fun create(): LiquidGlassNode {
return LiquidGlassNode(
state = state,
style = style,
luminanceSampler = luminanceSampler
luminanceSampler = luminanceSampler,
transformBlock = transformBlock
)
}
@@ -133,7 +145,8 @@ private class LiquidGlassElement(
node.update(
state = state,
style = style,
luminanceSampler = luminanceSampler
luminanceSampler = luminanceSampler,
transformBlock = transformBlock
)
}
@@ -142,6 +155,7 @@ private class LiquidGlassElement(
properties["state"] = state
properties["style"] = style
properties["luminanceSampler"] = luminanceSampler
properties["transformBlock"] = transformBlock
}
override fun equals(other: Any?): Boolean {
@@ -151,6 +165,7 @@ private class LiquidGlassElement(
if (state != other.state) return false
if (style != other.style) return false
if (luminanceSampler != other.luminanceSampler) return false
if (transformBlock != other.transformBlock) return false
return true
}
@@ -159,6 +174,7 @@ private class LiquidGlassElement(
var result = state.hashCode()
result = 31 * result + style.hashCode()
result = 31 * result + (luminanceSampler?.hashCode() ?: 0)
result = 31 * result + (transformBlock?.hashCode() ?: 0)
return result
}
}
@@ -167,7 +183,8 @@ private class LiquidGlassElement(
private class LiquidGlassNode(
var state: LiquidGlassProviderState,
var style: () -> GlassStyle,
var luminanceSampler: LuminanceSampler?
var luminanceSampler: LuminanceSampler?,
var transformBlock: (DrawTransform.() -> Unit)?
) : GlobalPositionAwareModifierNode, ObserverModifierNode, DelegatingNode() {
override val shouldAutoInvalidate: Boolean = false
@@ -177,6 +194,7 @@ private class LiquidGlassNode(
private var samplerJob: Job? = null
private var samplerLayer: GraphicsLayer? = null
private var samplerChanged = true
private var currentStyle = style()
@@ -197,7 +215,7 @@ private class LiquidGlassNode(
private var _innerRefractionRenderEffect: RenderEffect? = null
private var _renderEffect: androidx.compose.ui.graphics.RenderEffect? = null
private var transformBlockChanged = true
private var _position: Offset? = null
private val drawNode = delegate(CacheDrawModifierNode {
@@ -309,15 +327,19 @@ private class LiquidGlassNode(
}
val position = position
if (renderEffectChanged || _position != position) {
_position = position
val positionChanged = _position != position
_position = position
if (renderEffectChanged || transformBlockChanged || positionChanged) {
transformBlockChanged = false
graphicsLayer?.record {
translate(-position.x, -position.y) {
drawLayer(state.graphicsLayer)
}
}
if (samplerJob != null) {
samplerLayer?.record {
val transformBlock = transformBlock
if (transformBlock != null) {
withTransform(transformBlock) {
translate(-position.x, -position.y) {
drawLayer(state.graphicsLayer)
}
}
} else {
translate(-position.x, -position.y) {
drawLayer(state.graphicsLayer)
}
@@ -325,6 +347,23 @@ private class LiquidGlassNode(
}
}
if (samplerChanged) {
if (luminanceSampler != null) {
samplerLayer =
requireGraphicsContext().createGraphicsLayer().apply {
compositingStrategy = androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
}
}
}
if (samplerChanged || positionChanged) {
samplerLayer?.record {
translate(-position.x, -position.y) {
drawLayer(state.graphicsLayer)
}
}
}
samplerChanged = false
onDrawBehind {
graphicsLayer?.let { layer ->
drawLayer(layer)
@@ -350,12 +389,63 @@ private class LiquidGlassNode(
compositingStrategy = androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
}
luminanceSampler?.let { sampler ->
samplerLayer =
graphicsContext.createGraphicsLayer().apply {
compositingStrategy = androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
}
updateStyle()
updateSampler()
}
override fun onDetach() {
samplerJob?.cancel()
samplerJob = null
val graphicsContext = requireGraphicsContext()
graphicsLayer?.let { layer ->
graphicsContext.releaseGraphicsLayer(layer)
graphicsLayer = null
}
samplerLayer?.let { layer ->
graphicsContext.releaseGraphicsLayer(layer)
samplerLayer = null
samplerChanged = true
}
}
fun update(
state: LiquidGlassProviderState,
style: () -> GlassStyle,
luminanceSampler: LuminanceSampler?,
transformBlock: (DrawTransform.() -> Unit)?
) {
if (this.state != state ||
this.style != style
) {
this.state = state
this.style = style
updateStyle()
}
if (this.luminanceSampler != luminanceSampler) {
this.luminanceSampler = luminanceSampler
updateSampler()
}
if (this.transformBlock != transformBlock) {
this.transformBlock = transformBlock
transformBlockChanged = true
drawNode.invalidateDrawCache()
}
}
private fun updateStyle() {
observeReads { currentStyle = style() }
drawNode.invalidateDrawCache()
}
private fun updateSampler() {
samplerJob?.cancel()
samplerJob = null
samplerLayer?.let { layer ->
requireGraphicsContext().releaseGraphicsLayer(layer)
samplerLayer = null
}
this.luminanceSampler?.let { sampler ->
samplerJob =
coroutineScope.launch {
while (isActive) {
@@ -366,44 +456,7 @@ private class LiquidGlassNode(
}
}
}
updateStyle()
}
override fun onDetach() {
val graphicsContext = requireGraphicsContext()
graphicsLayer?.let { layer ->
graphicsContext.releaseGraphicsLayer(layer)
graphicsLayer = null
}
samplerJob?.cancel()
samplerJob = null
samplerLayer?.let { layer ->
graphicsContext.releaseGraphicsLayer(layer)
samplerLayer = null
}
}
fun update(
state: LiquidGlassProviderState,
style: () -> GlassStyle,
luminanceSampler: LuminanceSampler?
) {
if (this.state != state ||
this.style != style ||
this.luminanceSampler != luminanceSampler
) {
this.state = state
this.style = style
this.luminanceSampler = luminanceSampler
updateStyle()
}
}
private fun updateStyle() {
observeReads { currentStyle = style() }
samplerChanged = true
drawNode.invalidateDrawCache()
}
}
@@ -21,16 +21,8 @@ class ContinuousLuminanceSampler(
val onAnimateToLuminance: ((animationSpec: FiniteAnimationSpec<Float>, luminance: Float) -> Unit)? = null
) : LuminanceSampler {
private val luminanceAnimation = Animatable(0f)
private val animationSpec =
tween<Float>(durationMillis.toInt(), 0, easing)
override val sampleIntervalMillis: Long = 0L
override val luminance: Float
get() = luminanceAnimation.value.fastCoerceIn(0f, 1f)
private val impulseLuminanceSampler =
ImpulseLuminanceSampler(
sampleIntervalMillis = sampleIntervalMillis,
@@ -38,8 +30,16 @@ class ContinuousLuminanceSampler(
scaledSize = scaledSize
)
private val luminanceAnimation = Animatable(0f)
override val luminance: Float
get() = luminanceAnimation.value.fastCoerceIn(0f, 1f)
private var hasValidValue = false
private val animationSpec =
tween<Float>(durationMillis.toInt(), 0, easing)
override suspend fun sample(graphicsLayer: GraphicsLayer) {
impulseLuminanceSampler.sample(graphicsLayer)
val sampledLuminance = impulseLuminanceSampler.luminance
@@ -41,39 +41,40 @@ class ImpulseLuminanceSampler(
val scaledHeight = scaledSize.height
if (!graphicsLayer.isReleased && graphicsLayer.size.width > 0 && graphicsLayer.size.height > 0) {
withContext(Dispatchers.IO) {
val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap()
val scaledBitmap =
val bitmap =
withContext(Dispatchers.IO) {
graphicsLayer.toImageBitmap().asAndroidBitmap()
}
val scaledBitmap =
withContext(Dispatchers.IO) {
Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false)
.copy(Bitmap.Config.ARGB_8888, false)
var sumLuminance = 0f
val totalPixels = scaledWidth * scaledHeight
for (y in 0 until scaledHeight) {
for (x in 0 until scaledWidth) {
val pixel = scaledBitmap.getPixel(x, y)
val red = (pixel shr 16) and 0xFF
val green = (pixel shr 8) and 0xFF
val blue = pixel and 0xFF
val pixelLuminance = (0.299f * red + 0.587f * green + 0.114f * blue) / 255f
sumLuminance += pixelLuminance
}
}
val newLuminance =
((sumLuminance / totalPixels / precision).fastRoundToInt() * precision).fastCoerceIn(0f, 1f)
if (!newLuminance.isNaN()) {
withContext(Dispatchers.Main) {
onChangeToLuminance?.invoke(newLuminance)
luminance = newLuminance
}
}
var sumLuminance = 0f
val totalPixels = scaledWidth * scaledHeight
scaledBitmap.recycle()
bitmap.recycle()
for (y in 0 until scaledHeight) {
for (x in 0 until scaledWidth) {
val pixel = scaledBitmap.getPixel(x, y)
val red = (pixel shr 16) and 0xFF
val green = (pixel shr 8) and 0xFF
val blue = pixel and 0xFF
val pixelLuminance = (0.299f * red + 0.587f * green + 0.114f * blue) / 255f
sumLuminance += pixelLuminance
}
}
val newLuminance =
((sumLuminance / totalPixels / precision).fastRoundToInt() * precision).fastCoerceIn(0f, 1f)
if (!newLuminance.isNaN()) {
onChangeToLuminance?.invoke(newLuminance)
luminance = newLuminance
}
scaledBitmap.recycle()
bitmap.recycle()
}
}
}