Skip to content

Commit 6a3e012

Browse files
committed
Update hands angles calculation
1 parent a3c7d8d commit 6a3e012

3 files changed

Lines changed: 78 additions & 83 deletions

File tree

composeApp/src/commonMain/kotlin/com/danielebonaldo/dashboard/App.kt

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
package com.danielebonaldo.dashboard
22

3-
import androidx.compose.animation.AnimatedVisibility
4-
import androidx.compose.animation.slideIn
5-
import androidx.compose.animation.slideOut
6-
import androidx.compose.foundation.BorderStroke
73
import androidx.compose.foundation.background
84
import androidx.compose.foundation.layout.*
9-
import androidx.compose.material3.Button
105
import androidx.compose.material3.MaterialTheme
11-
import androidx.compose.material3.Text
126
import androidx.compose.runtime.*
137
import androidx.compose.ui.Alignment
148
import androidx.compose.ui.Modifier
159
import androidx.compose.ui.graphics.Color
1610
import androidx.compose.ui.layout.onGloballyPositioned
1711
import androidx.compose.ui.tooling.preview.Preview
18-
import androidx.compose.ui.unit.IntOffset
19-
import androidx.compose.ui.unit.dp
2012
import androidx.lifecycle.viewmodel.compose.viewModel
2113
import com.danielebonaldo.dashboard.clockcompassdial.DialMode
2214
import com.danielebonaldo.dashboard.clockcompassdial.MorphingDial
2315
import com.danielebonaldo.dashboard.clockcompassdial.MultiDialViewModel
24-
import com.danielebonaldo.dashboard.clockcompassdial.UiState
2516

2617
@Composable
2718
@Preview
@@ -53,19 +44,25 @@ fun App() {
5344
) {
5445
ControlsColumn(
5546
uiState = uiState,
56-
onModeSelected = { selectedMode = it },
47+
onModeSelected = {
48+
viewModel.updateClock()
49+
selectedMode = it
50+
},
5751
onStartStop = { viewModel.startStopWatch() },
5852
onReset = { viewModel.resetStopWatch() }
5953
)
60-
Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center){
54+
Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
6155
MorphingDial(uiState)
6256
}
6357
}
6458
} else {
6559
Column(Modifier.fillMaxSize()) {
6660
ControlsRow(
6761
uiState = uiState,
68-
onModeSelected = { selectedMode = it },
62+
onModeSelected = {
63+
viewModel.updateClock()
64+
selectedMode = it
65+
},
6966
onStartStop = { viewModel.startStopWatch() },
7067
onReset = { viewModel.resetStopWatch() }
7168
)

composeApp/src/commonMain/kotlin/com/danielebonaldo/dashboard/clockcompassdial/ClockCompassDial.kt

Lines changed: 63 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -115,77 +115,78 @@ fun MorphingDial(uiState: UiState) {
115115
}
116116
}
117117

118-
val isRunning = (uiState !is UiState.Stopwatch) || (uiState is UiState.Stopwatch.Running)
119-
var elapsedTimeMs by remember { mutableLongStateOf(0L) }
118+
// Separate time sources so each freezes when its mode is inactive.
119+
// This preserves the correct "from" position when a mode transition starts.
120+
var clockElapsedMs by remember {
121+
mutableLongStateOf(if (uiState is UiState.Clock) uiState.timeMillis else 0L)
122+
}
123+
var stopwatchElapsedMs by remember {
124+
mutableLongStateOf(
125+
when (uiState) {
126+
is UiState.Stopwatch.Running -> uiState.elapsedMillis
127+
is UiState.Stopwatch.Paused -> uiState.elapsedMillis
128+
else -> 0L
129+
}
130+
)
131+
}
120132

121133
LaunchedEffect(uiState) {
122-
elapsedTimeMs = when (uiState) {
123-
is UiState.Clock -> uiState.timeMillis
124-
is UiState.Compass -> 0L
125-
is UiState.Stopwatch.Running -> uiState.elapsedMillis
126-
is UiState.Stopwatch.Paused -> uiState.elapsedMillis
127-
is UiState.Stopwatch.Zero -> 0L
134+
when (uiState) {
135+
is UiState.Clock -> clockElapsedMs = uiState.timeMillis
136+
is UiState.Stopwatch.Running -> stopwatchElapsedMs = uiState.elapsedMillis
137+
is UiState.Stopwatch.Paused -> stopwatchElapsedMs = uiState.elapsedMillis
138+
is UiState.Stopwatch.Zero -> stopwatchElapsedMs = 0L
139+
is UiState.Compass -> {}
128140
}
129141
}
130142

131-
val millisInCurrentMinute = elapsedTimeMs % 60_000
132-
val millisInCurrentHour = elapsedTimeMs % 3600_000
133-
val millisIn12Hour = elapsedTimeMs % 43200_000
134-
135-
LaunchedEffect(isRunning) {
136-
if (isRunning) {
137-
var lastFrameTime = withFrameMillis { it }
138-
139-
while (isActive) {
140-
withFrameMillis { currentFrameTime ->
141-
// Calculate the delta (how much time passed since the last frame)
142-
val delta = currentFrameTime - lastFrameTime
143-
144-
// Add it to our total elapsed time
145-
elapsedTimeMs += delta
146-
147-
// Update lastFrameTime for the next loop iteration
148-
lastFrameTime = currentFrameTime
143+
val currentUiState by rememberUpdatedState(uiState)
144+
LaunchedEffect(Unit) {
145+
var lastFrameTime = withFrameMillis { it }
146+
while (isActive) {
147+
withFrameMillis { currentFrameTime ->
148+
val delta = currentFrameTime - lastFrameTime
149+
when (currentUiState) {
150+
is UiState.Clock, is UiState.Compass -> clockElapsedMs += delta
151+
is UiState.Stopwatch.Running -> stopwatchElapsedMs += delta
152+
else -> {}
149153
}
154+
lastFrameTime = currentFrameTime
150155
}
151156
}
152157
}
153-
val secondsHandAngle by transition.animateFloat(label = "SecondsHandAngle") { state ->
154-
when (state) {
155-
is UiState.Clock -> (millisInCurrentMinute / 60_000f) * 360f
156-
is UiState.Compass -> 0f
157-
is UiState.Stopwatch -> (millisInCurrentMinute / 60_000f) * 360f
158-
}
159-
}
160-
val minutesHandAngle by transition.animateFloat(label = "MinutesHandAngle") { state ->
161-
when (state) {
162-
is UiState.Clock -> (millisInCurrentHour / 3600_000f) * 360f
163-
is UiState.Compass -> 0f
164-
is UiState.Stopwatch -> secondsHandAngle
165-
}
166-
}
167-
val hoursHandAngle by transition.animateFloat(label = "HoursHandAngle") { state ->
168-
when (state) {
169-
is UiState.Clock -> (millisIn12Hour / 43200_000f) * 360f
170-
is UiState.Compass -> 0f
171-
is UiState.Stopwatch -> secondsHandAngle
172-
}
173-
}
174158

175-
val stopwatchMinutesHandAngle by transition.animateFloat(label = "StopwatchMinutesHandAngle") { state ->
176-
when (state) {
177-
is UiState.Clock -> 0f
178-
is UiState.Compass -> 0f
179-
is UiState.Stopwatch -> (millisInCurrentHour / 3600_000f) * 360f
180-
}
181-
}
182-
val stopwatchHoursHandAngle by transition.animateFloat(label = "StopwatchHoursHandAngle") { state ->
183-
when (state) {
184-
is UiState.Clock -> 0f
185-
is UiState.Compass -> 0f
186-
is UiState.Stopwatch -> (millisIn12Hour / 43200_000f) * 360f
187-
}
188-
}
159+
// Animated lerp factors for mode transitions (fixed targets, no time-based values)
160+
val nonCompassLerp by transition.animateFloat(
161+
transitionSpec = { tween(TransitionDurationMs) },
162+
label = "NonCompassLerp"
163+
) { state -> if (state is UiState.Compass) 0f else 1f }
164+
165+
val stopwatchLerp by transition.animateFloat(
166+
transitionSpec = { tween(TransitionDurationMs) },
167+
label = "StopwatchLerp"
168+
) { state -> if (state is UiState.Stopwatch) 1f else 0f }
169+
170+
// Clock angles (freeze when not in Clock/Compass mode, preserving position for transitions)
171+
val clockSecondsAngle = (clockElapsedMs % 60_000L / 60_000f) * 360f
172+
val clockMinutesAngle = (clockElapsedMs % 3_600_000L / 3_600_000f) * 360f
173+
val clockHoursAngle = (clockElapsedMs % 43_200_000L / 43_200_000f) * 360f
174+
175+
// Stopwatch angles (freeze when not in Stopwatch mode, preserving position for transitions)
176+
val swSecondsAngle = (stopwatchElapsedMs % 60_000L / 60_000f) * 360f
177+
val swMinutesAngle = (stopwatchElapsedMs % 3_600_000L / 3_600_000f) * 360f
178+
val swHoursAngle = (stopwatchElapsedMs % 43_200_000L / 43_200_000f) * 360f
179+
180+
// When Compass↔Stopwatch, both lerps animate simultaneously at the same rate, so their
181+
// ratio is always 1 — meaning we only use swSecondsAngle, never clockSecondsAngle.
182+
// When Compass↔Clock, stopwatchLerp stays at 0 so ratio is 0 — only clockSecondsAngle.
183+
// This prevents clock angles from bleeding into Compass↔Stopwatch transitions.
184+
val swRatio = if (nonCompassLerp > 0f) (stopwatchLerp / nonCompassLerp).coerceIn(0f, 1f) else 0f
185+
val secondsHandAngle = (clockSecondsAngle + (swSecondsAngle - clockSecondsAngle) * swRatio) * nonCompassLerp
186+
val minutesHandAngle = (clockMinutesAngle + (swSecondsAngle - clockMinutesAngle) * swRatio) * nonCompassLerp
187+
val hoursHandAngle = (clockHoursAngle + (swSecondsAngle - clockHoursAngle) * swRatio) * nonCompassLerp
188+
val stopwatchMinutesHandAngle = swMinutesAngle * stopwatchLerp
189+
val stopwatchHoursHandAngle = swHoursAngle * stopwatchLerp
189190

190191
val compassDegrees by transition.animateFloat(label = "CompassDegrees") { state ->
191192
when (state) {
@@ -435,16 +436,7 @@ private fun DrawScope.drawStopwatchTicks(
435436
}
436437
}
437438

438-
//Why this is great for your blog post:
439-
//If you are writing a tutorial, this is a perfect "Aha!" moment to share with your readers.
440-
//
441-
//In the previous drawClockNumbers example, we had to use sin and cos to plot the numbers because standard clock numbers
442-
// remain strictly upright.
443-
// But because the compass text rotates to face the center pin, you get to skip the math completely and leverage
444-
// Compose's rotate(degrees = angle, pivot = center) block. It's highly performant and keeps your drawing logic extremely clean.
445-
//
446-
//(Note: I set the text color to Color.Black in this snippet to match the light face of the compass in your image,
447-
// but you can bind that to your animated state transition just like the alpha!)
439+
448440
private fun DrawScope.drawCompassLabels(
449441
textMeasurer: TextMeasurer,
450442
center: Offset,

composeApp/src/commonMain/kotlin/com/danielebonaldo/dashboard/clockcompassdial/MultiDialViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ class MultiDialViewModel : ViewModel() {
4141
return now.toEpochMilliseconds()
4242
}
4343

44+
fun updateClock() {
45+
viewModelScope.launch {
46+
_clockState.emit(UiState.Clock(nowMillis()))
47+
}
48+
}
49+
4450
fun startStopWatch() {
4551
viewModelScope.launch {
4652
_stopwatchState.emit(

0 commit comments

Comments
 (0)