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)