Huge API refactor of modifiers

This commit is contained in:
Kyant0 committed 2025-07-14 13:29:04 +08:00
1 parent 3d397744b9
commit 19051076cd
12 files changed
+273 -278

No files matched your search

+21 -24
View File
@@ -19,34 +19,31 @@ It doesn't support nested glass effects for now.
### Examples
```kotlin
val providerState = rememberLiquidGlassProviderState()
val providerState = rememberLiquidGlassProviderState(
backgroundColor = Color.White
)
CompositionLocalProvider(
LocalLiquidGlassProviderState provides providerState
) {
Box(
Modifier
.liquidGlassProvider(providerState)
)
// content behind the glass
Box(Modifier.liquidGlassProvider(providerState))
Box(
Modifier
.liquidGlass(
LiquidGlassStyle(
CornerShape.large,
innerRefraction = InnerRefraction(
height = RefractionValue(8.dp),
amount = RefractionValue.Full
),
material = GlassMaterial(
blurRadius = 8.dp,
whitePoint = 0.1f,
chromaMultiplier = 1.5f
)
)
// glass
Box(
Modifier.liquidGlass(
providerState,
LiquidGlassStyle(
CornerShape.large,
innerRefraction = InnerRefraction(
height = RefractionValue(8.dp),
amount = RefractionValue.Full
),
material = GlassMaterial(
blurRadius = 8.dp,
whitePoint = 0.1f,
chromaMultiplier = 1.5f
)
)
)
}
)
```
## Glass parameters
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,6 +30,7 @@ import com.kyant.expressa.ui.Text
import com.kyant.glass.R
import com.kyant.liquidglass.GlassMaterial
import com.kyant.liquidglass.InnerRefraction
import com.kyant.liquidglass.LiquidGlassProviderState
import com.kyant.liquidglass.LiquidGlassStyle
import com.kyant.liquidglass.RefractionValue
import com.kyant.liquidglass.liquidGlass
@@ -37,6 +38,7 @@ import com.kyant.liquidglass.liquidGlass
@Composable
fun ConfigurationBottomSheet(
state: PreviewState,
providerState: LiquidGlassProviderState,
modifier: Modifier = Modifier
) {
// val providerState = rememberLiquidGlassProviderState()
@@ -48,6 +50,7 @@ fun ConfigurationBottomSheet(
modifier
// .liquidGlassProvider(providerState)
.liquidGlass(
providerState,
LiquidGlassStyle(
shape = CornerShape.extraLarge,
innerRefraction = InnerRefraction(
@@ -88,7 +91,7 @@ fun ConfigurationBottomSheet(
IconButton(
{ state.configurationMode = null },
Modifier.liquidGlass(LiquidGlassStyle(CornerShape.full)),
Modifier.liquidGlass(providerState, LiquidGlassStyle(CornerShape.full)),
colors = IconButtonColors.tonal(
containerColor = primaryContainer.copy(alpha = 0.85f)
)
@@ -112,24 +115,28 @@ fun ConfigurationBottomSheet(
state.blurRadius,
stringResource(R.string.blur_radius),
{ Icon(painterResource(R.drawable.lens_blur_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.contrast,
stringResource(R.string.contrast),
{ Icon(painterResource(R.drawable.contrast_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.whitePoint,
stringResource(R.string.white_point),
{ Icon(painterResource(R.drawable.wb_sunny_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.chromaMultiplier,
stringResource(R.string.chroma_multiplier),
{ Icon(painterResource(R.drawable.palette_24px)) },
providerState,
Modifier.fillMaxWidth()
)
}
@@ -137,7 +144,7 @@ fun ConfigurationBottomSheet(
ConfigurationMode.Advanced -> {
Button(
{ state.unsafeMode = !state.unsafeMode },
Modifier.liquidGlass(LiquidGlassStyle(CornerShape.full)),
Modifier.liquidGlass(providerState, LiquidGlassStyle(CornerShape.full)),
colors = ButtonColors.filled(
containerColor = primary.copy(alpha = 0.85f)
)
@@ -152,18 +159,21 @@ fun ConfigurationBottomSheet(
state.bleedBlurRadius,
stringResource(R.string.bleed_blur_radius),
{ Icon(painterResource(R.drawable.lens_blur_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.eccentricFactor,
stringResource(R.string.eccentric_factor),
{ Icon(painterResource(R.drawable.all_out_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.dispersionHeight,
stringResource(R.string.dispersion_height),
{ Icon(painterResource(R.drawable.star_shine_24px)) },
providerState,
Modifier.fillMaxWidth()
)
}
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -44,7 +43,6 @@ import androidx.compose.ui.util.fastCoerceAtMost
import com.kyant.expressa.prelude.*
import com.kyant.expressa.shape.RoundedRectangle
import com.kyant.glass.R
import com.kyant.liquidglass.LocalLiquidGlassProviderState
import com.kyant.liquidglass.liquidGlassProvider
import com.kyant.liquidglass.rememberLiquidGlassProviderState
import org.intellij.lang.annotations.Language
@@ -61,30 +59,28 @@ fun Preview() {
state.imageBitmap?.let { BitmapPainter(it) }
?: painterResource(R.drawable.homescreen)
val providerState = rememberLiquidGlassProviderState()
CompositionLocalProvider(
LocalLiquidGlassProviderState provides providerState
) {
Box(Modifier.fillMaxSize()) {
// background
Image(
painter, null,
Modifier
.fillMaxSize()
.drawWithContent {
drawContent()
val providerState = rememberLiquidGlassProviderState(backgroundColor)
graphicsLayer.record {
drawRect(backgroundColor)
this@drawWithContent.drawContent()
}
Box(Modifier.fillMaxSize()) {
// background
Image(
painter, null,
Modifier
.fillMaxSize()
.drawWithContent {
drawContent()
graphicsLayer.record {
drawRect(backgroundColor)
this@drawWithContent.drawContent()
}
.liquidGlassProvider(providerState),
contentScale = ContentScale.Crop
)
}
.liquidGlassProvider(providerState),
contentScale = ContentScale.Crop
)
@Language("AGSL")
val colorShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
@Language("AGSL")
val colorShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
const half3 rgbToY = half3(0.2126, 0.7152, 0.0722);
float luma(half4 color) {
@@ -92,8 +88,8 @@ fun Preview() {
}
""".trimIndent()
@Language("AGSL")
val refractionShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
@Language("AGSL")
val refractionShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
float circleMap(float x) {
return 1.0 - sqrt(1.0 - x * x);
}
@@ -147,24 +143,24 @@ fun Preview() {
}
""".trimIndent()
// glass
Box(
Modifier
.graphicsLayer { // transform and clip
translationX = state.offset.x
translationY = state.offset.y
clip = true
shape = RoundedRectangle(state.cornerRadius.value)
}
.graphicsLayer { // white point & chroma boost
val contrast = state.contrast.value
val whitePoint = state.whitePoint.value
val chromaMultiplier = state.chromaMultiplier.value
// glass
Box(
Modifier
.graphicsLayer { // transform and clip
translationX = state.offset.x
translationY = state.offset.y
clip = true
shape = RoundedRectangle(state.cornerRadius.value)
}
.graphicsLayer { // white point & chroma boost
val contrast = state.contrast.value
val whitePoint = state.whitePoint.value
val chromaMultiplier = state.chromaMultiplier.value
if (contrast != 0f || whitePoint != 0f || chromaMultiplier != 1f) {
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
if (contrast != 0f || whitePoint != 0f || chromaMultiplier != 1f) {
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float contrast;
@@ -194,19 +190,19 @@ fun Preview() {
return color;
}"""
).apply {
setFloatUniform("contrast", contrast)
setFloatUniform("whitePoint", whitePoint)
setFloatUniform("chromaMultiplier", chromaMultiplier)
},
"image"
).asComposeRenderEffect()
}
).apply {
setFloatUniform("contrast", contrast)
setFloatUniform("whitePoint", whitePoint)
setFloatUniform("chromaMultiplier", chromaMultiplier)
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // dispersion effect
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
}
.graphicsLayer { // dispersion effect
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -295,22 +291,22 @@ fun Preview() {
return color;
}
}"""
).apply {
val cornerRadius =
state.cornerRadius.value.toPx()
.fastCoerceAtMost(size.minDimension / 2f)
).apply {
val cornerRadius =
state.cornerRadius.value.toPx()
.fastCoerceAtMost(size.minDimension / 2f)
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", cornerRadius)
setFloatUniform("dispersionHeight", state.dispersionHeight.value.toPx())
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // refraction & bleed
val refractionRenderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", cornerRadius)
setFloatUniform("dispersionHeight", state.dispersionHeight.value.toPx())
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // refraction & bleed
val refractionRenderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -335,22 +331,22 @@ fun Preview() {
return color;
}
}"""
).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
setFloatUniform("refractionHeight", state.refractionHeight.value.toPx())
setFloatUniform("refractionAmount", state.refractionAmount.value.toPx())
setFloatUniform("eccentricFactor", state.eccentricFactor.value)
setFloatUniform("refractionHeight", state.refractionHeight.value.toPx())
setFloatUniform("refractionAmount", state.refractionAmount.value.toPx())
setFloatUniform("eccentricFactor", state.eccentricFactor.value)
setFloatUniform("bleedOpacity", state.bleedOpacity.value)
},
"image"
)
setFloatUniform("bleedOpacity", state.bleedOpacity.value)
},
"image"
)
val bleedRenderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
val bleedRenderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -368,110 +364,110 @@ fun Preview() {
color.rgb = mix(color.rgb, half3(1.0), 0.5 * circleMap(1.0 - luma));
return color;
}"""
).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
setFloatUniform("eccentricFactor", state.eccentricFactor.value)
setFloatUniform("bleedAmount", state.bleedAmount.value.toPx())
},
"image"
)
setFloatUniform("eccentricFactor", state.eccentricFactor.value)
setFloatUniform("bleedAmount", state.bleedAmount.value.toPx())
},
"image"
)
renderEffect =
if (state.bleedOpacity.value > 0f) {
val bleedBlurRadiusPx = state.bleedBlurRadius.value.toPx()
val blurredBleedRenderEffect =
if (bleedBlurRadiusPx > 0f) {
RenderEffect.createChainEffect(
bleedRenderEffect,
RenderEffect.createBlurEffect(
bleedBlurRadiusPx,
bleedBlurRadiusPx,
Shader.TileMode.CLAMP
)
renderEffect =
if (state.bleedOpacity.value > 0f) {
val bleedBlurRadiusPx = state.bleedBlurRadius.value.toPx()
val blurredBleedRenderEffect =
if (bleedBlurRadiusPx > 0f) {
RenderEffect.createChainEffect(
bleedRenderEffect,
RenderEffect.createBlurEffect(
bleedBlurRadiusPx,
bleedBlurRadiusPx,
Shader.TileMode.CLAMP
)
} else {
bleedRenderEffect
}
)
} else {
bleedRenderEffect
}
RenderEffect.createBlendModeEffect(
blurredBleedRenderEffect,
refractionRenderEffect,
android.graphics.BlendMode.SRC_OVER
).asComposeRenderEffect()
} else {
refractionRenderEffect.asComposeRenderEffect()
}
}
.graphicsLayer { // blur
val blurRadiusPx = state.blurRadius.value.toPx()
if (blurRadiusPx > 0f) {
renderEffect =
RenderEffect.createBlurEffect(
blurRadiusPx,
blurRadiusPx,
Shader.TileMode.DECAL
).asComposeRenderEffect()
RenderEffect.createBlendModeEffect(
blurredBleedRenderEffect,
refractionRenderEffect,
android.graphics.BlendMode.SRC_OVER
).asComposeRenderEffect()
} else {
refractionRenderEffect.asComposeRenderEffect()
}
}
.drawBehind {
clipRect(0f, 0f, rect.width, rect.height) {
translate(-rect.left, -rect.top) {
drawLayer(graphicsLayer)
}
}
}
.onGloballyPositioned { layoutCoordinates ->
rect = layoutCoordinates.boundsInParent()
}
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
state.offset += pan
state.size = DpSize(
(state.size.width * zoom).coerceIn(
state.minSize.width,
containerSize.width.toDp()
),
(state.size.height * zoom).coerceIn(
state.minSize.height,
containerSize.height.toDp()
)
)
}
}
.layout { measurable, constraints ->
val size = state.size.toSize().roundToIntSize()
val width = size.width.fastCoerceAtMost(constraints.maxWidth)
val height = size.height.fastCoerceAtMost(constraints.maxHeight)
val placeable = measurable.measure(Constraints.fixed(width, height))
layout(width, height) {
placeable.place(0, 0)
}
}
.align(Alignment.Center)
)
if (state.displayControls) {
PreviewControls(
state,
{ rect }
)
}
AnimatedContent(
state.isInConfigurationMode,
Modifier
.padding(16.dp)
.safeDrawingPadding()
.align(Alignment.BottomCenter)
) { isInConfigurationMode ->
if (!isInConfigurationMode) {
PreviewToolbar(state)
} else {
ConfigurationBottomSheet(state)
}
.graphicsLayer { // blur
val blurRadiusPx = state.blurRadius.value.toPx()
if (blurRadiusPx > 0f) {
renderEffect =
RenderEffect.createBlurEffect(
blurRadiusPx,
blurRadiusPx,
Shader.TileMode.DECAL
).asComposeRenderEffect()
}
}
.drawBehind {
clipRect(0f, 0f, rect.width, rect.height) {
translate(-rect.left, -rect.top) {
drawLayer(graphicsLayer)
}
}
}
.onGloballyPositioned { layoutCoordinates ->
rect = layoutCoordinates.boundsInParent()
}
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
state.offset += pan
state.size = DpSize(
(state.size.width * zoom).coerceIn(
state.minSize.width,
containerSize.width.toDp()
),
(state.size.height * zoom).coerceIn(
state.minSize.height,
containerSize.height.toDp()
)
)
}
}
.layout { measurable, constraints ->
val size = state.size.toSize().roundToIntSize()
val width = size.width.fastCoerceAtMost(constraints.maxWidth)
val height = size.height.fastCoerceAtMost(constraints.maxHeight)
val placeable = measurable.measure(Constraints.fixed(width, height))
layout(width, height) {
placeable.place(0, 0)
}
}
.align(Alignment.Center)
)
if (state.displayControls) {
PreviewControls(
state,
providerState,
rect = { rect }
)
}
AnimatedContent(
state.isInConfigurationMode,
Modifier
.padding(16.dp)
.safeDrawingPadding()
.align(Alignment.BottomCenter)
) { isInConfigurationMode ->
if (!isInConfigurationMode) {
PreviewToolbar(state, providerState)
} else {
ConfigurationBottomSheet(state, providerState)
}
}
}
@@ -31,11 +31,13 @@ import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.glass.R
import com.kyant.liquidglass.LiquidGlassProviderState
import kotlin.math.sqrt
@Composable
fun PreviewControls(
state: PreviewState,
providerState: LiquidGlassProviderState,
rect: () -> Rect,
modifier: Modifier = Modifier
) {
@@ -130,6 +132,7 @@ fun PreviewControls(
}
)
},
providerState,
Modifier
.graphicsLayer {
val rect = rect()
@@ -169,24 +172,28 @@ fun PreviewControls(
state.bleedAmount,
stringResource(R.string.bleed_amount),
{ Icon(painterResource(R.drawable.center_focus_weak_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.bleedOpacity,
stringResource(R.string.bleed_opacity),
{ Icon(painterResource(R.drawable.opacity_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.refractionAmount,
stringResource(R.string.refraction_amount),
{ Icon(painterResource(R.drawable.center_focus_weak_24px)) },
providerState,
Modifier.fillMaxWidth()
)
SliderChip(
state.refractionHeight,
stringResource(R.string.refraction_height),
{ Icon(painterResource(R.drawable.water_lux_24px)) },
providerState,
Modifier.fillMaxWidth()
)
}
@@ -28,6 +28,7 @@ import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.glass.R
import com.kyant.liquidglass.GlassMaterial
import com.kyant.liquidglass.LiquidGlassProviderState
import com.kyant.liquidglass.LiquidGlassStyle
import com.kyant.liquidglass.liquidGlass
import kotlinx.coroutines.launch
@@ -35,6 +36,7 @@ import kotlinx.coroutines.launch
@Composable
fun PreviewToolbar(
state: PreviewState,
providerState: LiquidGlassProviderState,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
@@ -53,7 +55,7 @@ fun PreviewToolbar(
) {
IconButton(
{ scope.launch { state.reset() } },
Modifier.liquidGlass(iconButtonLiquidGlassStyle),
Modifier.liquidGlass(providerState, iconButtonLiquidGlassStyle),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = primary.copy(alpha = 0.6f),
@@ -78,6 +80,7 @@ fun PreviewToolbar(
IconButton(
{ state.displayControls = !state.displayControls },
Modifier.liquidGlass(
providerState,
iconButtonLiquidGlassStyle.copy(
material = GlassMaterial(
whitePoint = if (state.displayControls) 0.5f else -0.5f
@@ -103,7 +106,7 @@ fun PreviewToolbar(
}
IconButton(
{ state.configurationMode = ConfigurationMode.Colors },
Modifier.liquidGlass(iconButtonLiquidGlassStyle),
Modifier.liquidGlass(providerState, iconButtonLiquidGlassStyle),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = Color.Transparent,
@@ -117,7 +120,7 @@ fun PreviewToolbar(
}
IconButton(
{ state.configurationMode = ConfigurationMode.Advanced },
Modifier.liquidGlass(iconButtonLiquidGlassStyle),
Modifier.liquidGlass(providerState, iconButtonLiquidGlassStyle),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = Color.Transparent,
@@ -148,7 +151,7 @@ fun PreviewToolbar(
)
)
},
Modifier.liquidGlass(iconButtonLiquidGlassStyle),
Modifier.liquidGlass(providerState, iconButtonLiquidGlassStyle),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = primary.copy(alpha = 0.6f),
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -27,6 +26,7 @@ import com.kyant.expressa.ui.LocalTextStyle
import com.kyant.expressa.ui.ProvideTextStyle
import com.kyant.expressa.ui.Text
import com.kyant.liquidglass.GlassMaterial
import com.kyant.liquidglass.LiquidGlassProviderState
import com.kyant.liquidglass.LiquidGlassStyle
import com.kyant.liquidglass.liquidGlass
@@ -35,6 +35,7 @@ fun <T : Comparable<T>> SliderChip(
value: PreviewState.LiquidGlassParamValue<T>,
label: String?,
icon: @Composable () -> Unit,
providerState: LiquidGlassProviderState,
modifier: Modifier = Modifier,
dragDelta: (dragAmount: Offset) -> Float = { it.x }
) {
@@ -47,12 +48,11 @@ fun <T : Comparable<T>> SliderChip(
Row(
modifier
.liquidGlass(
remember {
LiquidGlassStyle(
CornerShape.extraLarge,
material = GlassMaterial(chromaMultiplier = 1.5f)
)
}
providerState,
LiquidGlassStyle(
CornerShape.extraLarge,
material = GlassMaterial(chromaMultiplier = 1.5f)
)
)
.clip(CornerShape.full)
.background(surfaceBright.copy(alpha = 0.85f))
@@ -4,7 +4,6 @@ import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -37,15 +36,14 @@ import androidx.compose.ui.unit.Constraints
import kotlin.math.ceil
import kotlin.math.min
@Composable
fun Modifier.liquidGlass(
style: LiquidGlassStyle,
providerState: LiquidGlassProviderState = LocalLiquidGlassProviderState.current
state: LiquidGlassProviderState,
style: LiquidGlassStyle
): Modifier =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this then LiquidGlassElement(
style = style,
providerState = providerState
state = state,
style = style
)
} else {
this
@@ -53,51 +51,51 @@ fun Modifier.liquidGlass(
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class LiquidGlassElement(
val style: LiquidGlassStyle,
val providerState: LiquidGlassProviderState
val state: LiquidGlassProviderState,
val style: LiquidGlassStyle
) : ModifierNodeElement<LiquidGlassModifierNode>() {
override fun create(): LiquidGlassModifierNode {
return LiquidGlassModifierNode(
style = style,
providerState = providerState
state = state,
style = style
)
}
override fun update(node: LiquidGlassModifierNode) {
node.update(
style = style,
providerState = providerState
state = state,
style = style
)
}
override fun InspectorInfo.inspectableProperties() {
name = "liquidGlass"
properties["state"] = state
properties["style"] = style
properties["providerState"] = providerState
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LiquidGlassElement) return false
if (state != other.state) return false
if (style != other.style) return false
if (providerState != other.providerState) return false
return true
}
override fun hashCode(): Int {
var result = style.hashCode()
result = 31 * result + providerState.hashCode()
var result = state.hashCode()
result = 31 * result + style.hashCode()
return result
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
internal class LiquidGlassModifierNode(
var style: LiquidGlassStyle,
var providerState: LiquidGlassProviderState
var state: LiquidGlassProviderState,
var style: LiquidGlassStyle
) : LayoutModifierNode, GlobalPositionAwareModifierNode, DelegatingNode() {
override val shouldAutoInvalidate: Boolean = false
@@ -260,7 +258,7 @@ internal class LiquidGlassModifierNode(
val rect = rect ?: return@onDrawBehind
graphicsLayer.record {
translate(-rect.left, -rect.top) {
drawLayer(providerState.graphicsLayer)
drawLayer(state.graphicsLayer)
}
}
drawLayer(graphicsLayer)
@@ -299,20 +297,20 @@ internal class LiquidGlassModifierNode(
}
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
rect = providerState.rect?.let {
rect = state.rect?.let {
coordinates.boundsInRoot().translate(-it.topLeft)
}
}
fun update(
style: LiquidGlassStyle,
providerState: LiquidGlassProviderState
state: LiquidGlassProviderState,
style: LiquidGlassStyle
) {
if (this.style != style ||
this.providerState != providerState
if (this.state != state ||
this.style != style
) {
this.state = state
this.style = style
this.providerState = providerState
drawWithCacheModifierNode.invalidateDrawCache()
invalidateSubtree()
}
@@ -3,7 +3,6 @@ package com.kyant.liquidglass
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
@@ -13,42 +12,30 @@ import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
fun Modifier.liquidGlassProvider(
state: LiquidGlassProviderState,
backgroundColor: Color? = null
state: LiquidGlassProviderState
): Modifier =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this then LiquidGlassProviderElement(
state = state,
backgroundColor = backgroundColor
)
this then LiquidGlassProviderElement(state = state)
} else {
this
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class LiquidGlassProviderElement(
val state: LiquidGlassProviderState,
val backgroundColor: Color?
val state: LiquidGlassProviderState
) : ModifierNodeElement<LiquidGlassProviderModifierNode>() {
override fun create(): LiquidGlassProviderModifierNode {
return LiquidGlassProviderModifierNode(
state = state,
backgroundColor = backgroundColor
)
return LiquidGlassProviderModifierNode(state = state)
}
override fun update(node: LiquidGlassProviderModifierNode) {
node.update(
state = state,
backgroundColor = backgroundColor
)
node.update(state = state)
}
override fun InspectorInfo.inspectableProperties() {
name = "liquidGlassProvider"
properties["state"] = state
properties["backgroundColor"] = backgroundColor
}
override fun equals(other: Any?): Boolean {
@@ -56,30 +43,27 @@ private class LiquidGlassProviderElement(
if (other !is LiquidGlassProviderElement) return false
if (state != other.state) return false
if (backgroundColor != other.backgroundColor) return false
return true
}
override fun hashCode(): Int {
var result = state.hashCode()
result = 31 * result + backgroundColor.hashCode()
return result
return state.hashCode()
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
internal class LiquidGlassProviderModifierNode(
var state: LiquidGlassProviderState,
var backgroundColor: Color?
var state: LiquidGlassProviderState
) : DrawModifierNode, GlobalPositionAwareModifierNode, Modifier.Node() {
override val shouldAutoInvalidate: Boolean = false
override fun ContentDrawScope.draw() {
drawContent()
state.graphicsLayer.record {
backgroundColor?.let { drawRect(it) }
state.backgroundColor?.let { drawRect(it) }
this@draw.drawContent()
}
}
@@ -89,10 +73,8 @@ internal class LiquidGlassProviderModifierNode(
}
fun update(
state: LiquidGlassProviderState,
backgroundColor: Color?
state: LiquidGlassProviderState
) {
this.state = state
this.backgroundColor = backgroundColor
}
}
@@ -1,28 +1,30 @@
package com.kyant.liquidglass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
val LocalLiquidGlassProviderState: ProvidableCompositionLocal<LiquidGlassProviderState> =
staticCompositionLocalOf { error("CompositionLocal LocalLiquidGlassProviderState not present") }
@Composable
fun rememberLiquidGlassProviderState(): LiquidGlassProviderState {
fun rememberLiquidGlassProviderState(
backgroundColor: Color?
): LiquidGlassProviderState {
val graphicsLayer = rememberGraphicsLayer()
return remember(graphicsLayer) {
LiquidGlassProviderState(graphicsLayer)
return remember(backgroundColor, graphicsLayer) {
LiquidGlassProviderState(
backgroundColor = backgroundColor,
graphicsLayer = graphicsLayer
)
}
}
class LiquidGlassProviderState internal constructor(
val backgroundColor: Color?,
internal val graphicsLayer: GraphicsLayer
) {