android: improve liquid glass sliders

This commit is contained in:
Kavish Devar
2025-09-23 00:27:39 +05:30
parent 4bc76de750
commit 8760757b76

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.librepods.composables
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
@@ -29,9 +28,9 @@ import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -60,6 +59,9 @@ import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -96,8 +98,7 @@ fun StyledSlider(
endIcon: String? = null,
startLabel: String? = null,
endLabel: String? = null,
independent: Boolean = false,
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
independent: Boolean = false
) {
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val isLightTheme = !isSystemInDarkTheme()
@@ -119,16 +120,27 @@ fun StyledSlider(
val animationScope = rememberCoroutineScope()
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
val progressAnimation = remember { Animatable(0f) }
val trackBackdrop = rememberBackdrop()
val innerShadowLayer =
rememberGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Offscreen
}
val sliderBackdrop = rememberBackdrop()
val trackWidthState = remember { mutableFloatStateOf(0f) }
val trackPositionState = remember { mutableFloatStateOf(0f) }
val startIconWidthState = remember { mutableFloatStateOf(0f) }
val endIconWidthState = remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current
val content = @Composable {
Box(Modifier.fillMaxWidth()) {
Box(Modifier
.backdrop(sliderBackdrop)
.fillMaxWidth()) {
Column(
modifier = modifier.fillMaxWidth(1f).padding(vertical = 8.dp),
modifier = Modifier
.fillMaxWidth(1f)
.padding(vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -184,18 +196,22 @@ fun StyledSlider(
color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(horizontal = 12.dp)
modifier = Modifier
.padding(horizontal = 12.dp)
.onGloballyPositioned {
startIconWidthState.floatValue = it.size.width.toFloat()
}
)
}
BoxWithConstraints(
Box(
Modifier
.weight(1f),
contentAlignment = Alignment.CenterStart
.weight(1f)
.onSizeChanged { trackWidthState.floatValue = it.width.toFloat() }
.onGloballyPositioned {
trackPositionState.floatValue =
it.positionInParent().y + it.size.height / 2f
}
) {
val density = LocalDensity.current
val trackWidth = constraints.maxWidth
Box(Modifier.backdrop(trackBackdrop)) {
Box(
Modifier
.clip(RoundedCornerShape(28.dp))
@@ -212,28 +228,47 @@ fun StyledSlider(
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val fraction = fraction
val width = (fraction * constraints.maxWidth).fastRoundToInt()
val width =
(fraction * constraints.maxWidth).fastRoundToInt()
layout(width, placeable.height) {
placeable.place(0, 0)
}
}
)
}
if (endIcon != null) {
Text(
text = endIcon,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier
.padding(horizontal = 12.dp)
.onGloballyPositioned {
endIconWidthState.floatValue = it.size.width.toFloat()
}
)
}
}
}
}
Box(
Modifier
.graphicsLayer {
val fraction = fraction
val startOffset =
if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else 0f
translationX =
(-size.width / 2f + fraction * trackWidth)
.fastCoerceIn(
-size.width / 4f,
trackWidth - size.width * 3f / 4f
)
startOffset + fraction * trackWidthState.floatValue - size.width / 2f
translationY = trackPositionState.floatValue / 2f
}
.draggable(
rememberDraggableState { delta ->
val trackWidth = trackWidth - with(density) { 40f.dp.toPx() }
val trackWidth = trackWidthState.floatValue
if (trackWidth > 0f) {
val targetFraction = fraction + delta / trackWidth
val targetValue =
lerp(valueRange.start, valueRange.endInclusive, targetFraction)
@@ -244,6 +279,7 @@ fun StyledSlider(
snapThreshold
) else targetValue
onValueChange(snappedValue)
}
},
Orientation.Horizontal,
startDragImmediately = true,
@@ -260,7 +296,7 @@ fun StyledSlider(
}
)
.drawBackdrop(
rememberCombinedBackdropDrawer(backdrop, trackBackdrop),
rememberCombinedBackdropDrawer(backdrop, sliderBackdrop),
{ RoundedCornerShape(28.dp) },
highlight = {
val progress = progressAnimation.value
@@ -313,20 +349,6 @@ fun StyledSlider(
.size(40f.dp, 24f.dp)
)
}
if (endIcon != null) {
Text(
text = endIcon,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
}
if (independent) {
@@ -350,15 +372,30 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Composable
fun StyledSliderPreview() {
val a = remember { mutableFloatStateOf(1f) }
Box(
Modifier
.background(if (isSystemInDarkTheme()) Color(0xFF121212) else Color(0xFFF0F0F0))
.padding(16.dp)
.fillMaxSize()
) {
Box (
Modifier.align(Alignment.Center)
)
{
StyledSlider(
mutableFloatState = remember {mutableFloatStateOf(1f)},
onValueChange = {},
mutableFloatState = a,
onValueChange = {
a.floatValue = it
},
valueRange = 0f..2f,
independent = true,
startIcon = "A",
endIcon = "B"
)
}
}
}