refactor: set traversal order for player

This commit is contained in:
Antoine Jaury
2026-10-12 14:03:21 +02:00
parent 242d494c86
commit 1a23703911
3 changed files with 57 additions and 10 deletions
@@ -58,6 +58,7 @@ fun PlayerControl(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
SpeedPreferenceButton( SpeedPreferenceButton(
modifier = Modifier.traversalOrder(PlayerTraversalOrder.PLAYBACK_SETTINGS),
isActive = isPlaybackModified, isActive = isPlaybackModified,
onSpeedClicked = onSpeedClicked, onSpeedClicked = onSpeedClicked,
) )
@@ -69,17 +70,20 @@ fun PlayerControl(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
PreviousButton( PreviousButton(
modifier = Modifier.traversalOrder(PlayerTraversalOrder.PREVIOUS),
canGoToPreviousSentence = canGoToPreviousSentence, canGoToPreviousSentence = canGoToPreviousSentence,
onPreviousSentenceClicked = onPreviousSentenceClicked, onPreviousSentenceClicked = onPreviousSentenceClicked,
) )
PlayButton( PlayButton(
modifier = Modifier.traversalOrder(PlayerTraversalOrder.PLAY),
playbackState = playbackState, playbackState = playbackState,
breakDuration = breakBetweenPhrases, breakDuration = breakBetweenPhrases,
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked, onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
) )
NextButton( NextButton(
modifier = Modifier.traversalOrder(PlayerTraversalOrder.NEXT),
canGoToNextSentence = canGoToNextSentence, canGoToNextSentence = canGoToNextSentence,
onNextSentenceClicked = onNextSentenceClicked, onNextSentenceClicked = onNextSentenceClicked,
) )
@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -116,6 +117,9 @@ fun PlayerScreen(
var showSpeed by remember { mutableStateOf(false) } var showSpeed by remember { mutableStateOf(false) }
Scaffold( Scaffold(
// Root of the screen reader order defined by PlayerTraversalOrder, which interleaves the
// top bar and the controls.
modifier = Modifier.semantics { isTraversalGroup = true },
topBar = { topBar = {
PlayerTopBar( PlayerTopBar(
uiState = uiState, uiState = uiState,
@@ -191,7 +195,11 @@ private fun PlayerTopBar(
showPreferences: () -> Unit, showPreferences: () -> Unit,
) { ) {
TopAppBar( 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 = { title = {
// Animate the title in as it loads, and between records: the record data isn't there on // 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. // the first frame, so fade + rise it into place once uiState carries it.
@@ -204,9 +212,11 @@ private fun PlayerTopBar(
label = "PlayerTitle", label = "PlayerTitle",
) { (emoji, title) -> ) { (emoji, title) ->
Row( Row(
modifier = Modifier.clearAndSetSemantics { modifier = Modifier
contentDescription = title.translation ?: title.transcription .traversalOrder(PlayerTraversalOrder.TITLE)
}, .clearAndSetSemantics {
contentDescription = title.translation ?: title.transcription
},
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
@@ -242,12 +252,14 @@ private fun PlayerTopBar(
val openSettingsLabel = stringResource(Res.string.settings_open_action_description) val openSettingsLabel = stringResource(Res.string.settings_open_action_description)
IconButton( IconButton(
// Names the action: "double-tap to open settings" rather than the generic "to activate". // Names the action: "double-tap to open settings" rather than the generic "to activate".
modifier = Modifier.semantics { modifier = Modifier
onClick(label = openSettingsLabel) { .traversalOrder(PlayerTraversalOrder.SUBTITLE_SETTINGS)
showPreferences() .semantics {
true onClick(label = openSettingsLabel) {
} showPreferences()
}, true
}
},
onClick = showPreferences, onClick = showPreferences,
) { ) {
Icon( Icon(
@@ -265,6 +277,7 @@ private fun BackButton(onBackClicked: () -> Unit) {
IconButton( IconButton(
onClick = onBackClicked, onClick = onBackClicked,
modifier = Modifier modifier = Modifier
.traversalOrder(PlayerTraversalOrder.BACK)
.semantics { .semantics {
onClick(label = backActionLabel) { onClick(label = backActionLabel) {
onBackClicked() onBackClicked()
@@ -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()
}