Fix several issues with right-to-left locales (#1404)

## Description
Fix several issues when the current locale is right-to-left

- Fixes the backdrop dynamic colors
- Fixes mirroring of playback controls, including seeking

### Related issues
Fixes #1397
Fixes #1398

### Testing
Emulator w/ forced RTL in developer options

## Screenshots
<img width="1280" height="720" alt="rtl_backdrop Large"
src="https://github.com/user-attachments/assets/3e3b14ec-32cc-42c0-a224-d1d859a55acd"
/>
<img width="1280" height="720" alt="rtl_controls Large"
src="https://github.com/user-attachments/assets/79983bb9-e1d0-4712-a5c6-da3d0721db96"
/>


## AI or LLM usage

None
This commit is contained in:
Damontecres 2026-05-25 13:27:34 -04:00 committed by GitHub
parent 656393fa1d
commit 9eff22e939
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 214 additions and 117 deletions

View file

@ -45,7 +45,9 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
@ -106,9 +108,11 @@ fun NowPlayingPage(
).value.appPreferences ).value.appPreferences
val musicPrefs = preferences.musicPreferences val musicPrefs = preferences.musicPreferences
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val keyHandler = val keyHandler =
remember(preferences) { remember(isLtr, preferences) {
PlaybackKeyHandler( PlaybackKeyHandler(
isLtr = isLtr,
player = player, player = player,
controlsEnabled = true, controlsEnabled = true,
skipWithLeftRight = false, skipWithLeftRight = false,

View file

@ -23,6 +23,8 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -75,6 +77,7 @@ fun Backdrop(
useExistingImageAsPlaceholder: Boolean = false, useExistingImageAsPlaceholder: Boolean = false,
crossfadeDuration: Duration = 800.milliseconds, crossfadeDuration: Duration = 800.milliseconds,
) { ) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val baseBackgroundColor = MaterialTheme.colorScheme.background val baseBackgroundColor = MaterialTheme.colorScheme.background
if (backdrop.hasColors && if (backdrop.hasColors &&
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED) (backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
@ -100,12 +103,14 @@ fun Backdrop(
.fillMaxSize() .fillMaxSize()
.drawBehind { .drawBehind {
drawRect(color = baseBackgroundColor) drawRect(color = baseBackgroundColor)
val start = if (isRtl) size.width else 0f
val end = if (isRtl) 0f else size.width
// Top Left (Vibrant/Muted) // Top Left (Vibrant/Muted)
drawRect( drawRect(
brush = brush =
Brush.radialGradient( Brush.radialGradient(
colors = listOf(animSecondary, Color.Transparent), colors = listOf(animSecondary, Color.Transparent),
center = Offset(0f, 0f), center = Offset(start, 0f),
radius = size.width * 0.8f, radius = size.width * 0.8f,
), ),
) )
@ -114,7 +119,7 @@ fun Backdrop(
brush = brush =
Brush.radialGradient( Brush.radialGradient(
colors = listOf(animPrimary, Color.Transparent), colors = listOf(animPrimary, Color.Transparent),
center = Offset(size.width, size.height), center = Offset(end, size.height),
radius = size.width * 0.8f, radius = size.width * 0.8f,
), ),
) )
@ -127,7 +132,7 @@ fun Backdrop(
baseBackgroundColor, baseBackgroundColor,
Color.Transparent, Color.Transparent,
), ),
center = Offset(0f, size.height), center = Offset(start, size.height),
radius = size.width * 0.8f, radius = size.width * 0.8f,
), ),
) )
@ -136,7 +141,7 @@ fun Backdrop(
brush = brush =
Brush.radialGradient( Brush.radialGradient(
colors = listOf(animTertiary, Color.Transparent), colors = listOf(animTertiary, Color.Transparent),
center = Offset(size.width, 0f), center = Offset(end, 0f),
radius = size.width * 0.8f, radius = size.width * 0.8f,
), ),
) )
@ -165,6 +170,7 @@ fun Backdrop(
.fillMaxWidth(.7f) .fillMaxWidth(.7f)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent { .drawWithContent {
val start = if (isRtl) size.width else 0f
drawContent() drawContent()
if (drawerIsOpen) { if (drawerIsOpen) {
drawRect( drawRect(
@ -190,7 +196,7 @@ fun Backdrop(
brush = brush =
Brush.horizontalGradient( Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.Black), colors = listOf(Color.Transparent, Color.Black),
startX = 0f, startX = start,
endX = size.width * 0.6f, endX = size.width * 0.6f,
), ),
blendMode = BlendMode.DstIn, blendMode = BlendMode.DstIn,

View file

@ -32,6 +32,14 @@ fun isControllerMedia(event: KeyEvent) =
event.key == Key.ButtonL1 || event.key == Key.ButtonL1 ||
event.key == Key.ButtonL2 event.key == Key.ButtonL2
fun isDpadLeft(event: KeyEvent) =
event.key == Key.DirectionLeft ||
event.key == Key.SystemNavigationLeft
fun isDpadRight(event: KeyEvent) =
event.key == Key.DirectionRight ||
event.key == Key.SystemNavigationRight
fun isSkipBack(event: KeyEvent) = fun isSkipBack(event: KeyEvent) =
event.key == Key.DirectionLeft || event.key == Key.DirectionLeft ||
event.key == Key.ButtonL1 || event.key == Key.ButtonL1 ||

View file

@ -21,8 +21,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@ -85,6 +87,10 @@ fun PlaybackDialog(
onPlaybackActionClick: (PlaybackAction) -> Unit, onPlaybackActionClick: (PlaybackAction) -> Unit,
onChangeSubtitleDelay: (Duration) -> Unit, onChangeSubtitleDelay: (Duration) -> Unit,
) { ) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
// TODO, shouldn't this work out of the box?
val leftGravity = remember(isLtr) { if (isLtr) Gravity.START else Gravity.END }
val rightGravity = remember(isLtr) { if (isLtr) Gravity.END else Gravity.START }
when (type) { when (type) {
PlaybackDialogType.DEBUG -> { PlaybackDialogType.DEBUG -> {
throw IllegalStateException("Should not open a dialog with " + PlaybackDialogType.DEBUG) throw IllegalStateException("Should not open a dialog with " + PlaybackDialogType.DEBUG)
@ -113,7 +119,7 @@ fun PlaybackDialog(
onDismissRequest.invoke() onDismissRequest.invoke()
onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions) onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions)
}, },
gravity = Gravity.END, gravity = rightGravity,
) )
} }
@ -167,7 +173,7 @@ fun PlaybackDialog(
onClickPlaybackDialogType(choice.data) onClickPlaybackDialogType(choice.data)
} }
}, },
gravity = Gravity.START, gravity = leftGravity,
) )
} }
@ -182,7 +188,7 @@ fun PlaybackDialog(
onSelectChoice = { _, choice -> onSelectChoice = { _, choice ->
onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(choice.index)) onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(choice.index))
}, },
gravity = Gravity.END, gravity = rightGravity,
) )
} }
@ -205,7 +211,7 @@ fun PlaybackDialog(
onSelectChoice = { _, value -> onSelectChoice = { _, value ->
onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.data)) onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.data))
}, },
gravity = Gravity.START, gravity = leftGravity,
) )
} }
@ -228,7 +234,7 @@ fun PlaybackDialog(
onSelectChoice = { _, choice -> onSelectChoice = { _, choice ->
onPlaybackActionClick.invoke(PlaybackAction.Scale(choice.data)) onPlaybackActionClick.invoke(PlaybackAction.Scale(choice.data))
}, },
gravity = Gravity.START, gravity = leftGravity,
) )
} }

View file

@ -15,6 +15,7 @@ import kotlin.time.Duration
* Handles [KeyEvent]s during playback on [PlaybackPage] * Handles [KeyEvent]s during playback on [PlaybackPage]
*/ */
class PlaybackKeyHandler( class PlaybackKeyHandler(
private val isLtr: Boolean,
private val player: Player, private val player: Player,
private val controlsEnabled: Boolean, private val controlsEnabled: Boolean,
private val skipWithLeftRight: Boolean, private val skipWithLeftRight: Boolean,
@ -47,11 +48,21 @@ class PlaybackKeyHandler(
if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) { if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) {
if (!controllerViewState.controlsVisible) { if (!controllerViewState.controlsVisible) {
if (skipWithLeftRight && isSkipBack(it)) { if (skipWithLeftRight && isSkipBack(it)) {
updateSkipIndicator(-seekBack.inWholeMilliseconds) if (isLtr) {
player.seekBack(seekBack) updateSkipIndicator(-seekBack.inWholeMilliseconds)
player.seekBack(seekBack)
} else {
updateSkipIndicator(seekForward.inWholeMilliseconds)
player.seekForward(seekForward)
}
} else if (skipWithLeftRight && isSkipForward(it)) { } else if (skipWithLeftRight && isSkipForward(it)) {
player.seekForward(seekForward) if (isLtr) {
updateSkipIndicator(seekForward.inWholeMilliseconds) player.seekForward(seekForward)
updateSkipIndicator(seekForward.inWholeMilliseconds)
} else {
player.seekBack(seekBack)
updateSkipIndicator(seekBack.inWholeMilliseconds)
}
} else if (oneClickPause && isEnterKey(it)) { } else if (oneClickPause && isEnterKey(it)) {
val wasPlaying = player.isPlaying val wasPlaying = player.isPlaying
Util.handlePlayPauseButtonAction(player) Util.handlePlayPauseButtonAction(player)
@ -127,7 +138,7 @@ class PlaybackKeyHandler(
return false return false
} }
val isBack = isSkipBack(event) val isBack = if (isLtr) isSkipBack(event) else isSkipForward(event)
return when (event.type) { return when (event.type) {
KeyEventType.KeyDown -> { KeyEventType.KeyDown -> {
val repeatCount = event.nativeKeyEvent.repeatCount val repeatCount = event.nativeKeyEvent.repeatCount

View file

@ -50,7 +50,9 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -234,25 +236,29 @@ fun PlaybackPageContent(
skipIndicatorDuration += delta skipIndicatorDuration += delta
skipPosition = player.currentPosition skipPosition = player.currentPosition
} }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val keyHandler = val keyHandler =
PlaybackKeyHandler( remember(isLtr, preferences) {
player = player, PlaybackKeyHandler(
controlsEnabled = state.nextUp == null, isLtr = isLtr,
skipWithLeftRight = true, player = player,
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds, controlsEnabled = state.nextUp == null,
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds, skipWithLeftRight = true,
getDurationMs = { player.duration.coerceAtLeast(0L) }, seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
controllerViewState = controllerViewState, seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
updateSkipIndicator = updateSkipIndicator, getDurationMs = { player.duration.coerceAtLeast(0L) },
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, controllerViewState = controllerViewState,
onInteraction = viewModel::reportInteraction, updateSkipIndicator = updateSkipIndicator,
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause, skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
onStop = { onInteraction = viewModel::reportInteraction,
player.stop() oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
viewModel.navigationManager.goBack() onStop = {
}, player.stop()
onPlaybackDialogTypeClick = { playbackDialog = it }, viewModel.navigationManager.goBack()
) },
onPlaybackDialogTypeClick = { playbackDialog = it },
)
}
val onPlaybackActionClick: (PlaybackAction) -> Unit = { val onPlaybackActionClick: (PlaybackAction) -> Unit = {
when (it) { when (it) {

View file

@ -22,7 +22,9 @@ import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -47,6 +49,7 @@ fun ChapterRowOverlay(
onChangeState: (OverlayViewState) -> Unit, onChangeState: (OverlayViewState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val chapterInteractionSources = val chapterInteractionSources =
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } } remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
@ -135,7 +138,10 @@ fun ChapterRowOverlay(
index == 0, index == 0,
Modifier.focusProperties { Modifier.focusProperties {
// Prevent scrolling left on first card to prevent moving down // Prevent scrolling left on first card to prevent moving down
left = FocusRequester.Cancel left =
if (isLtr) FocusRequester.Cancel else FocusRequester.Default
right =
if (isLtr) FocusRequester.Default else FocusRequester.Cancel
}, },
), ),
) )

View file

@ -42,10 +42,12 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -368,12 +370,13 @@ fun PlaybackButtons(
seekForward: Duration, seekForward: Duration,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
Row( Row(
modifier = modifier.focusGroup(), modifier = modifier.focusGroup(),
horizontalArrangement = Arrangement.spacedBy(buttonSpacing), horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
) { ) {
PlaybackButton( PlaybackButton(
iconRes = R.drawable.baseline_skip_previous_24, iconRes = if (isLtr) R.drawable.baseline_skip_previous_24 else R.drawable.baseline_skip_next_24,
onClick = { onClick = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
onPlaybackActionClick.invoke(PlaybackAction.Previous) onPlaybackActionClick.invoke(PlaybackAction.Previous)
@ -382,7 +385,7 @@ fun PlaybackButtons(
onControllerInteraction = onControllerInteraction, onControllerInteraction = onControllerInteraction,
) )
PlaybackButton( PlaybackButton(
iconRes = R.drawable.baseline_fast_rewind_24, iconRes = if (isLtr) R.drawable.baseline_fast_rewind_24 else R.drawable.baseline_fast_forward_24,
onClick = { onClick = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
player.seekBack(seekBack) player.seekBack(seekBack)
@ -406,7 +409,7 @@ fun PlaybackButtons(
onControllerInteraction = onControllerInteraction, onControllerInteraction = onControllerInteraction,
) )
PlaybackButton( PlaybackButton(
iconRes = R.drawable.baseline_fast_forward_24, iconRes = if (isLtr) R.drawable.baseline_fast_forward_24 else R.drawable.baseline_fast_rewind_24,
onClick = { onClick = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
player.seekForward(seekForward) player.seekForward(seekForward)
@ -414,7 +417,7 @@ fun PlaybackButtons(
onControllerInteraction = onControllerInteraction, onControllerInteraction = onControllerInteraction,
) )
PlaybackButton( PlaybackButton(
iconRes = R.drawable.baseline_skip_next_24, iconRes = if (isLtr) R.drawable.baseline_skip_next_24 else R.drawable.baseline_skip_previous_24,
onClick = { onClick = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
onPlaybackActionClick.invoke(PlaybackAction.Next) onPlaybackActionClick.invoke(PlaybackAction.Next)

View file

@ -18,7 +18,6 @@ package com.github.damontecres.wholphin.ui.playback.overlay
* limitations under the License. * limitations under the License.
*/ */
import android.view.KeyEvent
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
@ -44,11 +43,16 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.ui.playback.ControllerViewState import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.calculateSeekAccelerationMultiplier import com.github.damontecres.wholphin.ui.playback.calculateSeekAccelerationMultiplier
import com.github.damontecres.wholphin.ui.playback.isDpadLeft
import com.github.damontecres.wholphin.ui.playback.isDpadRight
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import timber.log.Timber
import kotlin.time.Duration import kotlin.time.Duration
/** /**
@ -174,6 +178,7 @@ private fun SeekBarDisplay(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
) { ) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val color = MaterialTheme.colorScheme.border val color = MaterialTheme.colorScheme.border
val onSurface = MaterialTheme.colorScheme.onSurface val onSurface = MaterialTheme.colorScheme.onSurface
@ -192,70 +197,75 @@ private fun SeekBarDisplay(
.height(animatedIndicatorHeight) .height(animatedIndicatorHeight)
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.onPreviewKeyEvent { event -> .onPreviewKeyEvent { event ->
when (event.nativeKeyEvent.keyCode) { if (!isDpadLeft(event) && !isDpadRight(event)) {
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> { Timber.v("Ignoring %s", event)
when (event.type) { return@onPreviewKeyEvent false
KeyEventType.KeyDown -> { }
val repeatCount = event.nativeKeyEvent.repeatCount val seekBack =
if (repeatCount > 0) { if (isLtr) {
leftHandledByRepeat = true isDpadLeft(event)
onLeft.invoke( } else {
calculateSeekAccelerationMultiplier( isDpadRight(event)
repeatCount = repeatCount, }
durationMs = durationMs, if (seekBack) {
), when (event.type) {
) KeyEventType.KeyDown -> {
} else { val repeatCount = event.nativeKeyEvent.repeatCount
leftHandledByRepeat = false if (repeatCount > 0) {
} leftHandledByRepeat = true
} onLeft.invoke(
calculateSeekAccelerationMultiplier(
KeyEventType.KeyUp -> { repeatCount = repeatCount,
if (!leftHandledByRepeat) { durationMs = durationMs,
onLeft.invoke(1) ),
} )
} else {
leftHandledByRepeat = false leftHandledByRepeat = false
} }
else -> {
return@onPreviewKeyEvent false
}
} }
return@onPreviewKeyEvent true
}
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> { KeyEventType.KeyUp -> {
when (event.type) { if (!leftHandledByRepeat) {
KeyEventType.KeyDown -> { onLeft.invoke(1)
val repeatCount = event.nativeKeyEvent.repeatCount
if (repeatCount > 0) {
rightHandledByRepeat = true
onRight.invoke(
calculateSeekAccelerationMultiplier(
repeatCount = repeatCount,
durationMs = durationMs,
),
)
} else {
rightHandledByRepeat = false
}
} }
leftHandledByRepeat = false
}
KeyEventType.KeyUp -> { else -> {
if (!rightHandledByRepeat) { return@onPreviewKeyEvent false
onRight.invoke(1) }
} }
return@onPreviewKeyEvent true
} else {
when (event.type) {
KeyEventType.KeyDown -> {
val repeatCount = event.nativeKeyEvent.repeatCount
if (repeatCount > 0) {
rightHandledByRepeat = true
onRight.invoke(
calculateSeekAccelerationMultiplier(
repeatCount = repeatCount,
durationMs = durationMs,
),
)
} else {
rightHandledByRepeat = false rightHandledByRepeat = false
} }
else -> {
return@onPreviewKeyEvent false
}
} }
return@onPreviewKeyEvent true
KeyEventType.KeyUp -> {
if (!rightHandledByRepeat) {
onRight.invoke(1)
}
rightHandledByRepeat = false
}
else -> {
return@onPreviewKeyEvent false
}
} }
return@onPreviewKeyEvent true
} }
false
}.focusable(enabled = enabled, interactionSource = interactionSource), }.focusable(enabled = enabled, interactionSource = interactionSource),
onDraw = { onDraw = {
val yOffset = size.height.div(2) val yOffset = size.height.div(2)
@ -266,33 +276,70 @@ private fun SeekBarDisplay(
strokeWidth = size.height, strokeWidth = size.height,
cap = StrokeCap.Round, cap = StrokeCap.Round,
) )
drawLine( if (isLtr) {
color = onSurface.copy(alpha = .65f), drawLine(
start = Offset(x = 0f, y = yOffset), color = onSurface.copy(alpha = .65f),
end = start = Offset(x = 0f, y = yOffset),
Offset( end =
x = size.width.times(bufferedProgress), Offset(
y = yOffset, x = size.width.times(bufferedProgress),
), y = yOffset,
strokeWidth = size.height, ),
cap = StrokeCap.Round, strokeWidth = size.height,
) cap = StrokeCap.Round,
drawLine( )
color = color, drawLine(
start = Offset(x = 0f, y = yOffset), color = color,
end = start = Offset(x = 0f, y = yOffset),
Offset( end =
// x = size.width.times(if (isSelected) seekProgress else progress), Offset(
x = size.width.times(progress), x = size.width.times(progress),
y = yOffset, y = yOffset,
), ),
strokeWidth = size.height, strokeWidth = size.height,
cap = StrokeCap.Round, cap = StrokeCap.Round,
) )
} else {
drawLine(
color = onSurface.copy(alpha = .65f),
start =
Offset(
x = size.width - size.width.times(bufferedProgress),
y = yOffset,
),
end =
Offset(
x = size.width,
y = yOffset,
),
strokeWidth = size.height,
cap = StrokeCap.Round,
)
drawLine(
color = color,
start = Offset(x = size.width - size.width.times(progress), y = yOffset),
end =
Offset(
x = size.width,
y = yOffset,
),
strokeWidth = size.height,
cap = StrokeCap.Round,
)
}
drawCircle( drawCircle(
color = Color.White, color = Color.White,
radius = size.height + 2, radius = size.height + 2,
center = Offset(x = size.width.times(progress), y = yOffset), center =
Offset(
x =
if (isLtr) {
size.width.times(progress)
} else {
size.width - size.width.times(progress)
},
y = yOffset,
),
) )
}, },
) )