Skip to content

Commit e8dcb20

Browse files
committed
Fix duration display and progress animation pace
1 parent 5d14ced commit e8dcb20

File tree

4 files changed

+57
-35
lines changed

4 files changed

+57
-35
lines changed

app/src/main/java/net/opatry/countdowntimer/CounterViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.lifecycle.ViewModel
2828
import androidx.lifecycle.viewModelScope
2929
import kotlinx.coroutines.CoroutineDispatcher
3030
import kotlinx.coroutines.Dispatchers
31+
import kotlinx.coroutines.delay
3132
import kotlinx.coroutines.launch
3233
import kotlin.math.roundToLong
3334
import kotlin.time.Duration
@@ -68,6 +69,12 @@ class CounterViewModel(private val mainDispatcher: CoroutineDispatcher = Dispatc
6869

6970
private var countDownTimer: CountDownTimer? = null
7071

72+
private val tickIntervalMs
73+
get() = _tickInterval.value?.toLongMilliseconds() ?: 0L
74+
private val _tickInterval = MutableLiveData(1.seconds)
75+
val tickInterval: LiveData<Duration>
76+
get() = _tickInterval
77+
7178
fun stop() {
7279
viewModelScope.launch(mainDispatcher) {
7380
_state.value = null
@@ -89,7 +96,7 @@ class CounterViewModel(private val mainDispatcher: CoroutineDispatcher = Dispatc
8996
}.distinct()
9097

9198
countDownTimer?.cancel()
92-
countDownTimer = object : CountDownTimer(timer.duration.inMilliseconds.roundToLong(), 16L) {
99+
countDownTimer = object : CountDownTimer(timer.duration.inMilliseconds.roundToLong(), tickIntervalMs) {
93100
override fun onTick(millisUntilFinished: Long) {
94101
val state = _state.value ?: return
95102
_state.postValue(state.copy(remaining = millisUntilFinished.milliseconds))

app/src/main/java/net/opatry/countdowntimer/MainActivity.kt

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,15 @@ import androidx.compose.runtime.rememberCoroutineScope
4343
import androidx.compose.ui.Alignment
4444
import androidx.compose.ui.Modifier
4545
import androidx.lifecycle.viewmodel.compose.viewModel
46-
import kotlinx.coroutines.coroutineScope
4746
import kotlinx.coroutines.launch
4847
import net.opatry.countdowntimer.ui.component.TimerCircle
4948
import net.opatry.countdowntimer.ui.component.TimerControls
5049
import net.opatry.countdowntimer.ui.component.TimerLabel
5150
import net.opatry.countdowntimer.ui.component.TimerList
5251
import net.opatry.countdowntimer.ui.theme.MyTheme
53-
import kotlin.math.roundToInt
5452
import kotlin.time.Duration
5553
import kotlin.time.ExperimentalTime
54+
import kotlin.time.seconds
5655

5756
class MainActivity : AppCompatActivity() {
5857
@ExperimentalTime
@@ -90,6 +89,7 @@ fun CountDownTimerDispatcher() {
9089
val viewModel = viewModel<CounterViewModel>()
9190
val timers by viewModel.timers.observeAsState(listOf())
9291
val state by viewModel.state.observeAsState(null)
92+
val tickInterval by viewModel.tickInterval.observeAsState(0.seconds)
9393
val coroutineScope = rememberCoroutineScope()
9494

9595
val scaffoldState = rememberBackdropScaffoldState(if (state == null) BackdropValue.Revealed else BackdropValue.Concealed)
@@ -114,6 +114,7 @@ fun CountDownTimerDispatcher() {
114114
state?.let { uiState ->
115115
CountDownTimerLayout(
116116
uiState.remaining,
117+
tickInterval,
117118
onFABClicked = {
118119
if (uiState.remaining.isPositive()) {
119120
coroutineScope.launch {
@@ -139,31 +140,20 @@ fun CountDownTimerDispatcher() {
139140
@ExperimentalFoundationApi
140141
fun CountDownTimerLayout(
141142
remainingDuration: Duration,
143+
tickInterval: Duration,
142144
onFABClicked: () -> Unit
143145
) {
144-
val hours = (remainingDuration.inHours % 24).coerceAtLeast(.0)
145-
val minutes = (remainingDuration.inMinutes % 60).coerceAtLeast(.0)
146-
val seconds = (remainingDuration.inSeconds % 60).coerceAtLeast(.0)
147-
148-
Column(
149-
Modifier
150-
.fillMaxWidth()
151-
.fillMaxHeight(.8f),
152-
horizontalAlignment = Alignment.CenterHorizontally,
153-
verticalArrangement = Arrangement.SpaceEvenly
154-
) {
155-
// FIXME values rounding is incorrect
156-
// 1. when switching from 35min 30sec to 35min 29sec, it displays 34min 29sec
157-
// 2. when seconds is almost 0 but not yet, digit changes to 0 before the progress reach 12 O'clock
158-
// (especially visible for last second)
159-
val hoursI = hours.roundToInt()
160-
val minutesI = minutes.roundToInt()
161-
val secondsI = seconds.roundToInt()
162-
TimerLabel(hoursI, minutesI, secondsI)
163-
val hoursP = if (hoursI == 0) 0f else (hours / 24).toFloat()
164-
val minutesP = if (minutesI == 0) 0f else (minutes / 60).toFloat()
165-
val secondsP = if (secondsI == 0) 0f else (seconds / 60).toFloat()
166-
TimerCircle(hoursP, minutesP, secondsP, onFABClicked)
167-
TimerControls(onClose = { /* TODO */ }, onDelete = { /* TODO */ })
146+
remainingDuration.coerceAtLeast(0.seconds).toComponents { hours, minutes, seconds, _ ->
147+
Column(
148+
Modifier
149+
.fillMaxWidth()
150+
.fillMaxHeight(.8f),
151+
horizontalAlignment = Alignment.CenterHorizontally,
152+
verticalArrangement = Arrangement.SpaceEvenly
153+
) {
154+
TimerLabel(hours, minutes, seconds)
155+
TimerCircle(hours / 24f, minutes / 60f, seconds / 60f, tickInterval, onFABClicked)
156+
TimerControls(onClose = { /* TODO */ }, onDelete = { /* TODO */ })
157+
}
168158
}
169159
}

app/src/main/java/net/opatry/countdowntimer/ui/component/TimerCircle.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
*/
2222
package net.opatry.countdowntimer.ui.component
2323

24+
import androidx.compose.animation.core.LinearEasing
2425
import androidx.compose.animation.core.animateFloatAsState
26+
import androidx.compose.animation.core.tween
2527
import androidx.compose.foundation.Canvas
2628
import androidx.compose.foundation.layout.aspectRatio
2729
import androidx.compose.foundation.layout.fillMaxSize
@@ -50,12 +52,19 @@ import androidx.compose.ui.res.stringResource
5052
import androidx.compose.ui.unit.Dp
5153
import androidx.compose.ui.unit.dp
5254
import net.opatry.countdowntimer.R
55+
import kotlin.time.Duration
5356
import kotlin.time.DurationUnit
5457
import kotlin.time.ExperimentalTime
5558

5659
@Composable
5760
@ExperimentalTime
58-
fun TimerCircle(hoursProgress: Float, minutesProgress: Float, secondsProgress: Float, onFABClicked: () -> Unit) {
61+
fun TimerCircle(
62+
hoursProgress: Float,
63+
minutesProgress: Float,
64+
secondsProgress: Float,
65+
tickInterval: Duration,
66+
onFABClicked: () -> Unit
67+
) {
5968
// TODO animate card size when reaching 0 to make it wrapping the FAB
6069
Card(
6170
Modifier
@@ -72,7 +81,8 @@ fun TimerCircle(hoursProgress: Float, minutesProgress: Float, secondsProgress: F
7281
.fillMaxSize()
7382
.padding(12.dp),
7483
color = DurationUnit.SECONDS.color,
75-
strokeWidth = 8.dp
84+
strokeWidth = 8.dp,
85+
animationDuration = tickInterval
7686
)
7787
}
7888
if (minutesProgress > 0 || hoursProgress > 0) {
@@ -82,7 +92,8 @@ fun TimerCircle(hoursProgress: Float, minutesProgress: Float, secondsProgress: F
8292
.fillMaxSize()
8393
.padding(32.dp),
8494
color = DurationUnit.MINUTES.color,
85-
strokeWidth = 8.dp
95+
strokeWidth = 8.dp,
96+
animationDuration = tickInterval
8697
)
8798
}
8899
if (hoursProgress > 0) {
@@ -92,7 +103,8 @@ fun TimerCircle(hoursProgress: Float, minutesProgress: Float, secondsProgress: F
92103
.fillMaxSize()
93104
.padding(56.dp),
94105
color = DurationUnit.HOURS.color,
95-
strokeWidth = 8.dp
106+
strokeWidth = 8.dp,
107+
animationDuration = tickInterval
96108
)
97109
}
98110
FloatingActionButton(
@@ -118,11 +130,15 @@ fun CountDownTimerProgressIndicator(
118130
modifier: Modifier = Modifier,
119131
color: Color = MaterialTheme.colors.primary,
120132
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
121-
backgroundColor: Color = MaterialTheme.colors.onBackground.copy(alpha = .05f)
133+
animationDuration: Duration,
134+
backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = .05f)
122135
) {
123136
val animatedProgress = animateFloatAsState(
124137
targetValue = progress,
125-
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
138+
animationSpec = tween(
139+
durationMillis = animationDuration.toLongMilliseconds().toInt(),
140+
easing = LinearEasing
141+
)
126142
).value
127143
val stroke = with(LocalDensity.current) {
128144
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)

app/src/main/java/net/opatry/countdowntimer/ui/component/TimerList.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,27 @@ fun ActiveTimer(timerName: String) {
106106
@Composable
107107
@ExperimentalTime
108108
fun TimerListItem(timer: Timer, onTimerClicked: (Timer) -> Unit) {
109+
fun Int.pad0(length: Int = 2) = toString().padStart(length, '0')
110+
109111
Text(
110112
buildAnnotatedString {
111113
// FIXME shouldn't come from string resource for proper localization?
112114
if (timer.name != null) {
113115
append(timer.name)
114116
} else {
115-
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
117+
withStyle(
118+
style = SpanStyle(
119+
fontStyle = FontStyle.Italic,
120+
color = MaterialTheme.colors.onBackground.copy(alpha = .5f)
121+
)
122+
) {
116123
append(stringResource(R.string.timer_unnamed))
117124
}
118125
}
119126
withStyle(style = SpanStyle(fontSize = 10.sp)) {
120-
append(" (${timer.duration})")
127+
timer.duration.toComponents { hours, minutes, seconds, _ ->
128+
append(" (${hours.pad0()}:${minutes.pad0()}:${seconds.pad0()})")
129+
}
121130
}
122131
},
123132
Modifier

0 commit comments

Comments
 (0)