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,
) {
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,
)
@@ -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()
@@ -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()
}