Coverage Summary for Class: MetronomeScreenViewKt (com.vsevolodganin.clicktrack.ui.screen)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| MetronomeScreenViewKt |
0%
(0/12)
|
0%
(0/62)
|
0%
(0/96)
|
0%
(0/1352)
|
| MetronomeScreenViewKt$bottomSheetState$1$1 |
0%
(0/1)
|
25%
(1/4)
|
0%
(0/3)
|
0%
(0/38)
|
| MetronomeScreenViewKt$bottomSheetState$2$1 |
0%
(0/1)
|
0%
(0/3)
|
0%
(0/3)
|
18.2%
(4/22)
|
| MetronomeScreenViewKt$bottomSheetState$2$1$WhenMappings |
|
| MetronomeScreenViewKt$Content$1$4$1 |
0%
(0/2)
|
|
0%
(0/9)
|
0%
(0/85)
|
| MetronomeScreenViewKt$MetronomeScreenPreview$1$1 |
0%
(0/9)
|
|
0%
(0/17)
|
0%
(0/46)
|
| Total |
0%
(0/25)
|
1.4%
(1/69)
|
0%
(0/128)
|
0.3%
(4/1543)
|
package com.vsevolodganin.clicktrack.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import clicktrack.multiplatform.generated.resources.Res
import clicktrack.multiplatform.generated.resources.general_metronome_click_track_title
import clicktrack.multiplatform.generated.resources.metronome_bpm_meter_tap
import clicktrack.multiplatform.generated.resources.metronome_screen_title
import com.vsevolodganin.clicktrack.metronome.MetronomeState
import com.vsevolodganin.clicktrack.metronome.MetronomeViewModel
import com.vsevolodganin.clicktrack.metronome.metronomeClickTrack
import com.vsevolodganin.clicktrack.model.BeatsPerMinuteOffset
import com.vsevolodganin.clicktrack.model.NotePattern
import com.vsevolodganin.clicktrack.model.PlayProgress
import com.vsevolodganin.clicktrack.model.TimeSignature
import com.vsevolodganin.clicktrack.model.bpm
import com.vsevolodganin.clicktrack.ui.piece.BpmWheel
import com.vsevolodganin.clicktrack.ui.piece.ClickTrackView
import com.vsevolodganin.clicktrack.ui.piece.DarkTopAppBarWithBack
import com.vsevolodganin.clicktrack.ui.piece.PlayStopButton
import com.vsevolodganin.clicktrack.ui.piece.SubdivisionsChooser
import com.vsevolodganin.clicktrack.ui.piece.TimeSignatureView
import com.vsevolodganin.clicktrack.ui.theme.ClickTrackTheme
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.Companion.milliseconds
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MetronomeScreenView(viewModel: MetronomeViewModel, modifier: Modifier = Modifier) {
Scaffold(
topBar = {
DarkTopAppBarWithBack(
onBackClick = viewModel::onBackClick,
title = { Text(text = stringResource(Res.string.metronome_screen_title)) },
actions = {
IconButton(onClick = viewModel::onToggleOptions) {
Icon(imageVector = Icons.Default.Tune, contentDescription = null)
}
},
)
},
modifier = modifier,
) { paddingValues ->
Content(
viewModel = viewModel,
state = viewModel.state.collectAsState().value ?: return@Scaffold,
modifier = Modifier.padding(paddingValues),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Content(
viewModel: MetronomeViewModel,
state: MetronomeState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(bottom = 32.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.height(200.dp),
) {
val metronomeClickTrackName = stringResource(Res.string.general_metronome_click_track_title)
val metronomeClickTrack = remember(state.bpm, state.pattern, state.timeSignature) {
metronomeClickTrack(
name = metronomeClickTrackName,
bpm = state.bpm,
pattern = state.pattern,
timeSignature = state.timeSignature,
)
}
ClickTrackView(
clickTrack = metronomeClickTrack,
drawAllBeatsMarks = true,
drawTextMarks = false,
progress = state.progress,
defaultLineWidth = with(LocalDensity.current) { 1f.dp.toPx() },
)
}
Text(
text = state.bpm.value.toString(),
style = MaterialTheme.typography.displayLarge.copy(
fontWeight = FontWeight.Medium,
letterSpacing = 8.sp,
),
)
TimeSignatureView(
value = state.timeSignature,
onValueChange = viewModel::onTimeSignatureChange,
)
Layout(
content = {
Box(contentAlignment = Alignment.Center) {
BpmWheel(
value = state.bpm,
onValueChange = viewModel::onBpmChange,
modifier = Modifier.size(200.dp),
)
PlayStopButton(
isPlaying = state.isPlaying,
onToggle = viewModel::onTogglePlay,
)
}
FloatingActionButton(
onClick = viewModel::onBpmMeterClick,
modifier = Modifier.size(64.dp),
shape = CircleShape,
) {
Text(
text = stringResource(Res.string.metronome_bpm_meter_tap),
style = LocalTextStyle.current.copy(
fontWeight = FontWeight.Black,
letterSpacing = 4.sp,
),
)
}
},
modifier = Modifier.fillMaxWidth(),
) { measurables, constraints ->
val wheel = measurables[0].measure(Constraints())
val fab = measurables[1].measure(Constraints())
val width = constraints.maxWidth
val height = maxOf(wheel.height, fab.height)
layout(width, height) {
// Wheel is centered horizontally, nothing special
wheel.placeRelative(x = (width - wheel.width) / 2, y = 0)
// FAB is placed in the middle between wheel's and parent's right borders
fab.placeRelative(
x = (width * 3 + wheel.width - fab.width * 2) / 4,
y = (height - fab.height) / 2,
)
}
}
}
val bottomSheetState = bottomSheetState(viewModel)
// FIXME: Checking both states to give animation an opportunity to finish
if (state.areOptionsExpanded || bottomSheetState.isVisible) {
ModalBottomSheet(
onDismissRequest = { viewModel.onOptionsExpandedChange(false) },
sheetState = bottomSheetState,
) {
Options(viewModel)
}
}
}
@Composable
private fun Options(viewModel: MetronomeViewModel) {
val state = viewModel.state.collectAsState().value ?: return
SubdivisionsChooser(
pattern = state.pattern,
timeSignature = state.timeSignature,
onSubdivisionChoose = viewModel::onPatternChoose,
modifier = Modifier.padding(8.dp).navigationBarsPadding(),
alwaysExpanded = true,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun bottomSheetState(viewModel: MetronomeViewModel): SheetState {
val externalAreOptionsExpanded = viewModel.state.collectAsState().value?.areOptionsExpanded ?: false
val localBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// Sync the external state with the local state
LaunchedEffect(externalAreOptionsExpanded) {
when (externalAreOptionsExpanded) {
true -> localBottomSheetState.expand()
false -> localBottomSheetState.hide()
}
}
LaunchedEffect(localBottomSheetState.currentValue) {
when (localBottomSheetState.currentValue) {
SheetValue.PartiallyExpanded,
SheetValue.Hidden,
-> viewModel.onOptionsExpandedChange(false)
SheetValue.Expanded -> viewModel.onOptionsExpandedChange(true)
}
}
return localBottomSheetState
}
@Preview
@Composable
internal fun MetronomeScreenPreview(expanded: Boolean = false) = ClickTrackTheme {
MetronomeScreenView(
viewModel = object : MetronomeViewModel {
override val state: StateFlow<MetronomeState?> = MutableStateFlow(
MetronomeState(
bpm = 90.bpm,
pattern = NotePattern.QUINTUPLET_X2,
timeSignature = TimeSignature(4, 4),
progress = PlayProgress(100.milliseconds),
isPlaying = false,
areOptionsExpanded = expanded,
),
)
override fun onBackClick() = Unit
override fun onToggleOptions() = Unit
override fun onOptionsExpandedChange(isOpened: Boolean) = Unit
override fun onPatternChoose(pattern: NotePattern) = Unit
override fun onTimeSignatureChange(timeSignature: TimeSignature) = Unit
override fun onBpmChange(bpmDiff: BeatsPerMinuteOffset) = Unit
override fun onTogglePlay() = Unit
override fun onBpmMeterClick() = Unit
},
)
}
@Preview
@Composable
private fun ExpandedMetronomeScreenPreview() = MetronomeScreenPreview(expanded = true)