@@ -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+
448440private fun DrawScope.drawCompassLabels (
449441 textMeasurer : TextMeasurer ,
450442 center : Offset ,
0 commit comments