fix(screensaver): fade only on a move, survive dock and theme changes
The in-app screen saver also handles uiMode, density, font scale and locale changes in place, so docking it or the dark-theme schedule no longer recreates it at night brightness. Only a move crossfades; a new next alarm or a settings change redraws in place, and the outgoing copy keeps its own minute. The AM/PM split skips quoted pattern text, and rememberLocalTimeFormatter reads the same currentLocale() as the screen saver.
This commit is contained in:
@@ -308,12 +308,13 @@
|
||||
|
||||
<!-- The same screen saver on demand, from the tabs' overflow menu: an
|
||||
app cannot start the system's dream itself. Not exported; it
|
||||
finishes itself once no longer shown. Rotating into a dock or a
|
||||
stand re-lays it out rather than recreating it, which would
|
||||
flash black and reset its brightness. -->
|
||||
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. -->
|
||||
<activity
|
||||
android:name=".ui.screensaver.ScreenSaverActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode|density|fontScale|locale|layoutDirection"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Clockula.ScreenSaver" />
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ScreenSaverDrift.Position, ScreenSaverUiState>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<ScreenSaverDrift.Position, ScreenSaverUiState>?): 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user