feat(worldclock): show home time as text under the analog face
The World clock tab now prints the home time under the hero face in the app's 12/24-hour format, reversing the earlier call to leave it out. Reading the exact minute off a dial is slow, and that minute is what most visits to the tab are for. The face no longer has a content description, so TalkBack does not read the time twice. The readout, "Home", the city and the zone name are one merged node instead. Closes #4
This commit is contained in:
+3
-2
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
@@ -94,7 +93,9 @@ class WorldClockInstrumentedTest {
|
||||
|
||||
val home = java.time.ZoneId.systemDefault().id.substringAfterLast('/')
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(home, substring = true).assertExists()
|
||||
// The face itself is hidden from TalkBack; the readout and caption
|
||||
// under it are one merged node that names the city.
|
||||
composeTestRule.onNodeWithText(home, substring = true).assertExists()
|
||||
}
|
||||
|
||||
/** §5.11 #2 */
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.graphics.shapes.Morph
|
||||
import de.jeanlucmakiola.clockula.domain.worldclock.AnalogFace
|
||||
@@ -40,17 +39,15 @@ import java.time.LocalTime
|
||||
* colour, space and motion" means. M7 refused a decorative arc on the stopwatch
|
||||
* for the mirror-image reason: there, there was nothing to be a fraction of.
|
||||
*
|
||||
* Scheme tokens only, no literal colour. The semantics are set outright — an
|
||||
* analog face has nothing a screen reader can read off it — and deliberately
|
||||
* **not** a live region: at 1 Hz it would recite the clock for as long as the
|
||||
* tab was open, the mistake M4 already refused for the live pill.
|
||||
* Scheme tokens only, no literal colour. The face is hidden from screen
|
||||
* readers: the time readout under it says the same time as text, so
|
||||
* describing the face as well would read the time twice.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
internal fun WorldClockFace(
|
||||
time: LocalTime,
|
||||
dayFraction: Float,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dialColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
@@ -81,7 +78,7 @@ internal fun WorldClockFace(
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(WorldClockDefaults.HeroFaceSize)
|
||||
.clearAndSetSemantics { this.contentDescription = contentDescription },
|
||||
.clearAndSetSemantics {},
|
||||
) {
|
||||
drawDial(morph, progress, dialPath, dialMatrix, dialColor)
|
||||
drawTicks(markColor)
|
||||
|
||||
@@ -46,10 +46,13 @@ import java.time.LocalTime
|
||||
* The World clock tab: a hero analog face for **home**, then the cities, each
|
||||
* with its offset and day relative to home.
|
||||
*
|
||||
* The face is the home readout and the digital time is deliberately not
|
||||
* repeated under it — an analog face that also prints its own time is a face
|
||||
* nobody looks at (D11). The caption is where the ICU long zone name is shown,
|
||||
* which is the one place it earns its length.
|
||||
* Under the face, the home time is repeated as a digital readout in the app's
|
||||
* 12/24-hour format. This reverses D11, which left it out because an analog
|
||||
* face that also prints its own time is a face nobody looks at: reading the
|
||||
* exact minute off a dial is slower than reading it off digits, and that
|
||||
* minute is what most visits to the tab are for. The face still carries what
|
||||
* digits cannot: day or night at home, at a glance. The caption is where the
|
||||
* ICU long zone name is shown, which is the one place it earns its length.
|
||||
*
|
||||
* `scrollable` stays true: `ReorderableColumn` is not lazy and cannot live
|
||||
* inside a `LazyColumn`, and a list capped at twenty-four rows is exactly the
|
||||
@@ -187,40 +190,52 @@ private fun HomeFace(home: HomeFaceState, formatTime: (LocalTime) -> String) {
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = GroupedListInset, vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
// Hidden from TalkBack: the readout below says the same time.
|
||||
WorldClockFace(
|
||||
time = home.time,
|
||||
dayFraction = home.dayFraction,
|
||||
contentDescription = stringResource(
|
||||
R.string.world_clock_face_description,
|
||||
home.city,
|
||||
formatTime(home.time),
|
||||
),
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
)
|
||||
// The face has no other label saying whose time it is, and a city here
|
||||
// is otherwise indistinguishable from a city in the list below.
|
||||
Text(
|
||||
text = stringResource(R.string.world_clock_home),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = home.city,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
// The one place the ICU long zone name earns its length.
|
||||
home.zoneName?.let { name ->
|
||||
// One TalkBack stop for the time and whose time it is ("14:05, Home,
|
||||
// Berlin, Central European Standard Time") rather than four. Not a
|
||||
// live region: it would recite the clock every minute for as long as
|
||||
// the tab was open, the mistake M4 already refused for the live pill.
|
||||
Column(
|
||||
modifier = Modifier.semantics(mergeDescendants = true) {},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = formatTime(home.time),
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
// The face has no other label saying whose time it is, and a city
|
||||
// here is otherwise indistinguishable from a city in the list below.
|
||||
Text(
|
||||
text = stringResource(R.string.world_clock_home),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = home.city,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
// The one place the ICU long zone name earns its length.
|
||||
home.zoneName?.let { name ->
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
<string name="world_clock_day_today">Today</string>
|
||||
<string name="world_clock_day_tomorrow">Tomorrow</string>
|
||||
<string name="world_clock_unknown_zone">This time zone is no longer on your device</string>
|
||||
<string name="world_clock_face_description">%1$s, %2$s</string>
|
||||
<string name="world_clock_row_description">%1$s, %2$s, %3$s</string>
|
||||
<!-- A row whose zone the device no longer knows has no time to read out. -->
|
||||
<string name="world_clock_row_description_unknown">%1$s, %2$s</string>
|
||||
|
||||
+11
-6
@@ -2383,12 +2383,17 @@ The rest is ordinary `Canvas` work over `AnalogFace`'s pure angles — twelve ho
|
||||
marks with the four quarters longer, three hands, a centre cap — in scheme
|
||||
tokens only. The morph is built once and the path re-derived into a reused
|
||||
`Path` per frame through `toPath(Morph, Float, Path)`, which is not
|
||||
`@Composable` and is therefore callable from a `DrawScope`. The face is the home
|
||||
readout and the digital time is **not** repeated under it: an analog face that
|
||||
also prints its own time is a face nobody looks at. It carries a content
|
||||
description naming the city and the time, and is deliberately **not** a live
|
||||
region — at 1 Hz it would recite the clock for as long as the tab was open,
|
||||
which is the mistake M4 already refused for the live pill (§12).
|
||||
`@Composable` and is therefore callable from a `DrawScope`. M8 left the digital
|
||||
time out from under the face on the grounds that an analog face that also
|
||||
prints its own time is a face nobody looks at (D11). **That is reversed:** the
|
||||
home time now sits under the face as a `displaySmall` readout in the app's
|
||||
12/24-hour format, because the exact minute is what most visits to the tab are
|
||||
for and a dial is the slow way to read it. The face keeps the day/night shape
|
||||
digits cannot carry. With the time on screen as text, the face is hidden from
|
||||
TalkBack, and the readout, "Home", the city and the zone name are one merged
|
||||
node, so the time is read once. Neither is a live region — that would recite
|
||||
the clock for as long as the tab was open, which is the mistake M4 already
|
||||
refused for the live pill (§12).
|
||||
|
||||
### The cadence
|
||||
|
||||
|
||||
Reference in New Issue
Block a user