fix(worldclock): let the seconds time grow on wide windows

The row's seconds time was capped at 168dp everywhere, so on a tablet a
larger font scale was shrunk back for no reason. The cap now grows to
45% of the window, with 168dp as the floor, and the fitted size is
recomputed when the text measurer changes. A row the column has not
re-seeded yet is skipped rather than crashing, and the source's KDoc
is re-wrapped.
This commit is contained in:
makiolaj committed 2026-10-22 11:11:20 +02:00
1 parent d70d0c78da
commit 0218339161
3 files changed
+27 -16

No files matched your search

@@ -18,6 +18,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@@ -177,8 +178,14 @@ internal data class RowTimeStyle(val style: TextStyle, val maxWidth: Dp) {
private const val FitMargin = 0.95f
/** Wide enough for "12:59:59 PM" at the default scale; the city keeps the rest. */
private val SecondsMaxWidth = 168.dp
/**
* The least the time may take: wide enough for "12:59:59 PM" at the default
* scale. On a wider window the cap grows with it, to [SecondsMaxShare] of the
* window, so a larger font scale is honoured wherever there is room for it.
*/
private val SecondsMinCap = 168.dp
private const val SecondsMaxShare = 0.45f
/**
* Wide times in every format: a two-digit hour, eights, and each day period,
@@ -189,7 +196,7 @@ private val WidestSamples: List<LocalTime> = listOf(LocalTime.of(10, 58, 58), Lo
/**
* Without seconds, the row's time exactly as it always was: `headlineMedium`,
* bold, uncapped. With seconds, a size down, and shrunk further only if the
* widest sample would not fit [SecondsMaxWidth], so at a large font scale the
* widest sample would not fit the cap, so at a large font scale the
* time cannot take the whole row and leave the city no width at all.
* Measured once per format, style and density, never per tick: the digits are
* tabular, so no time is wider than the samples.
@@ -199,13 +206,15 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime)
val base = if (showSeconds) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.headlineMedium
val measurer = rememberTextMeasurer(cacheSize = 0)
val density = LocalDensity.current
return remember(showSeconds, base, formatTime, density) {
val windowWidth = LocalConfiguration.current.screenWidthDp.dp
val maxWidth = maxOf(SecondsMinCap, windowWidth * SecondsMaxShare)
return remember(showSeconds, base, formatTime, density, measurer, maxWidth) {
val style = base.copy(fontWeight = FontWeight.Bold)
if (!showSeconds) return@remember RowTimeStyle(style, Dp.Infinity)
val width = WidestSamples.maxOf { sample ->
measurer.measure(formatTime(sample), style, maxLines = 1, softWrap = false).size.width
}
val cap = with(density) { SecondsMaxWidth.toPx() }
val cap = with(density) { maxWidth.toPx() }
// Through px and back, not by scaling the sp value: from Android 14
// large font scales are non-linear, so sp and px are not proportional.
val fitted = if (width <= cap) {
@@ -214,7 +223,7 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime)
// A few percent spare: hinted glyph advances do not scale exactly.
style.copy(fontSize = with(density) { (style.fontSize.toPx() * FitMargin * cap / width).toSp() })
}
RowTimeStyle(fitted, SecondsMaxWidth)
RowTimeStyle(fitted, maxWidth)
}
}
@@ -147,7 +147,9 @@ fun WorldClockScreen(
modifier = Modifier.fillMaxWidth(),
rowHeight = 80.dp,
) { id, position, dragHandle, isDragging ->
val row = rowsById.getValue(id)
// Always present while the column re-seeds in composition;
// drawn as nothing rather than crashing if that ever lags.
val row = rowsById[id] ?: return@ReorderableColumn
WorldClockRow(
row = row,
position = position,
@@ -46,17 +46,17 @@ internal class WorldClockSource @Inject constructor(
* **every tick**. [ClockChanges] restarts the tick, so `TIME_SET` and
* `TIMEZONE_CHANGED` show at once rather than at the next minute, and the
* restarted ticker measures its next wait against the clock as it now
* reads. It sits
* outside the `flatMapLatest`, so its receiver is registered once per
* subscription rather than once per list edit, and `onStart` lets the
* first state through without waiting for a broadcast. [refreshes] does
* the same for what no broadcast announces: the screen sends one on every
* resume, which covers a per-app language change (the activity is
* recreated, the ViewModel and this flow are not) and a `delay` stretched
* by a deep sleep (the main looper's clock stops while the device sleeps)
* without waiting for the next minute.
* reads. It sits outside the `flatMapLatest`, so its receiver is
* registered once per subscription rather than once per list edit, and
* `onStart` lets the first state through without waiting for a broadcast.
* `SystemEventReceiver` still needs no new branch (D2).
*
* [refreshes] does the same for what no broadcast announces: the screen
* sends one on every resume, which covers a per-app language change (the
* activity is recreated, the ViewModel and this flow are not) and a
* `delay` stretched by a deep sleep (the main looper's clock stops while
* the device sleeps) without waiting for the next minute.
*
* The tick is aligned to the wall clock and only as fine as the readouts:
* once a minute, or once a second when the user turned seconds on (#23).
* Everything shown is truncated to that same unit, so each tick is one new