Coverage Summary for Class: PolyrhythmsScreenViewKt (com.vsevolodganin.clicktrack.ui.screen)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| PolyrhythmsScreenViewKt |
0%
(0/11)
|
0%
(0/58)
|
0%
(0/80)
|
0%
(0/1195)
|
| PolyrhythmsScreenViewKt$progressAngle$1$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/10)
|
0%
(0/83)
|
| Total |
0%
(0/12)
|
0%
(0/60)
|
0%
(0/90)
|
0%
(0/1278)
|
package com.vsevolodganin.clicktrack.ui.screen
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import clicktrack.multiplatform.generated.resources.Res
import clicktrack.multiplatform.generated.resources.polyrhythms_screen_title
import com.vsevolodganin.clicktrack.model.PlayProgress
import com.vsevolodganin.clicktrack.model.TwoLayerPolyrhythm
import com.vsevolodganin.clicktrack.model.bpm
import com.vsevolodganin.clicktrack.polyrhythm.PolyrhythmsState
import com.vsevolodganin.clicktrack.polyrhythm.PolyrhythmsViewModel
import com.vsevolodganin.clicktrack.ui.piece.DarkTopAppBarWithBack
import com.vsevolodganin.clicktrack.ui.piece.PlayStopButton
import com.vsevolodganin.clicktrack.ui.piece.PolyrhythmCircle
import com.vsevolodganin.clicktrack.ui.theme.ClickTrackTheme
import com.vsevolodganin.clicktrack.utils.compose.AnimatableFloat
import com.vsevolodganin.clicktrack.utils.compose.FULL_ANGLE_DEGREES
import com.vsevolodganin.clicktrack.utils.compose.widthByText
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
@Composable
fun PolyrhythmsScreenView(viewModel: PolyrhythmsViewModel, modifier: Modifier = Modifier) {
val state by viewModel.state.collectAsState()
Scaffold(
topBar = {
DarkTopAppBarWithBack(
onBackClick = viewModel::onBackClick,
title = { Text(stringResource(Res.string.polyrhythms_screen_title)) },
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
PlayStopButton(
isPlaying = state?.isPlaying ?: return@Scaffold,
onToggle = viewModel::onTogglePlay,
)
},
modifier = modifier,
) { paddingValues ->
Content(
viewModel = viewModel,
state = state ?: return@Scaffold,
modifier = Modifier.padding(paddingValues),
)
}
}
@Composable
private fun Content(
viewModel: PolyrhythmsViewModel,
state: PolyrhythmsState,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.padding(16.dp)) {
Row {
NumberChooser(
value = state.twoLayerPolyrhythm.layer1,
onValueChoose = viewModel::onLayer1Change,
)
Spacer(modifier = Modifier.weight(1f))
NumberChooser(
value = state.twoLayerPolyrhythm.layer2,
onValueChoose = viewModel::onLayer2Change,
)
}
PolyrhythmCircleWrapper(
layer1 = state.twoLayerPolyrhythm.layer1,
layer2 = state.twoLayerPolyrhythm.layer2,
progress = state.playableProgress,
totalDuration = state.twoLayerPolyrhythm.durationInTime,
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
)
}
}
@Composable
private fun NumberChooser(value: Int, onValueChoose: (Int) -> Unit, modifier: Modifier = Modifier) {
Row(modifier = modifier) {
IconButton(
onClick = { onValueChoose(value - 1) },
modifier = Modifier.align(CenterVertically),
) {
Icon(Icons.Default.ChevronLeft, contentDescription = null)
}
val textStyle = MaterialTheme.typography.headlineSmall
Text(
text = value.toString(),
modifier = Modifier
.align(CenterVertically)
.widthByText("99", textStyle),
style = textStyle,
textAlign = TextAlign.Center,
)
IconButton(
onClick = { onValueChoose(value + 1) },
modifier = Modifier.align(CenterVertically),
) {
Icon(Icons.Default.ChevronRight, contentDescription = null)
}
}
}
@Composable
private fun PolyrhythmCircleWrapper(layer1: Int, layer2: Int, progress: PlayProgress?, totalDuration: Duration, modifier: Modifier) {
val progressAngle = progressAngle(progress, totalDuration)?.asState()
PolyrhythmCircle(
outerDotNumber = layer1,
innerDotNumber = layer2,
modifier = modifier,
progressAngle = progressAngle?.value,
progressVelocity = (FULL_ANGLE_DEGREES / totalDuration.toDouble(DurationUnit.SECONDS)).toFloat(),
)
}
@Composable
private fun progressAngle(progress: PlayProgress?, totalDuration: Duration): AnimatableFloat? {
progress ?: return null
val animatableProgressAngle = remember {
Animatable(0f).apply {
updateBounds(0f, FULL_ANGLE_DEGREES)
}
}
LaunchedEffect(progress) {
val progressTimePosition = progress.realPosition
val progressAnglePosition = progressTimePosition.toAngle(totalDuration)
val animationDuration = totalDuration - progressTimePosition
animatableProgressAngle.snapTo(progressAnglePosition)
if (!progress.isPaused) {
animatableProgressAngle.animateTo(
targetValue = FULL_ANGLE_DEGREES,
animationSpec = tween(
durationMillis = animationDuration.coerceAtLeast(Duration.ZERO).inWholeMilliseconds.toInt(),
easing = LinearEasing,
),
)
}
}
return animatableProgressAngle
}
private fun Duration.toAngle(totalDuration: Duration): Float {
return if (totalDuration == Duration.ZERO) {
0f
} else {
(this / totalDuration * FULL_ANGLE_DEGREES).toFloat()
}
}
@Preview
@Composable
internal fun PolyrhythmsScreenPreview() = ClickTrackTheme {
PolyrhythmsScreenView(
viewModel = object : PolyrhythmsViewModel {
override val state: StateFlow<PolyrhythmsState?> = MutableStateFlow(
PolyrhythmsState(
twoLayerPolyrhythm = TwoLayerPolyrhythm(
bpm = 120.bpm,
layer1 = 3,
layer2 = 2,
),
isPlaying = true,
playableProgress = PlayProgress(100.milliseconds),
),
)
override fun onBackClick() = Unit
override fun onTogglePlay() = Unit
override fun onLayer1Change(value: Int) = Unit
override fun onLayer2Change(value: Int) = Unit
},
)
}