change player control button arrangement in RTL layouts

This commit is contained in:
MM20
2025-11-30 13:51:48 +01:00
parent 52587dfa62
commit 1609c62489
2 changed files with 221 additions and 203 deletions

View File

@@ -19,16 +19,19 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
@@ -63,77 +66,81 @@ class MusicPartProvider : PartProvider, KoinComponent {
val supportedActions by musicService.supportedActions.collectAsState(SupportedActions())
if (compactLayout) {
Row(
verticalAlignment = Alignment.CenterVertically
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
Column(
modifier = Modifier
.weight(1f)
.padding(8.dp)
.combinedClickable(
onClick = {
try {
musicService
.openPlayer()
?.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
},
onLongClick = {
musicService.openPlayerChooser(context)
}
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
title?.let {
Text(
text = it,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
artist?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
IconButton(onClick = { musicService.togglePause() }) {
AnimatedContent(
state == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (state == PlaybackState.Playing) 90f else 0f
).value
)
) {
if (it) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
Column(
modifier = Modifier
.weight(1f)
.padding(8.dp)
.combinedClickable(
onClick = {
try {
musicService
.openPlayer()
?.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
},
onLongClick = {
musicService.openPlayerChooser(context)
}
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
) {
title?.let {
Text(
text = it,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
artist?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
if (supportedActions.skipToNext) {
IconButton(onClick = { musicService.next() }) {
Icon(
painterResource(R.drawable.skip_next_24px),
contentDescription = stringResource(R.string.music_widget_next_track)
)
IconButton(onClick = { musicService.togglePause() }) {
AnimatedContent(
state == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (state == PlaybackState.Playing) 90f else 0f
).value
)
) {
if (it) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
)
}
}
}
if (supportedActions.skipToNext) {
IconButton(onClick = { musicService.next() }) {
Icon(
painterResource(R.drawable.skip_next_24px),
contentDescription = stringResource(R.string.music_widget_next_track)
)
}
}
}
}
@@ -177,47 +184,51 @@ class MusicPartProvider : PartProvider, KoinComponent {
)
}
}
Row {
if (supportedActions.skipToPrevious) {
IconButton(onClick = { musicService.previous() }) {
Icon(
painterResource(R.drawable.skip_previous_24px),
contentDescription = stringResource(R.string.music_widget_previous_track)
)
}
}
IconButton(onClick = { musicService.togglePause() }) {
AnimatedContent(
state == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (state == PlaybackState.Playing) 90f else 0f
).value
)
) {
if (it) {
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
Row {
if (supportedActions.skipToPrevious) {
IconButton(onClick = { musicService.previous() }) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
painterResource(R.drawable.skip_previous_24px),
contentDescription = stringResource(R.string.music_widget_previous_track)
)
}
}
}
if (supportedActions.skipToPrevious) {
IconButton(onClick = { musicService.next() }) {
Icon(
painterResource(R.drawable.skip_next_24px),
contentDescription = stringResource(R.string.music_widget_next_track)
)
IconButton(onClick = { musicService.togglePause() }) {
AnimatedContent(
state == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (state == PlaybackState.Playing) 90f else 0f
).value
)
) {
if (it) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
)
}
}
}
if (supportedActions.skipToPrevious) {
IconButton(onClick = { musicService.next() }) {
Icon(
painterResource(R.drawable.skip_next_24px),
contentDescription = stringResource(R.string.music_widget_next_track)
)
}
}
}
}

View File

@@ -7,13 +7,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.animation.scaleIn
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -48,6 +43,7 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -62,12 +58,14 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect
import androidx.core.content.res.ResourcesCompat
@@ -165,39 +163,44 @@ fun MusicWidget(widget: MusicWidget) {
var seekPosition by remember { mutableStateOf<Float?>(null) }
if (pos != null && dur != null && dur > 0) {
if (playbackState != PlaybackState.Stopped && supportedActions.seekTo && widget.config.interactiveProgressBar) {
Slider(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.requiredHeightIn(max = 20.dp),
value = (if (isDragged) seekPosition else pos?.toFloat()) ?: 0f,
valueRange = 0f..dur.toFloat(),
interactionSource = interactionSource,
onValueChange = {
seekPosition = it
},
onValueChangeFinished = {
seekPosition?.let {
viewModel.seekTo(it.toLong())
pos = it.toLong()
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
if (playbackState != PlaybackState.Stopped && supportedActions.seekTo && widget.config.interactiveProgressBar) {
Slider(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.requiredHeightIn(max = 20.dp),
value = (if (isDragged) seekPosition else pos?.toFloat())
?: 0f,
valueRange = 0f..dur.toFloat(),
interactionSource = interactionSource,
onValueChange = {
seekPosition = it
},
onValueChangeFinished = {
seekPosition?.let {
viewModel.seekTo(it.toLong())
pos = it.toLong()
}
},
track = {
SliderDefaults.Track(
sliderState = it,
modifier = Modifier.requiredHeight(4.dp)
)
}
},
track = {
SliderDefaults.Track(
sliderState = it,
modifier = Modifier.requiredHeight(4.dp)
)
}
)
} else {
LinearProgressIndicator(
progress = { (pos?.toFloat() ?: 0f) / dur.toFloat() },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
strokeCap = StrokeCap.Round,
)
)
} else {
LinearProgressIndicator(
progress = { (pos?.toFloat() ?: 0f) / dur.toFloat() },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
strokeCap = StrokeCap.Round,
)
}
}
} else {
Box(
@@ -302,80 +305,84 @@ fun MusicWidget(widget: MusicWidget) {
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
if (supportedActions.skipToPrevious) {
Tooltip(
tooltipText = stringResource(R.string.music_widget_previous_track)
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = {
viewModel.skipPrevious()
}) {
Icon(
painter = painterResource(R.drawable.skip_previous_24px),
stringResource(R.string.music_widget_previous_track)
)
}
}
}
val playPauseIcon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
Tooltip(
tooltipText = stringResource(
if (playbackState == PlaybackState.Playing) R.string.music_widget_pause
else R.string.music_widget_play
)
) {
FilledTonalIconButton(
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = MaterialTheme.transparency.surface),
),
onClick = { viewModel.togglePause() },
) {
AnimatedContent(
playbackState == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (playbackState == PlaybackState.Playing) 90f else 0f
).value
)
if (supportedActions.skipToPrevious) {
Tooltip(
tooltipText = stringResource(R.string.music_widget_previous_track)
) {
if (it) {
IconButton(
onClick = {
viewModel.skipPrevious()
}) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
painter = painterResource(R.drawable.skip_previous_24px),
stringResource(R.string.music_widget_previous_track)
)
}
}
}
}
if (supportedActions.skipToNext) {
Tooltip(
tooltipText = stringResource(R.string.music_widget_next_track)
tooltipText = stringResource(
if (playbackState == PlaybackState.Playing) R.string.music_widget_pause
else R.string.music_widget_play
)
) {
IconButton(onClick = {
viewModel.skipNext()
}) {
Icon(
painter = painterResource(R.drawable.skip_next_24px),
stringResource(R.string.music_widget_next_track)
)
FilledTonalIconButton(
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = MaterialTheme.transparency.surface),
),
onClick = { viewModel.togglePause() },
) {
AnimatedContent(
playbackState == PlaybackState.Playing,
transitionSpec = {
fadeIn().togetherWith(fadeOut())
},
modifier = Modifier.rotate(
animateFloatAsState(
if (playbackState == PlaybackState.Playing) 90f else 0f
).value
)
) {
if (it) {
Icon(
painterResource(R.drawable.pause_24px),
stringResource(R.string.music_widget_pause),
modifier = Modifier.rotate(-90f)
)
} else {
Icon(
painterResource(R.drawable.play_arrow_24px),
stringResource(R.string.music_widget_play),
)
}
}
}
}
if (supportedActions.skipToNext) {
Tooltip(
tooltipText = stringResource(R.string.music_widget_next_track)
) {
IconButton(onClick = {
viewModel.skipNext()
}) {
Icon(
painter = painterResource(R.drawable.skip_next_24px),
stringResource(R.string.music_widget_next_track)
)
}
}
}
}