Configure playback controls for RTL (#1945)
## Description Reverts/fixes parts of #1404 for the playback UI. Generally the playback UI always uses left-to-right for seek bar & media controls even on RTL. Other buttons or texts will be RTL. ### Related issues Fixes #1397 ### Testing Emulator w/ forced RTL English ## Screenshots See https://github.com/damontecres/Wholphin/issues/1397#issuecomment-5553711593 ## AI or LLM usage None
This commit is contained in:
parent
4a567cbcf4
commit
0b995b5404
6 changed files with 185 additions and 214 deletions
|
|
@ -45,9 +45,7 @@ import androidx.compose.ui.input.key.KeyEventType
|
|||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
|
@ -106,11 +104,9 @@ fun NowPlayingPage(
|
|||
).value.appPreferences
|
||||
val musicPrefs = preferences.musicPreferences
|
||||
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val keyHandler =
|
||||
remember(isLtr, preferences) {
|
||||
remember(preferences) {
|
||||
PlaybackKeyHandler(
|
||||
isLtr = isLtr,
|
||||
player = player,
|
||||
controlsEnabled = true,
|
||||
skipWithLeftRight = false,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import kotlin.time.Duration
|
|||
* Handles [KeyEvent]s during playback on [PlaybackPage]
|
||||
*/
|
||||
class PlaybackKeyHandler(
|
||||
private val isLtr: Boolean,
|
||||
private val player: Player,
|
||||
private val controlsEnabled: Boolean,
|
||||
private val skipWithLeftRight: Boolean,
|
||||
|
|
@ -53,17 +52,9 @@ class PlaybackKeyHandler(
|
|||
if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) {
|
||||
if (!controllerViewState.controlsVisible) {
|
||||
if (skipWithLeftRight && isSkipBack(it)) {
|
||||
if (isLtr) {
|
||||
seekBy(-seekBack)
|
||||
} else {
|
||||
seekBy(seekForward)
|
||||
}
|
||||
seekBy(-seekBack)
|
||||
} else if (skipWithLeftRight && isSkipForward(it)) {
|
||||
if (isLtr) {
|
||||
seekBy(seekForward)
|
||||
} else {
|
||||
seekBy(-seekBack)
|
||||
}
|
||||
seekBy(seekForward)
|
||||
} else if (isEnterKey(it) && isDpadSeekVisible()) {
|
||||
// If d-pad seek bar is visible, hide it
|
||||
clearSkipIndicator.invoke()
|
||||
|
|
@ -142,7 +133,7 @@ class PlaybackKeyHandler(
|
|||
return false
|
||||
}
|
||||
|
||||
val isBack = if (isLtr) isSkipBack(event) else isSkipForward(event)
|
||||
val isBack = isSkipBack(event)
|
||||
return when (event.type) {
|
||||
KeyEventType.KeyDown -> {
|
||||
val repeatCount = event.nativeKeyEvent.repeatCount
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.systemBars
|
|||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -243,11 +244,9 @@ fun PlaybackPageContent(
|
|||
)
|
||||
seekBarState.onValueChange(skipPosition)
|
||||
}
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val keyHandler =
|
||||
remember(isLtr, preferences) {
|
||||
remember(preferences) {
|
||||
PlaybackKeyHandler(
|
||||
isLtr = isLtr,
|
||||
player = player,
|
||||
controlsEnabled = state.nextUp == null,
|
||||
skipWithLeftRight = true,
|
||||
|
|
@ -390,17 +389,19 @@ fun PlaybackPageContent(
|
|||
Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
DpadSeekOverlay(
|
||||
player = player,
|
||||
seekPositionMs = skipPosition,
|
||||
trickplayInfo = state.currentMediaInfo.trickPlayInfo,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp)
|
||||
.fillMaxWidth(.95f),
|
||||
)
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
DpadSeekOverlay(
|
||||
player = player,
|
||||
seekPositionMs = skipPosition,
|
||||
trickplayInfo = state.currentMediaInfo.trickPlayInfo,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp)
|
||||
.fillMaxWidth(.95f),
|
||||
)
|
||||
}
|
||||
// Clear the overlay after a delay
|
||||
LaunchedEffect(skipIndicatorDuration) {
|
||||
delay(1.5.seconds)
|
||||
|
|
@ -426,15 +427,23 @@ fun PlaybackPageContent(
|
|||
// Show a small progress bar along the bottom of the screen
|
||||
if (prefs.dpadSeekMode == DpadSeekMode.SEEKBAR_MINIMAL) {
|
||||
val percent = skipPosition.toFloat() / player.duration.toFloat()
|
||||
Box(
|
||||
modifier =
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(MaterialTheme.colorScheme.border)
|
||||
.clip(RectangleShape)
|
||||
.height(3.dp)
|
||||
.fillMaxWidth(percent),
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(MaterialTheme.colorScheme.border)
|
||||
.clip(RectangleShape)
|
||||
.height(3.dp)
|
||||
.fillMaxWidth(percent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import androidx.compose.foundation.relocation.bringIntoViewRequester
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
|
|
@ -191,6 +192,7 @@ fun PlaybackControls(
|
|||
onClickPlaybackDialogType = onClickPlaybackDialogType,
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
)
|
||||
|
||||
PlaybackButtons(
|
||||
player = player,
|
||||
initialFocusRequester = initialFocusRequester,
|
||||
|
|
@ -204,6 +206,7 @@ fun PlaybackControls(
|
|||
skipBackOnResume = skipBackOnResume,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
|
|
@ -286,31 +289,40 @@ fun SeekTimecodes(
|
|||
durationMs: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
val positionSec = positionMs / 1000
|
||||
val remainingSec = (durationMs - positionMs) / 1000
|
||||
val positionText = remember(positionSec) { resources.formatDuration(positionSec.seconds) }
|
||||
val remainingText = remember(remainingSec) { "-${resources.formatDuration(remainingSec.seconds)}" }
|
||||
Text(
|
||||
text = positionText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
)
|
||||
Text(
|
||||
text = remainingText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
)
|
||||
val currentLayoutDirection = LocalLayoutDirection.current
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
val positionSec = positionMs / 1000
|
||||
val remainingSec = (durationMs - positionMs) / 1000
|
||||
val positionText =
|
||||
remember(positionSec) { resources.formatDuration(positionSec.seconds) }
|
||||
val remainingText =
|
||||
remember(remainingSec) { "-${resources.formatDuration(remainingSec.seconds)}" }
|
||||
CompositionLocalProvider(LocalLayoutDirection provides currentLayoutDirection) {
|
||||
Text(
|
||||
text = positionText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalLayoutDirection provides currentLayoutDirection) {
|
||||
Text(
|
||||
text = remainingText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,61 +401,62 @@ fun PlaybackButtons(
|
|||
seekForward: Duration,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
Row(
|
||||
modifier = modifier.focusGroup(),
|
||||
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
||||
) {
|
||||
PlaybackButton(
|
||||
iconRes = if (isLtr) R.drawable.baseline_skip_previous_24 else R.drawable.baseline_skip_next_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
onPlaybackActionClick.invoke(PlaybackAction.Previous)
|
||||
},
|
||||
enabled = previousEnabled,
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = if (isLtr) R.drawable.baseline_fast_rewind_24 else R.drawable.baseline_fast_forward_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
player.seekBack(seekBack)
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
modifier = Modifier.focusRequester(initialFocusRequester),
|
||||
iconRes = if (showPlay) R.drawable.baseline_play_arrow_24 else R.drawable.baseline_pause_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
if (showPlay) {
|
||||
player.play()
|
||||
skipBackOnResume?.let {
|
||||
player.seekBack(it)
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
modifier = modifier.focusGroup(),
|
||||
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
||||
) {
|
||||
PlaybackButton(
|
||||
iconRes = R.drawable.baseline_skip_previous_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
onPlaybackActionClick.invoke(PlaybackAction.Previous)
|
||||
},
|
||||
enabled = previousEnabled,
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = R.drawable.baseline_fast_rewind_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
player.seekBack(seekBack)
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
modifier = Modifier.focusRequester(initialFocusRequester),
|
||||
iconRes = if (showPlay) R.drawable.baseline_play_arrow_24 else R.drawable.baseline_pause_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
if (showPlay) {
|
||||
player.play()
|
||||
skipBackOnResume?.let {
|
||||
player.seekBack(it)
|
||||
}
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = if (isLtr) R.drawable.baseline_fast_forward_24 else R.drawable.baseline_fast_rewind_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
player.seekForward(seekForward)
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = if (isLtr) R.drawable.baseline_skip_next_24 else R.drawable.baseline_skip_previous_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
onPlaybackActionClick.invoke(PlaybackAction.Next)
|
||||
},
|
||||
enabled = nextEnabled,
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = R.drawable.baseline_fast_forward_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
player.seekForward(seekForward)
|
||||
},
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
PlaybackButton(
|
||||
iconRes = R.drawable.baseline_skip_next_24,
|
||||
onClick = {
|
||||
onControllerInteraction.invoke()
|
||||
onPlaybackActionClick.invoke(PlaybackAction.Next)
|
||||
},
|
||||
enabled = nextEnabled,
|
||||
onControllerInteraction = onControllerInteraction,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.heightIn
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
|
|
@ -40,8 +41,10 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -254,39 +257,41 @@ fun PlaybackOverlay(
|
|||
}
|
||||
}
|
||||
|
||||
// Trickplay
|
||||
AnimatedVisibility(
|
||||
visible = controllerViewState.controlsVisible && seekProgressPercent >= 0 && seekBarFocused,
|
||||
enter =
|
||||
expandVertically(
|
||||
spring(
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
visibilityThreshold = IntSize.VisibilityThreshold,
|
||||
),
|
||||
) + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(.95f),
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
// Trickplay
|
||||
AnimatedVisibility(
|
||||
visible = controllerViewState.controlsVisible && seekProgressPercent >= 0 && seekBarFocused,
|
||||
enter =
|
||||
expandVertically(
|
||||
spring(
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
visibilityThreshold = IntSize.VisibilityThreshold,
|
||||
),
|
||||
) + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.offsetByPercent(
|
||||
xPercentage = seekProgressPercent.coerceIn(0f, 1f),
|
||||
).padding(bottom = controllerHeight - titleHeight - subtitleHeight),
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(.95f),
|
||||
) {
|
||||
TrickplayPreview(
|
||||
seekProgressMs = seekProgressMs,
|
||||
trickplayInfo = trickplayInfo,
|
||||
trickplayUrlFor = trickplayUrlFor,
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.offsetByPercent(
|
||||
xPercentage = seekProgressPercent.coerceIn(0f, 1f),
|
||||
).padding(bottom = controllerHeight - titleHeight - subtitleHeight),
|
||||
) {
|
||||
TrickplayPreview(
|
||||
seekProgressMs = seekProgressMs,
|
||||
trickplayInfo = trickplayInfo,
|
||||
trickplayUrlFor = trickplayUrlFor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ import androidx.compose.ui.graphics.StrokeCap
|
|||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
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.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
|
|
@ -202,7 +200,6 @@ private fun SeekBarDisplay(
|
|||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val color = MaterialTheme.colorScheme.border
|
||||
val onSurface = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
|
|
@ -225,13 +222,7 @@ private fun SeekBarDisplay(
|
|||
Timber.v("Ignoring %s", event)
|
||||
return@onPreviewKeyEvent false
|
||||
}
|
||||
val seekBack =
|
||||
if (isLtr) {
|
||||
isDpadLeft(event)
|
||||
} else {
|
||||
isDpadRight(event)
|
||||
}
|
||||
if (seekBack) {
|
||||
if (isDpadLeft(event)) {
|
||||
when (event.type) {
|
||||
KeyEventType.KeyDown -> {
|
||||
val repeatCount = event.nativeKeyEvent.repeatCount
|
||||
|
|
@ -300,68 +291,34 @@ private fun SeekBarDisplay(
|
|||
strokeWidth = size.height,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
if (isLtr) {
|
||||
drawLine(
|
||||
color = onSurface.copy(alpha = .65f),
|
||||
start = Offset(x = 0f, y = yOffset),
|
||||
end =
|
||||
Offset(
|
||||
x = size.width.times(bufferedProgress),
|
||||
y = yOffset,
|
||||
),
|
||||
strokeWidth = size.height,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = 0f, y = yOffset),
|
||||
end =
|
||||
Offset(
|
||||
x = size.width.times(progress),
|
||||
y = yOffset,
|
||||
),
|
||||
strokeWidth = size.height,
|
||||
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,
|
||||
)
|
||||
}
|
||||
drawLine(
|
||||
color = onSurface.copy(alpha = .65f),
|
||||
start = Offset(x = 0f, y = yOffset),
|
||||
end =
|
||||
Offset(
|
||||
x = size.width.times(bufferedProgress),
|
||||
y = yOffset,
|
||||
),
|
||||
strokeWidth = size.height,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = 0f, y = yOffset),
|
||||
end =
|
||||
Offset(
|
||||
x = size.width.times(progress),
|
||||
y = yOffset,
|
||||
),
|
||||
strokeWidth = size.height,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
drawCircle(
|
||||
color = Color.White,
|
||||
radius = size.height + 2,
|
||||
center =
|
||||
Offset(
|
||||
x =
|
||||
if (isLtr) {
|
||||
size.width.times(progress)
|
||||
} else {
|
||||
size.width - size.width.times(progress)
|
||||
},
|
||||
x = size.width.times(progress),
|
||||
y = yOffset,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue