mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-30 18:16:42 +00:00
android: improve liquid glass sliders
This commit is contained in:
@@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user