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