From 1a23703911c1c23907d9687a5808293a6b825006 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Mon, 12 Oct 2026 14:00:57 +0200 Subject: [PATCH] refactor: set traversal order for player --- .../ajaury/chombev/player/ui/PlayerControl.kt | 4 +++ .../ajaury/chombev/player/ui/PlayerScreen.kt | 33 +++++++++++++------ .../chombev/player/ui/PlayerTraversalOrder.kt | 30 +++++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerTraversalOrder.kt diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt index e2742fc..8a30ca7 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt @@ -58,6 +58,7 @@ fun PlayerControl( contentAlignment = Alignment.Center, ) { SpeedPreferenceButton( + modifier = Modifier.traversalOrder(PlayerTraversalOrder.PLAYBACK_SETTINGS), isActive = isPlaybackModified, onSpeedClicked = onSpeedClicked, ) @@ -69,17 +70,20 @@ fun PlayerControl( verticalAlignment = Alignment.CenterVertically, ) { PreviousButton( + modifier = Modifier.traversalOrder(PlayerTraversalOrder.PREVIOUS), canGoToPreviousSentence = canGoToPreviousSentence, onPreviousSentenceClicked = onPreviousSentenceClicked, ) PlayButton( + modifier = Modifier.traversalOrder(PlayerTraversalOrder.PLAY), playbackState = playbackState, breakDuration = breakBetweenPhrases, onMainPlayActionButtonClicked = onMainPlayActionButtonClicked, ) NextButton( + modifier = Modifier.traversalOrder(PlayerTraversalOrder.NEXT), canGoToNextSentence = canGoToNextSentence, onNextSentenceClicked = onNextSentenceClicked, ) diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt index 90ec8a5..46622c5 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow @@ -116,6 +117,9 @@ fun PlayerScreen( var showSpeed by remember { mutableStateOf(false) } Scaffold( + // Root of the screen reader order defined by PlayerTraversalOrder, which interleaves the + // top bar and the controls. + modifier = Modifier.semantics { isTraversalGroup = true }, topBar = { PlayerTopBar( uiState = uiState, @@ -191,7 +195,11 @@ private fun PlayerTopBar( showPreferences: () -> Unit, ) { TopAppBar( - modifier = Modifier.shadow(elevation = 2.dp), + modifier = Modifier + .shadow(elevation = 2.dp) + // TopAppBar makes itself a traversal group, which would keep its subtitle settings + // button next to the title. Flattened so PlayerTraversalOrder can move it last. + .semantics { isTraversalGroup = false }, title = { // Animate the title in as it loads, and between records: the record data isn't there on // the first frame, so fade + rise it into place once uiState carries it. @@ -204,9 +212,11 @@ private fun PlayerTopBar( label = "PlayerTitle", ) { (emoji, title) -> Row( - modifier = Modifier.clearAndSetSemantics { - contentDescription = title.translation ?: title.transcription - }, + modifier = Modifier + .traversalOrder(PlayerTraversalOrder.TITLE) + .clearAndSetSemantics { + contentDescription = title.translation ?: title.transcription + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -242,12 +252,14 @@ private fun PlayerTopBar( val openSettingsLabel = stringResource(Res.string.settings_open_action_description) IconButton( // Names the action: "double-tap to open settings" rather than the generic "to activate". - modifier = Modifier.semantics { - onClick(label = openSettingsLabel) { - showPreferences() - true - } - }, + modifier = Modifier + .traversalOrder(PlayerTraversalOrder.SUBTITLE_SETTINGS) + .semantics { + onClick(label = openSettingsLabel) { + showPreferences() + true + } + }, onClick = showPreferences, ) { Icon( @@ -265,6 +277,7 @@ private fun BackButton(onBackClicked: () -> Unit) { IconButton( onClick = onBackClicked, modifier = Modifier + .traversalOrder(PlayerTraversalOrder.BACK) .semantics { onClick(label = backActionLabel) { onBackClicked() diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerTraversalOrder.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerTraversalOrder.kt new file mode 100644 index 0000000..725aa34 --- /dev/null +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerTraversalOrder.kt @@ -0,0 +1,30 @@ +package bzh.ajaury.chombev.player.ui + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex + +/** + * Screen reader order on the player, which departs from the visual layout: the title and back + * button first, then the transport controls, the settings last. + */ +internal enum class PlayerTraversalOrder { + TITLE, + BACK, + PLAY, + PREVIOUS, + NEXT, + PLAYBACK_SETTINGS, + SUBTITLE_SETTINGS, +} + +/** + * Places this element at [order] in the player's screen reader order. Grouped so that an element + * made of several nodes (e.g. the play button and its break ring) moves as a whole. + */ +internal fun Modifier.traversalOrder(order: PlayerTraversalOrder): Modifier = + semantics { + isTraversalGroup = true + traversalIndex = order.ordinal.toFloat() + }