diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 846869d..3872211 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -308,12 +308,13 @@
+ finishes itself once no longer shown. Rotating it into a stand,
+ docking it, or the dark-theme schedule firing re-lays it out
+ rather than recreating it, which would flash black and reset its
+ brightness. -->
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
index dab3129..5e9ae40 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
@@ -6,6 +6,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import de.jeanlucmakiola.clockula.domain.TimeOfDay
+import de.jeanlucmakiola.floret.locale.currentLocale
import java.time.LocalTime
import java.time.chrono.IsoChronology
import java.time.format.DateTimeFormatter
@@ -26,8 +27,8 @@ internal fun rememberLocalTimeFormatter(): (LocalTime) -> String {
// The locale can change under a running activity, and the 12/24-hour
// choice rides the same configuration, so both are re-read with it.
val configuration = LocalConfiguration.current
- val formatter = remember(configuration) {
- val locale = Locale.getDefault(Locale.Category.FORMAT)
+ val locale = currentLocale()
+ val formatter = remember(configuration, locale) {
DateTimeFormatter.ofPattern(localTimePattern(DateFormat.is24HourFormat(context), locale), locale)
}
return remember(formatter) {
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/screensaver/ScreenSaverContent.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/screensaver/ScreenSaverContent.kt
index 0e47b6e..894a739 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/screensaver/ScreenSaverContent.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/screensaver/ScreenSaverContent.kt
@@ -61,7 +61,8 @@ import kotlin.math.roundToInt
* moves once a minute ([ScreenSaverDrift]) so nothing stays lit in one place.
* The move is a slow crossfade rather than a slide, as DeskClock's is, and
* a cut under reduce motion: the old minute fades out where it was as the
- * new one fades in at its new spot.
+ * new one fades in at its new spot. Only a move fades: a new next alarm or
+ * a settings change redraws in place.
*/
@Composable
internal fun ScreenSaverContent(state: ScreenSaverUiState, modifier: Modifier = Modifier) {
@@ -76,19 +77,31 @@ internal fun ScreenSaverContent(state: ScreenSaverUiState, modifier: Modifier =
// Sized from the screen's shorter side, so the clock is as large in
// landscape on a bedside dock as it is upright in the hand.
val side = minOf(maxWidth, maxHeight)
+ // The last frame drawn at each spot, so the outgoing copy keeps its
+ // own minute while it fades. Two are all a crossfade ever shows.
+ val frames = remember { RecentFrames() }
+ frames[state.position] = state
Crossfade(
- targetState = state,
+ targetState = state.position,
animationSpec = if (reduceMotion) snap() else tween(ScreenSaverDefaults.MOVE_MILLIS),
label = "drift",
- ) { shown ->
+ ) { position ->
+ val current = position == state.position
+ val shown = if (current) state else frames[position] ?: state
// The outgoing minute is only a picture while it fades: a screen
// reader finds one time on screen, never two.
- val semantics = if (shown == state) Modifier else Modifier.clearAndSetSemantics {}
- Drifting(shown.position, semantics) { Clock(shown, side) }
+ val semantics = if (current) Modifier else Modifier.clearAndSetSemantics {}
+ Drifting(position, semantics) { Clock(shown, side) }
}
}
}
+/** The newest two frames by position; older ones have finished fading. */
+private class RecentFrames : LinkedHashMap() {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean =
+ size > 2
+}
+
/** Places its one child at [position]'s fractions of the space the child leaves free. */
@Composable
private fun Drifting(position: ScreenSaverDrift.Position, modifier: Modifier, content: @Composable () -> Unit) {
@@ -221,7 +234,7 @@ private data class TimeParts(val before: String, val marker: String, val after:
private fun timeParts(time: LocalTime, use24Hour: Boolean, locale: Locale): TimeParts {
val pattern = localTimePattern(use24Hour, locale)
- val markerAt = pattern.indexOf('a')
+ val markerAt = markerIndex(pattern)
if (markerAt < 0) return TimeParts(format(time, pattern, locale), "", "")
return runCatching {
TimeParts(
@@ -232,6 +245,18 @@ private fun timeParts(time: LocalTime, use24Hour: Boolean, locale: Locale): Time
}.getOrElse { TimeParts(format(time, pattern, locale), "", "") }
}
+/** Where the pattern's AM/PM field is, skipping quoted literal text; -1 when it has none. */
+private fun markerIndex(pattern: String): Int {
+ var quoted = false
+ pattern.forEachIndexed { index, char ->
+ when {
+ char == '\'' -> quoted = !quoted
+ !quoted && char == 'a' -> return index
+ }
+ }
+ return -1
+}
+
private fun format(time: LocalTime, pattern: String, locale: Locale): String =
if (pattern.isBlank()) pattern else DateTimeFormatter.ofPattern(pattern, locale).format(time)