Tweak sleep timer dialog UI

This commit is contained in:
vfsfitvnm 2022-08-29 20:07:52 +02:00
parent 37c5f66801
commit 93a40f9d15
2 changed files with 50 additions and 357 deletions

View file

@ -1,318 +0,0 @@
package it.vfsfitvnm.vimusic.ui.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.center
import androidx.compose.ui.util.lerp
import kotlin.math.absoluteValue
import kotlinx.coroutines.launch
@Composable
fun Pager(
selectedIndex: Int,
onSelectedIndex: (Int) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val velocityTracker = remember {
VelocityTracker()
}
val state = remember {
Animatable(0f)
}
var steps by remember {
mutableStateOf(emptyList<Int>())
}
Layout(
modifier = modifier
.clipToBounds()
.pointerInput(Unit) {
val function = when (orientation) {
Orientation.Vertical -> ::detectVerticalDragGestures
Orientation.Horizontal -> ::detectHorizontalDragGestures
}
function(
{},
{
val velocity = -velocityTracker.calculateVelocity().x
val initialTargetValue = splineBasedDecay<Float>(this).calculateTargetValue(
state.value,
velocity
)
velocityTracker.resetTracking()
val (targetValue, newSelectedIndex) = run {
for (i in 1..steps.lastIndex) {
val current = steps[i]
val previous = steps[i - 1]
val currentDelta = current - initialTargetValue
val previousDelta = initialTargetValue - previous
return@run when {
currentDelta >= 0 && previousDelta > 0 -> if (currentDelta < previousDelta) {
current to i
} else {
previous to i - 1
}
previousDelta <= 0 -> previous to i - 1
else -> continue
}
}
steps.last() to steps.lastIndex
}
coroutineScope.launch {
state.animateTo(
targetValue = targetValue.toFloat(),
initialVelocity = velocity,
)
}
onSelectedIndex(newSelectedIndex)
},
{},
{ change, dragAmount ->
coroutineScope.launch {
state.snapTo(state.value - dragAmount)
}
velocityTracker.addPosition(change.uptimeMillis, change.position)
change.consume()
},
)
},
content = content
) { measurables, constraints ->
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map {
it.measure(childConstraints)
}
var acc = 0
steps = placeables.map {
val dim = when (orientation) {
Orientation.Horizontal -> it.width
Orientation.Vertical -> it.height
}
val step = acc + dim / 2
acc += dim
step
}.also {
if (steps.isEmpty()) {
coroutineScope.launch {
state.animateTo(it[selectedIndex].toFloat())
}
}
}
state.updateBounds(
lowerBound = steps.first().toFloat(),
upperBound = steps.last().toFloat()
)
val layoutDimension = IntSize(
width = if (constraints.minWidth > 0 || placeables.isEmpty()) {
constraints.minWidth
} else {
placeables.maxOf {
it.width
}
},
height = if (constraints.minHeight > 0 || placeables.isEmpty()) {
constraints.minHeight
} else {
placeables.maxOf {
it.height
}
}
)
val center = when (orientation) {
Orientation.Horizontal -> layoutDimension.center.x
Orientation.Vertical -> layoutDimension.center.y
}
layout(
width = layoutDimension.width,
height = layoutDimension.height
) {
var position = center - state.value.toInt()
for (placeable in placeables) {
val otherPosition = alignment.align(
size = IntSize(
width = placeable.width,
height = placeable.height
),
space = layoutDimension,
layoutDirection = layoutDirection
).let {
when (orientation) {
Orientation.Horizontal -> it.y
Orientation.Vertical -> it.x
}
}
val placeablePosition = when (orientation) {
Orientation.Horizontal -> IntOffset(position, otherPosition)
Orientation.Vertical -> IntOffset(otherPosition, position)
}
placeable.placeWithLayer(position = placeablePosition) {
with(transformer) {
val size = when (orientation) {
Orientation.Horizontal -> placeable.width
Orientation.Vertical -> placeable.height
}.toFloat()
val offset = (center - (position + size / 2)).absoluteValue / size
apply(distance = offset)
}
}
position += when (orientation) {
Orientation.Horizontal -> placeable.width
Orientation.Vertical -> placeable.height
}
}
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T> ItemPager(
items: List<T>,
selectedIndex: Int,
onSelectedIndex: (Int) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
Pager(
modifier = modifier,
selectedIndex = selectedIndex,
onSelectedIndex = onSelectedIndex,
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T> ItemPager(
items: List<T>,
selectedValue: T,
onSelectedValue: (T) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
Pager(
modifier = modifier,
selectedIndex = items.indexOf(selectedValue).coerceAtLeast(0),
onSelectedIndex = {
onSelectedValue(items[it])
},
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T : Enum<T>> EnumPager(
value: T,
onSelectedValue: (T) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
val items = remember {
value.declaringJavaClass.enumConstants!!
}
Pager(
modifier = modifier,
selectedIndex = value.ordinal,
onSelectedIndex = {
onSelectedValue(items[it])
},
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
@Immutable
fun interface PagerTransformer {
fun GraphicsLayerScope.apply(distance: Float)
companion object {
@Stable
val Empty = PagerTransformer {}
@Stable
val Default = PagerTransformer {
val value = 1f - it.coerceIn(0f, 1f)
lerp(start = 0.85f, stop = 1f, fraction = value).also { scale ->
scaleX = scale
scaleY = scale
}
alpha = lerp(start = 0.5f, stop = 1f, fraction = value)
}
}
}

View file

@ -4,14 +4,17 @@ import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.with
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
@ -23,6 +26,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
@ -42,7 +47,6 @@ import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.Pager
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
import it.vfsfitvnm.vimusic.ui.screens.viewPlaylistsRoute
@ -465,12 +469,8 @@ fun MediaItemMenu(
isShowingSleepTimerDialog = false
}
) {
var hours by remember {
mutableStateOf(0)
}
var minutes by remember {
mutableStateOf(0)
var amount by remember {
mutableStateOf(1)
}
BasicText(
@ -482,43 +482,54 @@ fun MediaItemMenu(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally
),
modifier = Modifier
.padding(vertical = 16.dp)
) {
Pager(
selectedIndex = hours,
onSelectedIndex = {
hours = it
},
orientation = Orientation.Vertical,
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(horizontal = 8.dp)
.height(92.dp)
.alpha(if (amount <= 1) 0.5f else 1f)
.clip(CircleShape)
.clickable(enabled = amount > 1) { amount-- }
.size(48.dp)
.background(colorPalette.background0)
) {
repeat(12) {
BasicText(
text = "$it h",
style = typography.xs.semiBold
)
}
BasicText(
text = "-",
style = typography.xs.semiBold
)
}
Pager(
selectedIndex = minutes,
onSelectedIndex = {
minutes = it
},
orientation = Orientation.Vertical,
Box(contentAlignment = Alignment.Center) {
BasicText(
text = "88h 88m",
style = typography.s.semiBold,
modifier = Modifier
.alpha(0f)
)
BasicText(
text = "${amount / 6}h ${(amount % 6) * 10}m",
style = typography.s.semiBold
)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(horizontal = 8.dp)
.height(72.dp)
.alpha(if (amount >= 60) 0.5f else 1f)
.clip(CircleShape)
.clickable(enabled = amount < 60) { amount++ }
.size(48.dp)
.background(colorPalette.background0)
) {
repeat(4) {
BasicText(
text = "${it * 15} m",
style = typography.xs.semiBold
)
}
BasicText(
text = "+",
style = typography.xs.semiBold
)
}
}
@ -540,9 +551,9 @@ fun MediaItemMenu(
text = "Set",
textStyle = typography.xs.semiBold.color(colorPalette.onAccent),
shape = RoundedCornerShape(36.dp),
isEnabled = hours > 0 || minutes > 0,
isEnabled = amount > 0,
onClick = {
binder?.startSleepTimer((hours * 60 + minutes * 15) * 60 * 1000L)
binder?.startSleepTimer(amount * 10 * 60 * 1000L)
isShowingSleepTimerDialog = false
}
)