Coverage Summary for Class: MetronomeScreenViewKt (com.vsevolodganin.clicktrack.ui.screen)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
MetronomeScreenViewKt |
0%
(0/10)
|
0%
(0/54)
|
0%
(0/99)
|
0.3%
(4/1245)
|
MetronomeScreenViewKt$backdropState$2$1$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/3)
|
10.8%
(4/37)
|
MetronomeScreenViewKt$backdropState$2$1$1$WhenMappings |
|
MetronomeScreenViewKt$Content$1$3$1 |
0%
(0/3)
|
|
0%
(0/10)
|
0%
(0/105)
|
MetronomeScreenViewKt$WhenMappings |
|
Total |
0%
(0/14)
|
0%
(0/56)
|
0%
(0/112)
|
0.6%
(8/1387)
|
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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
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.material.BackdropScaffold
import androidx.compose.material.BackdropScaffoldDefaults
import androidx.compose.material.BackdropScaffoldState
import androidx.compose.material.BackdropValue
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.rememberBackdropScaffoldState
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 com.vsevolodganin.clicktrack.generated.resources.MR
import com.vsevolodganin.clicktrack.metronome.MetronomeState
import com.vsevolodganin.clicktrack.metronome.MetronomeTimeSignature
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.bpm
import com.vsevolodganin.clicktrack.ui.piece.BpmWheel
import com.vsevolodganin.clicktrack.ui.piece.ClickTrackView
import com.vsevolodganin.clicktrack.ui.piece.FloatingActionButton
import com.vsevolodganin.clicktrack.ui.piece.PlayStopButton
import com.vsevolodganin.clicktrack.ui.piece.SubdivisionsChooser
import com.vsevolodganin.clicktrack.ui.piece.TopAppBar
import com.vsevolodganin.clicktrack.ui.piece.darkAppBar
import com.vsevolodganin.clicktrack.ui.piece.onDarkAppBarSurface
import com.vsevolodganin.clicktrack.ui.theme.ClickTrackTheme
import com.vsevolodganin.clicktrack.utils.compose.navigationBarsPadding
import com.vsevolodganin.clicktrack.utils.compose.statusBars
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun MetronomeScreenView(viewModel: MetronomeViewModel, modifier: Modifier = Modifier) {
viewModel.state.collectAsState().value ?: return // FIXME: Otherwise crash in swipeable state
BackdropScaffold(
appBar = { AppBar(viewModel) },
backLayerContent = { Options(viewModel) },
frontLayerContent = { Content(viewModel) },
modifier = modifier,
scaffoldState = backdropState(viewModel),
peekHeight = BackdropScaffoldDefaults.PeekHeight + WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
backLayerBackgroundColor = MaterialTheme.colors.darkAppBar,
backLayerContentColor = MaterialTheme.colors.onDarkAppBarSurface,
)
}
@Composable
private fun AppBar(viewModel: MetronomeViewModel) {
TopAppBar(
title = { Text(text = stringResource(MR.strings.metronome_screen_title)) },
navigationIcon = {
IconButton(onClick = viewModel::onBackClick) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
actions = {
IconButton(onClick = viewModel::onToggleOptions) {
Icon(imageVector = Icons.Default.Tune, contentDescription = null)
}
},
)
}
@Composable
private fun Content(viewModel: MetronomeViewModel) {
val state = viewModel.state.collectAsState().value ?: return
Column(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.padding(bottom = 32.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.height(200.dp),
elevation = 8.dp,
) {
val metronomeClickTrackName = stringResource(MR.strings.general_metronome_click_track_title)
val metronomeClickTrack = remember(state.bpm, state.pattern) {
metronomeClickTrack(
name = metronomeClickTrackName,
bpm = state.bpm,
pattern = state.pattern,
)
}
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.h1.copy(
fontWeight = FontWeight.Medium,
letterSpacing = 8.sp,
),
)
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,
enableInsets = false,
)
}
FloatingActionButton(
onClick = viewModel::onBpmMeterClick,
modifier = Modifier.size(64.dp),
enableInsets = false,
) {
Text(
text = stringResource(MR.strings.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,
)
}
}
}
}
@Composable
private fun Options(viewModel: MetronomeViewModel) {
val pattern = viewModel.state.collectAsState().value?.pattern ?: return
SubdivisionsChooser(
pattern = pattern,
timeSignature = MetronomeTimeSignature,
onSubdivisionChoose = viewModel::onPatternChoose,
modifier = Modifier.padding(8.dp),
alwaysExpanded = true,
)
}
@Composable
private fun backdropState(viewModel: MetronomeViewModel): BackdropScaffoldState {
val areOptionsExpanded = viewModel.state.collectAsState().value?.areOptionsExpanded ?: false
val backdropValue = if (areOptionsExpanded) BackdropValue.Revealed else BackdropValue.Concealed
return rememberBackdropScaffoldState(
initialValue = backdropValue,
confirmStateChange = remember {
{ newDrawerValue ->
when (newDrawerValue) {
BackdropValue.Concealed -> viewModel.onOptionsExpandedChange(false)
BackdropValue.Revealed -> viewModel.onOptionsExpandedChange(true)
}
true
}
},
).apply {
LaunchedEffect(backdropValue) {
when (backdropValue) {
BackdropValue.Concealed -> conceal()
BackdropValue.Revealed -> reveal()
}
}
}
}
@ScreenPreview
@Composable
private fun Preview() = ClickTrackTheme {
MetronomeScreenView(
viewModel = object : MetronomeViewModel {
override val state: StateFlow<MetronomeState?> = MutableStateFlow(
MetronomeState(
bpm = 90.bpm,
pattern = NotePattern.QUINTUPLET_X2,
progress = PlayProgress(100.milliseconds),
isPlaying = false,
areOptionsExpanded = false,
),
)
override fun onBackClick() = Unit
override fun onToggleOptions() = Unit
override fun onOptionsExpandedChange(isOpened: Boolean) = Unit
override fun onPatternChoose(pattern: NotePattern) = Unit
override fun onBpmChange(bpmDiff: BeatsPerMinuteOffset) = Unit
override fun onTogglePlay() = Unit
override fun onBpmMeterClick() = Unit
},
)
}