From 05c5b07ae5d30068e81fcb6f0d7ac3205565610d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Fri, 26 Jul 2024 16:56:40 +0200 Subject: [PATCH] Move `TVSlider` to `pillarbox-demo-shared` (#650) --- pillarbox-demo-shared/build.gradle.kts | 11 + .../shared}/extension/ModifierExtensions.kt | 2 +- .../shared/ui/components/PillarboxSlider.kt | 466 ++++++++++++++++++ .../demo/tv/ui/components/TVDemoTopBar.kt | 2 +- .../demo/tv/ui/components/TVSlider.kt | 196 -------- .../demo/tv/ui/examples/ExamplesHome.kt | 2 +- .../pillarbox/demo/tv/ui/lists/ListsHome.kt | 4 +- .../demo/tv/ui/player/compose/PlayerView.kt | 18 +- .../compose/controls/PlayerPlaybackRow.kt | 2 +- .../pillarbox/demo/tv/ui/search/SearchHome.kt | 2 +- .../ui/player/controls/PlayerTimeSlider.kt | 79 ++- .../showcases/misc/ResizablePlayerShowcase.kt | 120 +++-- 12 files changed, 600 insertions(+), 304 deletions(-) rename {pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv => pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared}/extension/ModifierExtensions.kt (97%) create mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt delete mode 100644 pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVSlider.kt diff --git a/pillarbox-demo-shared/build.gradle.kts b/pillarbox-demo-shared/build.gradle.kts index 6ac999321..5ad7ae4dd 100644 --- a/pillarbox-demo-shared/build.gradle.kts +++ b/pillarbox-demo-shared/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.pillarbox.android.library) + alias(libs.plugins.pillarbox.android.library.compose) } dependencies { @@ -12,11 +13,19 @@ dependencies { api(project(":pillarbox-player")) implementation(libs.androidx.annotation) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.animation.core) implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.material.icons.core) implementation(libs.androidx.compose.material.icons.extended) + api(libs.androidx.compose.runtime) api(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.geometry) api(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.ui.unit) api(libs.androidx.lifecycle.viewmodel) api(libs.androidx.media3.common) implementation(libs.androidx.media3.exoplayer) @@ -29,5 +38,7 @@ dependencies { api(libs.srg.dataprovider.paging) api(libs.srg.dataprovider.retrofit) + debugImplementation(libs.androidx.compose.ui.tooling) + testImplementation(libs.junit) } diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/extension/ModifierExtensions.kt similarity index 97% rename from pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/extension/ModifierExtensions.kt index 73aa1913d..04e6f407f 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/extension/ModifierExtensions.kt @@ -2,7 +2,7 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.demo.tv.extension +package ch.srgssr.pillarbox.demo.shared.extension import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt new file mode 100644 index 000000000..4781aa512 --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt @@ -0,0 +1,466 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_inverseSurface +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_onSurface +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_primary +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_surfaceVariant +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_light_inverseSurface +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_light_onSurface +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_light_primary +import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_light_surfaceVariant + +/** + * Custom slider component that can be used on mobile devices as well as TV. + * + * @param value The current value of this slider. + * @param range The range of values supported by this slider. + * @param compactMode If `true`, the slider will be thinner. + * @param modifier The [Modifier] to apply to the layout. + * @param secondaryValue An optional second value to display on the slider. + * @param enabled Whether this slider is enabled. + * @param thumbColorEnabled The thumb color when the component is enabled. + * @param thumbColorDisabled The thumb color when the component is disabled. + * @param activeTrackColorEnabled The active track color when the component is enabled. + * @param activeTrackColorDisabled The active track color when the component is disabled. + * @param inactiveTrackColorEnabled The inactive track color when the component is enabled. + * @param inactiveTrackColorDisabled The inactive track color when the component is disabled. + * @param secondaryTrackColorEnabled The secondary track color when the component is enabled. + * @param secondaryTrackColorDisabled The secondary track color when the component is disabled. + * @param interactionSource The [MutableInteractionSource] representing the stream of [Interaction]s for this slider. + * You can create and pass in your own `remember`ed instance to observe [Interaction]s. + * @param onValueChange The action to perform whenever the slider value changes. + * @param onValueChangeFinished The action to perform when the slider value is done changing. + * @param onSeekBack The action to perform when seeking back. + * @param onSeekForward The action to perform when seeking forward. + */ +@Composable +fun PillarboxSlider( + value: Long, + range: LongRange, + compactMode: Boolean, + modifier: Modifier = Modifier, + secondaryValue: Long? = null, + enabled: Boolean = true, + thumbColorEnabled: Color, + thumbColorDisabled: Color, + activeTrackColorEnabled: Color, + activeTrackColorDisabled: Color, + inactiveTrackColorEnabled: Color, + inactiveTrackColorDisabled: Color, + secondaryTrackColorEnabled: Color = Color.Unspecified, + secondaryTrackColorDisabled: Color = Color.Unspecified, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onValueChange: (value: Long) -> Unit = {}, + onValueChangeFinished: () -> Unit = {}, + onSeekBack: () -> Unit = {}, + onSeekForward: () -> Unit = {}, +) { + PillarboxSliderInternal( + activeTrackWeight = value / range.last.toFloat(), + compactMode = compactMode, + modifier = modifier, + secondaryValueWeight = secondaryValue?.let { it / range.last.toFloat() }, + enabled = enabled, + thumbColorEnabled = thumbColorEnabled, + thumbColorDisabled = thumbColorDisabled, + activeTrackColorEnabled = activeTrackColorEnabled, + activeTrackColorDisabled = activeTrackColorDisabled, + inactiveTrackColorEnabled = inactiveTrackColorEnabled, + inactiveTrackColorDisabled = inactiveTrackColorDisabled, + secondaryTrackColorEnabled = secondaryTrackColorEnabled, + secondaryTrackColorDisabled = secondaryTrackColorDisabled, + interactionSource = interactionSource, + onSliderValueChange = { ratio -> + onValueChange((ratio * (range.last - range.start)).toLong()) + }, + onSliderValueChangeFinished = onValueChangeFinished, + onSeekBack = onSeekBack, + onSeekForward = onSeekForward, + ) +} + +/** + * Custom slider component that can be used on mobile devices as well as TV. + * + * @param value The current value of this slider. + * @param range The range of values supported by this slider. + * @param compactMode If `true`, the slider will be thinner. + * @param modifier The [Modifier] to apply to the layout. + * @param secondaryValue An optional second value to display on the slider. + * @param enabled Whether this slider is enabled. + * @param thumbColorEnabled The thumb color when the component is enabled. + * @param thumbColorDisabled The thumb color when the component is disabled. + * @param activeTrackColorEnabled The active track color when the component is enabled. + * @param activeTrackColorDisabled The active track color when the component is disabled. + * @param inactiveTrackColorEnabled The inactive track color when the component is enabled. + * @param inactiveTrackColorDisabled The inactive track color when the component is disabled. + * @param secondaryTrackColorEnabled The secondary track color when the component is enabled. + * @param secondaryTrackColorDisabled The secondary track color when the component is disabled. + * @param interactionSource The [MutableInteractionSource] representing the stream of [Interaction]s for this slider. + * You can create and pass in your own `remember`ed instance to observe [Interaction]s. + * @param onValueChange The action to perform whenever the slider value changes. + * @param onValueChangeFinished The action to perform when the slider value is done changing. + * @param onSeekBack The action to perform when seeking back. + * @param onSeekForward The action to perform when seeking forward. + */ +@Composable +fun PillarboxSlider( + value: Float, + range: ClosedRange, + compactMode: Boolean, + modifier: Modifier = Modifier, + secondaryValue: Float? = null, + enabled: Boolean = true, + thumbColorEnabled: Color, + thumbColorDisabled: Color, + activeTrackColorEnabled: Color, + activeTrackColorDisabled: Color, + inactiveTrackColorEnabled: Color, + inactiveTrackColorDisabled: Color, + secondaryTrackColorEnabled: Color = Color.Unspecified, + secondaryTrackColorDisabled: Color = Color.Unspecified, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onValueChange: (value: Float) -> Unit = {}, + onValueChangeFinished: () -> Unit = {}, + onSeekBack: () -> Unit = {}, + onSeekForward: () -> Unit = {}, +) { + PillarboxSliderInternal( + activeTrackWeight = value / range.endInclusive, + compactMode = compactMode, + modifier = modifier, + secondaryValueWeight = secondaryValue?.let { it / range.endInclusive }, + enabled = enabled, + thumbColorEnabled = thumbColorEnabled, + thumbColorDisabled = thumbColorDisabled, + activeTrackColorEnabled = activeTrackColorEnabled, + activeTrackColorDisabled = activeTrackColorDisabled, + inactiveTrackColorEnabled = inactiveTrackColorEnabled, + inactiveTrackColorDisabled = inactiveTrackColorDisabled, + secondaryTrackColorEnabled = secondaryTrackColorEnabled, + secondaryTrackColorDisabled = secondaryTrackColorDisabled, + interactionSource = interactionSource, + onSliderValueChange = { ratio -> + onValueChange(ratio * (range.endInclusive - range.start)) + }, + onSliderValueChangeFinished = onValueChangeFinished, + onSeekBack = onSeekBack, + onSeekForward = onSeekForward, + ) +} + +@Composable +private fun PillarboxSliderInternal( + activeTrackWeight: Float, + compactMode: Boolean, + modifier: Modifier = Modifier, + secondaryValueWeight: Float?, + enabled: Boolean, + thumbColorEnabled: Color, + thumbColorDisabled: Color, + activeTrackColorEnabled: Color, + activeTrackColorDisabled: Color, + inactiveTrackColorEnabled: Color, + inactiveTrackColorDisabled: Color, + secondaryTrackColorEnabled: Color, + secondaryTrackColorDisabled: Color, + interactionSource: MutableInteractionSource, + onSliderValueChange: (ratio: Float) -> Unit, + onSliderValueChangeFinished: () -> Unit, + onSeekBack: () -> Unit, + onSeekForward: () -> Unit, +) { + val seekBarHeight by animateDpAsState(targetValue = if (compactMode) 8.dp else 16.dp, label = "seek_bar_height") + val thumbColor by animateColorAsState(targetValue = if (enabled) thumbColorEnabled else thumbColorDisabled, label = "thumb_color") + + val animatedActiveTrackWeight by animateFloatAsState(targetValue = activeTrackWeight, label = "active_track_weight") + val activeTrackColor by animateColorAsState( + targetValue = if (enabled) activeTrackColorEnabled else activeTrackColorDisabled, + label = "active_track_color", + ) + + val inactiveTrackWeight by animateFloatAsState(targetValue = 1f - animatedActiveTrackWeight, label = "inactive_track_weight") + val inactiveTrackColor by animateColorAsState( + targetValue = if (enabled) inactiveTrackColorEnabled else inactiveTrackColorDisabled, + label = "inactive_track_color", + ) + + val secondaryTrackColor by animateColorAsState( + targetValue = if (enabled) secondaryTrackColorEnabled else secondaryTrackColorDisabled, + label = "secondary_track_color", + ) + + Row( + modifier = modifier + .height(seekBarHeight) + .clickToSlide( + interactionSource = interactionSource, + onSliderValueChange = onSliderValueChange, + onSliderValueChangeFinished = onSliderValueChangeFinished, + ) + .dragThumb( + interactionSource = interactionSource, + onSliderValueChange = onSliderValueChange, + onSliderValueChangeFinished = onSliderValueChangeFinished, + ), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Track( + weight = animatedActiveTrackWeight, + color = activeTrackColor, + ) + + Thumb( + color = thumbColor, + enabled = enabled, + onSeekBack = onSeekBack, + onSeekForward = onSeekForward, + ) + + if (inactiveTrackWeight > 0f) { + Box( + modifier = Modifier.weight(inactiveTrackWeight), + ) { + Track( + weight = 1f, + color = inactiveTrackColor, + ) + + if (secondaryValueWeight != null) { + Track( + weight = secondaryValueWeight / inactiveTrackWeight, + color = secondaryTrackColor, + ) + } + } + } + } +} + +private fun Modifier.clickToSlide( + interactionSource: MutableInteractionSource, + onSliderValueChange: (ratio: Float) -> Unit, + onSliderValueChangeFinished: () -> Unit, +): Modifier { + var pressInteraction: PressInteraction.Press? = null + + fun PointerInputScope.initPressInteraction(offset: Offset) { + if (pressInteraction == null) { + pressInteraction = PressInteraction.Press(offset) + .also { interactionSource.tryEmit(it) } + + onSliderValueChange(offset.x / size.width) + } + } + + return this then pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + initPressInteraction(offset) + + val nextInteraction = if (tryAwaitRelease()) { + PressInteraction::Release + } else { + PressInteraction::Cancel + } + + pressInteraction?.let { + interactionSource.emit(nextInteraction(it)) + pressInteraction = null + + onSliderValueChangeFinished() + } + }, + onTap = ::initPressInteraction, + ) + } +} + +private fun Modifier.dragThumb( + interactionSource: MutableInteractionSource, + onSliderValueChange: (ratio: Float) -> Unit, + onSliderValueChangeFinished: () -> Unit, +): Modifier { + var startInteraction: DragInteraction.Start? = null + + fun destroyStartInteraction(finalInteractionConstructor: (start: DragInteraction.Start) -> DragInteraction) { + startInteraction?.let { + interactionSource.tryEmit(finalInteractionConstructor(it)) + startInteraction = null + } + + onSliderValueChangeFinished() + } + + return this then Modifier.pointerInput(Unit) { + detectHorizontalDragGestures( + onDragStart = { offset -> + startInteraction = DragInteraction.Start() + .also { interactionSource.tryEmit(it) } + + onSliderValueChange(offset.x / size.width) + }, + onDragEnd = { + destroyStartInteraction(DragInteraction::Stop) + }, + onDragCancel = { + destroyStartInteraction(DragInteraction::Cancel) + }, + onHorizontalDrag = { change, _ -> + onSliderValueChange(change.position.x / size.width) + }, + ) + } +} + +@Composable +private fun Track( + weight: Float, + color: Color, + modifier: Modifier = Modifier, +) { + if (weight > 0f) { + Box( + modifier = modifier + .fillMaxHeight() + .fillMaxWidth(fraction = weight) + .background( + color = color, + shape = CircleShape, + ), + ) + } +} + +@Composable +private fun Thumb( + color: Color, + enabled: Boolean, + modifier: Modifier = Modifier, + onSeekBack: () -> Unit, + onSeekForward: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxHeight() + .width(8.dp) + .background( + color = color, + shape = CircleShape, + ) + .then( + if (enabled) { + Modifier + .focusable() + .onDpadEvent( + onLeft = { + onSeekBack() + true + }, + onRight = { + onSeekForward() + true + }, + ) + } else { + Modifier + } + ), + ) +} + +@Composable +@PreviewLightDark +private fun PillarboxSliderPreview( + @PreviewParameter(PillarboxSliderPreviewParameters::class) previewParameters: PreviewParameters, +) { + val isDark = isSystemInDarkTheme() + var progress by remember { + mutableLongStateOf(previewParameters.initialValue) + } + + PillarboxSlider( + value = progress, + range = 0L..100L, + compactMode = previewParameters.compactMode, + secondaryValue = previewParameters.secondaryValue, + enabled = previewParameters.enabled, + thumbColorEnabled = if (isDark) md_theme_dark_primary else md_theme_light_primary, + thumbColorDisabled = (if (isDark) md_theme_dark_onSurface else md_theme_light_onSurface).copy(alpha = 0.38f), + activeTrackColorEnabled = if (isDark) md_theme_dark_primary else md_theme_light_primary, + activeTrackColorDisabled = (if (isDark) md_theme_dark_onSurface else md_theme_light_onSurface).copy(alpha = 0.38f), + inactiveTrackColorEnabled = if (isDark) md_theme_dark_surfaceVariant else md_theme_light_surfaceVariant, + inactiveTrackColorDisabled = (if (isDark) md_theme_dark_onSurface else md_theme_light_onSurface).copy(alpha = 0.12f), + secondaryTrackColorEnabled = if (isDark) md_theme_dark_inverseSurface else md_theme_light_inverseSurface, + secondaryTrackColorDisabled = (if (isDark) md_theme_dark_inverseSurface else md_theme_light_inverseSurface).copy(alpha = 0.12f), + onValueChange = { progress = it }, + onSeekBack = { progress-- }, + onSeekForward = { progress++ }, + ) +} + +private class PreviewParameters( + val compactMode: Boolean, + val enabled: Boolean, + val initialValue: Long, + val secondaryValue: Long?, +) + +private class PillarboxSliderPreviewParameters : PreviewParameterProvider { + override val values = sequence { + listOf(false, true).forEach { compactMode -> + listOf(false, true).forEach { enabled -> + listOf(0L, 50L, 100L).forEach { initialValue -> + listOf(null, 10L).forEach { secondaryValue -> + yield( + PreviewParameters( + compactMode = compactMode, + enabled = enabled, + initialValue = initialValue, + secondaryValue = secondaryValue, + ) + ) + } + } + } + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt index d75128bd0..e664b1e13 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt @@ -27,8 +27,8 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.Text +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVSlider.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVSlider.kt deleted file mode 100644 index da0347b19..000000000 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVSlider.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.tv.ui.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.tv.material3.MaterialTheme -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent -import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme - -/** - * Slider component suited for use on TV. - * - * @param value The current value of this slider. - * @param range The range of values supported by this slider. - * @param compactMode If `true`, the slider will be thinner. - * @param modifier The [Modifier] to apply to the layout. - * @param enabled Whether or not this slider is enabled. - * @param onSeekBack The action to perform when seeking back. - * @param onSeekForward The action to perform when seeking forward. - */ -@Composable -fun TVSlider( - value: Long, - range: LongRange, - compactMode: Boolean, - modifier: Modifier = Modifier, - enabled: Boolean = true, - onSeekBack: () -> Unit, - onSeekForward: () -> Unit, -) { - val seekBarHeight by animateDpAsState(targetValue = if (compactMode) 8.dp else 16.dp, label = "seek_bar_height") - val thumbColor by animateColorAsState( - targetValue = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - label = "thumb_color", - ) - - val activeTrackWeight by animateFloatAsState(targetValue = value / range.last.toFloat(), label = "active_track_weight") - val activeTrackColor by animateColorAsState( - targetValue = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - label = "active_track_color", - ) - - val inactiveTrackWeight by animateFloatAsState(targetValue = 1f - activeTrackWeight, label = "inactive_track_weight") - val inactiveTrackColor by animateColorAsState( - targetValue = if (enabled) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - label = "inactive_track_color", - ) - - Row( - modifier = modifier.height(seekBarHeight), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Track( - weight = activeTrackWeight, - color = activeTrackColor, - ) - - Thumb( - color = thumbColor, - enabled = enabled, - onSeekBack = onSeekBack, - onSeekForward = onSeekForward, - ) - - Track( - weight = inactiveTrackWeight, - color = inactiveTrackColor, - ) - } -} - -@Composable -private fun RowScope.Track( - weight: Float, - color: Color, - modifier: Modifier = Modifier, -) { - if (weight > 0f) { - Box( - modifier = modifier - .fillMaxHeight() - .weight(weight) - .background( - color = color, - shape = CircleShape, - ), - ) - } -} - -@Composable -private fun Thumb( - color: Color, - enabled: Boolean, - modifier: Modifier = Modifier, - onSeekBack: () -> Unit, - onSeekForward: () -> Unit, -) { - Box( - modifier = modifier - .fillMaxHeight() - .width(8.dp) - .background( - color = color, - shape = CircleShape, - ) - .then( - if (enabled) { - Modifier - .focusable() - .onDpadEvent( - onLeft = { - onSeekBack() - true - }, - onRight = { - onSeekForward() - true - }, - ) - } else { - Modifier - } - ), - ) -} - -@Composable -@PreviewLightDark -private fun TVSliderPreview( - @PreviewParameter(TVSliderPreviewParameters::class) previewParameters: PreviewParameters, -) { - var progress by remember { - mutableLongStateOf(previewParameters.initialValue) - } - - PillarboxTheme { - TVSlider( - value = progress, - range = 0L..100L, - compactMode = previewParameters.compactMode, - enabled = previewParameters.enabled, - onSeekBack = { progress-- }, - onSeekForward = { progress++ }, - ) - } -} - -private class PreviewParameters( - val compactMode: Boolean, - val enabled: Boolean, - val initialValue: Long, -) - -private class TVSliderPreviewParameters : PreviewParameterProvider { - override val values = sequence { - listOf(false, true).forEach { compactMode -> - listOf(false, true).forEach { enabled -> - listOf(0L, 50L, 100L).forEach { initialValue -> - yield( - PreviewParameters( - compactMode = compactMode, - enabled = enabled, - initialValue = initialValue, - ) - ) - } - } - } - } -} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt index b995cc202..e735b2692 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt @@ -53,9 +53,9 @@ import androidx.tv.material3.Card import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.NavigationRoutes import ch.srgssr.pillarbox.demo.shared.ui.examples.ExamplesViewModel -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import coil.compose.AsyncImage diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt index d35b2e919..6626a1eef 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt @@ -79,6 +79,7 @@ import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth import ch.srg.dataProvider.integrationlayer.request.image.decorated import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.NavigationRoutes import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentList import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentListViewModel @@ -87,7 +88,6 @@ import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ContentListSecti import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListFactories import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections import ch.srgssr.pillarbox.demo.tv.R -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.player.PlayerActivity import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings @@ -99,7 +99,7 @@ import kotlin.time.Duration.Companion.seconds import ch.srgssr.pillarbox.demo.shared.R as sharedR /** - * Screen of the "Lists" tab of the demo app on TV. + * Screen of the "Lists" tab in the TV demo app. * * @param sections The list of section to display. * @param modifier The [Modifier] to apply to this screen. diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt index 81af64636..1e35d9124 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt @@ -47,9 +47,9 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import ch.srgssr.pillarbox.demo.shared.R +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent +import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider import ch.srgssr.pillarbox.demo.shared.ui.getFormatter -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent -import ch.srgssr.pillarbox.demo.tv.ui.components.TVSlider import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerError import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerPlaybackRow import ch.srgssr.pillarbox.demo.tv.ui.player.compose.settings.PlaybackSettingsDrawer @@ -72,7 +72,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds /** - * Tv player view + * TV player view * * @param player * @param modifier @@ -288,14 +288,18 @@ private fun PlayerTimeRow( color = Color.White, ) - TVSlider( + PillarboxSlider( value = positionMs, range = 0..durationMs, compactMode = compactMode, - modifier = Modifier - .onFocusChanged { compactMode = !it.hasFocus } - .padding(bottom = MaterialTheme.paddings.baseline), + modifier = Modifier.onFocusChanged { compactMode = !it.hasFocus }, enabled = availableCommands.canSeek(), + thumbColorEnabled = MaterialTheme.colorScheme.primary, + thumbColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + activeTrackColorEnabled = MaterialTheme.colorScheme.primary, + activeTrackColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + inactiveTrackColorEnabled = MaterialTheme.colorScheme.surfaceVariant, + inactiveTrackColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), onSeekBack = { onSeekProxy(positionMs - player.seekBackIncrement) }, onSeekForward = { onSeekProxy(positionMs + player.seekBackIncrement) }, ) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/controls/PlayerPlaybackRow.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/controls/PlayerPlaybackRow.kt index 79b69856d..814e40f61 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/controls/PlayerPlaybackRow.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/controls/PlayerPlaybackRow.kt @@ -24,7 +24,7 @@ import androidx.media3.common.Player import androidx.tv.material3.Icon import androidx.tv.material3.IconButton import androidx.tv.material3.MaterialTheme -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import ch.srgssr.pillarbox.player.extension.canSeekBack import ch.srgssr.pillarbox.player.extension.canSeekForward diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt index 3aa902cf2..be939d0fd 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt @@ -45,9 +45,9 @@ import ch.srg.dataProvider.integrationlayer.request.image.decorated import ch.srg.dataProvider.integrationlayer.request.parameters.Bu import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.bus -import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.lists.ListsSection import ch.srgssr.pillarbox.demo.tv.ui.player.PlayerActivity import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index d3c53a9d2..f95feb518 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -5,25 +5,25 @@ package ch.srgssr.pillarbox.demo.ui.player.controls import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderColors -import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.media3.common.C import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider import ch.srgssr.pillarbox.demo.shared.ui.getFormatter import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.player.PillarboxExoPlayer @@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.milliseconds * Creates a [ProgressTrackerState] to track manual changes made to the current media being player. * * @param player The [Player] to observe. - * @param smoothTracker `true` to use smooth tracking, ie. the media position is updated while tracking is in progress, `false` to update the + * @param smoothTracker `true` to use smooth tracking, i.e., the media position is updated while tracking is in progress, `false` to update the * media position only when tracking is finished. * @param coroutineScope */ @@ -67,7 +67,7 @@ fun rememberProgressTrackerState( * @param player The [Player] to observe. * @param modifier The [Modifier] to apply to the layout. * @param progressTracker The progress tracker. - * @param interactionSource The [Slider] interaction source. + * @param interactionSource The [PillarboxSlider] interaction source. */ @Composable fun PlayerTimeSlider( @@ -76,61 +76,50 @@ fun PlayerTimeSlider( progressTracker: ProgressTrackerState = rememberProgressTrackerState(player = player, smoothTracker = true), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { + val rememberedProgressTracker by rememberUpdatedState(progressTracker) val durationMs by player.durationAsState() val duration = remember(durationMs) { if (durationMs == C.TIME_UNSET) ZERO else durationMs.milliseconds } - val currentProgress by progressTracker.progress.collectAsState() + val currentProgress by rememberedProgressTracker.progress.collectAsState() val currentProgressPercent = currentProgress.inWholeMilliseconds / player.duration.coerceAtLeast(1).toFloat() val bufferPercentage by player.currentBufferedPercentageAsState() val availableCommands by player.availableCommandsAsState() val formatter = duration.getFormatter() + val isDragged by interactionSource.collectIsDraggedAsState() + val isPressed by interactionSource.collectIsPressedAsState() + val compactSlider = !isDragged && !isPressed + Row( modifier = modifier.padding(horizontal = MaterialTheme.paddings.mini), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.mini) ) { Text(text = formatter(currentProgress), color = Color.White) - Box(modifier = Modifier.weight(1f)) { - Slider( - value = bufferPercentage, - onValueChange = {}, - enabled = false, - colors = playerSecondaryColors(), - ) - Slider( - value = currentProgressPercent, - onValueChange = { percent -> - progressTracker.onChanged((percent * player.duration).toLong().milliseconds) - }, - onValueChangeFinished = progressTracker::onFinished, - enabled = availableCommands.canSeek(), - colors = playerPrimaryColors(), - interactionSource = interactionSource, - ) - } + PillarboxSlider( + value = currentProgressPercent, + range = 0f..1f, + compactMode = compactSlider, + modifier = Modifier.weight(1f), + secondaryValue = bufferPercentage, + enabled = availableCommands.canSeek(), + thumbColorEnabled = Color.White, + thumbColorDisabled = Color.White, + activeTrackColorEnabled = Color.Red, + activeTrackColorDisabled = Color.Red, + inactiveTrackColorEnabled = Color.White, + inactiveTrackColorDisabled = Color.White, + secondaryTrackColorEnabled = Color.Gray, + secondaryTrackColorDisabled = Color.Gray, + interactionSource = interactionSource, + onValueChange = { percent -> + progressTracker.onChanged((percent * player.duration).toLong().milliseconds) + }, + onValueChangeFinished = progressTracker::onFinished, + ) + Text(text = formatter(duration), color = Color.White) } } - -@Composable -private fun playerPrimaryColors(): SliderColors = SliderDefaults.colors( - thumbColor = Color.White, - activeTrackColor = Color.Red, - activeTickColor = Color.Transparent, - inactiveTrackColor = Color.Transparent, - inactiveTickColor = Color.Transparent, -) - -@Composable -private fun playerSecondaryColors(): SliderColors = SliderDefaults.colors( - thumbColor = Color.Transparent, - activeTrackColor = Color.Transparent, - inactiveTrackColor = Color.Transparent, - inactiveTickColor = Color.Transparent, - disabledThumbColor = Color.Transparent, - disabledActiveTrackColor = Color.Gray, - disabledInactiveTrackColor = Color.White -) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt index d620d27df..06ebcf3eb 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt @@ -4,7 +4,9 @@ */ package ch.srgssr.pillarbox.demo.ui.showcases.misc +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -13,10 +15,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.systemGestureExclusion +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -29,17 +33,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.di.PlayerModule +import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.ui.ScaleMode import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface /** * Resizable player demo - * The view allow to resize the player view and changing the scale mode + * The view allows resizing the player view and changing the scale mode */ @Composable fun ResizablePlayerShowcase() { @@ -67,64 +73,67 @@ fun ResizablePlayerShowcase() { } @Composable +@OptIn(ExperimentalMaterial3Api::class) private fun AdaptivePlayer(player: Player, modifier: Modifier = Modifier) { - var resizeMode by remember { - mutableStateOf(ScaleMode.Fit) - } - var widthPercent by remember { - mutableFloatStateOf(1f) - } - var heightPercent by remember { - mutableFloatStateOf(1f) - } - BoxWithConstraints(modifier = modifier.padding(MaterialTheme.paddings.baseline)) { + var resizeMode by remember { mutableStateOf(ScaleMode.Fit) } + val (widthPercent, setWidthPercent) = remember { mutableFloatStateOf(1f) } + val (heightPercent, setHeightPercent) = remember { mutableFloatStateOf(1f) } + + BoxWithConstraints(modifier = modifier) { + val playerWidth by animateDpAsState(targetValue = maxWidth * widthPercent, label = "player_width") + val playerHeight by animateDpAsState(targetValue = maxHeight * heightPercent, label = "player_height") + Box( - modifier = Modifier - .size(maxWidth * widthPercent, maxHeight * heightPercent), - contentAlignment = Alignment.Center + modifier = Modifier.size(width = playerWidth, height = playerHeight), + contentAlignment = Alignment.Center, ) { PlayerSurface( modifier = Modifier .matchParentSize() - .background(color = Color.Black), + .background(Color.Black), player = player, displayDebugView = true, contentAlignment = Alignment.Center, - scaleMode = resizeMode + scaleMode = resizeMode, ) } + Column( modifier = Modifier .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.background.copy(0.5f)) - .align(Alignment.BottomStart) + .background(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f)) + .padding(MaterialTheme.paddings.baseline) + .align(Alignment.BottomStart), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small), ) { - SliderWithLabel(label = "W: ", value = widthPercent, onValueChange = { widthPercent = it }) - SliderWithLabel(label = "H :", value = heightPercent, onValueChange = { heightPercent = it }) - Row { - for (mode in ScaleMode.entries) { - RadioButtonWithLabel(label = mode.name, selected = mode == resizeMode) { - resizeMode = mode - } + SliderWithLabel( + label = "W:", + value = widthPercent, + onValueChange = setWidthPercent, + ) + + SliderWithLabel( + label = "H:", + value = heightPercent, + onValueChange = setHeightPercent, + ) + + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + ) { + ScaleMode.entries.forEachIndexed { index, mode -> + SegmentedButton( + selected = mode == resizeMode, + onClick = { resizeMode = mode }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = ScaleMode.entries.size), + label = { Text(mode.name) }, + ) } } } } } -@Composable -private fun RadioButtonWithLabel( - modifier: Modifier = Modifier, - label: String, - selected: Boolean, - onClick: (() -> Unit) -) { - Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { - RadioButton(selected = selected, onClick = onClick) - Text(text = label, style = MaterialTheme.typography.labelMedium) - } -} - @Composable private fun SliderWithLabel( modifier: Modifier = Modifier, @@ -132,14 +141,27 @@ private fun SliderWithLabel( value: Float, onValueChange: (Float) -> Unit ) { - Row(modifier) { - Text(text = label) - Slider( - value = value, onValueChange = onValueChange, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.secondary, - activeTrackColor = MaterialTheme.colorScheme.secondaryContainer - ) + Row( + modifier = modifier.systemGestureExclusion(), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + fontFamily = FontFamily.Monospace, + ) + + PillarboxSlider( + value = value, + range = 0f..1f, + compactMode = false, + thumbColorEnabled = MaterialTheme.colorScheme.primary, + thumbColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + activeTrackColorEnabled = MaterialTheme.colorScheme.primary, + activeTrackColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + inactiveTrackColorEnabled = MaterialTheme.colorScheme.surfaceVariant, + inactiveTrackColorDisabled = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + onValueChange = onValueChange, ) } }