From 2b9693de69cd7639354cfa7b18fe1b289791875e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Tue, 30 Apr 2024 17:01:43 +0200 Subject: [PATCH 1/4] Fix toggling player controls --- .../demo/tv/extension/ModifierExtensions.kt | 4 ++- .../demo/tv/ui/player/compose/PlayerView.kt | 2 ++ .../ui/widget/DelayedVisibilityState.kt | 31 +++++-------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt index 69bb0ae64..73aa1913d 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.input.key.type * This [Modifier] allows you to define actions to perform when a button of the D-pad or the back button is press. Each action returns a [Boolean] * to indicate if the event was handled or not. * + * @param eventType The event type to check before calling the provided actions. * @param onLeft The action to perform when the left button is press. * @param onUp The action to perform when the up button is press. * @param onRight The action to perform when the right button is press. @@ -23,6 +24,7 @@ import androidx.compose.ui.input.key.type * @param onBack The action to perform when the back button is press. */ fun Modifier.onDpadEvent( + eventType: KeyEventType = KeyEventType.KeyDown, onLeft: () -> Boolean = { false }, onUp: () -> Boolean = { false }, onRight: () -> Boolean = { false }, @@ -31,7 +33,7 @@ fun Modifier.onDpadEvent( onBack: () -> Boolean = { false } ): Modifier { return onPreviewKeyEvent { - if (it.type == KeyEventType.KeyDown) { + if (it.type == eventType) { when (it.key) { Key.DirectionLeft, Key.SystemNavigationLeft, 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 bb0dd77d9..547c5c2dc 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 @@ -24,6 +24,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.res.stringResource import androidx.media3.common.Player import androidx.tv.material3.Button @@ -88,6 +89,7 @@ fun PlayerView( modifier = Modifier .fillMaxSize() .onDpadEvent( + eventType = KeyEventType.KeyUp, onEnter = { visibilityState.show() true diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt index 89dd9be63..9ec74c844 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt @@ -50,25 +50,15 @@ class DelayedVisibilityState internal constructor( initialVisible: Boolean = true, initialDuration: Duration = DefaultDuration ) { - internal var state by mutableStateOf(DelayedVisibility(initialVisible, initialDuration)) - /** * Visible */ - var isVisible: Boolean - get() = state.visible - set(value) = setVisible(visible = value, duration = duration) + var isVisible by mutableStateOf(initialVisible) /** * Duration */ - var duration: Duration - get() = state.duration - set(value) = setVisible(visible = isVisible, duration = value) - - private fun setVisible(visible: Boolean, duration: Duration = DefaultDuration) { - state = DelayedVisibility(visible, duration) - } + var duration by mutableStateOf(initialDuration) /** * Toggle @@ -105,11 +95,6 @@ class DelayedVisibilityState internal constructor( return duration < INFINITE && duration > ZERO } - internal class DelayedVisibility( - val visible: Boolean = true, - val duration: Duration = DefaultDuration - ) - companion object { /** * Default duration @@ -183,13 +168,11 @@ fun Modifier.toggleable( * @param delayedVisibilityState the delayed visibility state to link */ fun Modifier.maintainVisibleOnFocus(delayedVisibilityState: DelayedVisibilityState): Modifier { - return this.then( - Modifier.onFocusChanged { - if (it.isFocused) { - delayedVisibilityState.show() - } + return onFocusChanged { + if (it.isFocused) { + delayedVisibilityState.show() } - ) + } } /** @@ -266,7 +249,7 @@ fun rememberDelayedVisibilityState( delayedVisibilityState.isVisible = visible } - LaunchedEffect(delayedVisibilityState.state) { + LaunchedEffect(delayedVisibilityState.isVisible, delayedVisibilityState.duration) { if (delayedVisibilityState.isVisible && delayedVisibilityState.isAutoHideEnabled()) { delay(delayedVisibilityState.duration) delayedVisibilityState.hide() From cfe77b542250a5907ad6a7f8c2875b5ac92f8bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 1 May 2024 09:04:57 +0200 Subject: [PATCH 2/4] Keep the controls visible while they are being used --- .../player/compose/controls/PlayerPlaybackRow.kt | 15 ++++++++++++++- .../pillarbox/ui/widget/DelayedVisibilityState.kt | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) 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 71fb7e5db..13966bc27 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 @@ -25,6 +25,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api 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.tv.ui.theme.paddings import ch.srgssr.pillarbox.player.extension.canSeekBack import ch.srgssr.pillarbox.player.extension.canSeekForward @@ -50,13 +51,25 @@ fun PlayerPlaybackRow( ) { val isPlaying by player.isPlayingAsState() val focusRequester = remember { FocusRequester() } + val resetAutoHideCallback = remember { + { + state.resetAutoHide() + false + } + } + LaunchedEffect(state.isVisible) { if (state.isVisible) { focusRequester.requestFocus() } } + Row( - modifier = modifier, + modifier = modifier.onDpadEvent( + onLeft = resetAutoHideCallback, + onRight = resetAutoHideCallback, + onEnter = resetAutoHideCallback, + ), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline), ) { val availableCommands by player.availableCommandsAsState() diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt index 9ec74c844..29b4f7c36 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt @@ -50,6 +50,9 @@ class DelayedVisibilityState internal constructor( initialVisible: Boolean = true, initialDuration: Duration = DefaultDuration ) { + internal var autoHideResetTrigger by mutableStateOf(false) + private set + /** * Visible */ @@ -85,7 +88,14 @@ class DelayedVisibilityState internal constructor( * Disable auto hide */ fun disableAutoHide() { - duration = ZERO + duration = DisabledDuration + } + + /** + * Reset the auto hide countdown + */ + fun resetAutoHide() { + autoHideResetTrigger = !autoHideResetTrigger } /** @@ -249,7 +259,7 @@ fun rememberDelayedVisibilityState( delayedVisibilityState.isVisible = visible } - LaunchedEffect(delayedVisibilityState.isVisible, delayedVisibilityState.duration) { + LaunchedEffect(delayedVisibilityState.isVisible, delayedVisibilityState.duration, delayedVisibilityState.autoHideResetTrigger) { if (delayedVisibilityState.isVisible && delayedVisibilityState.isAutoHideEnabled()) { delay(delayedVisibilityState.duration) delayedVisibilityState.hide() From 1758baab3cb3f87b16d6b525f769eda350d8e62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 1 May 2024 10:04:39 +0200 Subject: [PATCH 3/4] Redesign chapter display --- .../tv/ui/player/compose/MediaMetadataView.kt | 53 +++++++++++++++---- .../demo/tv/ui/player/compose/PlayerView.kt | 4 +- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/MediaMetadataView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/MediaMetadataView.kt index a290d9d75..b10e1ad1d 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/MediaMetadataView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/MediaMetadataView.kt @@ -4,16 +4,21 @@ */ package ch.srgssr.pillarbox.demo.tv.ui.player.compose +import android.net.Uri import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow @@ -24,6 +29,7 @@ import androidx.media3.common.MediaMetadata import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import coil.compose.AsyncImage @@ -39,18 +45,33 @@ fun MediaMetadataView( mediaMetadata: MediaMetadata, modifier: Modifier = Modifier, ) { - Row(modifier.background(color = Color.Black)) { + Row( + modifier = modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black), + ), + ), + verticalAlignment = Alignment.Bottom, + ) { AsyncImage( modifier = Modifier + .padding(MaterialTheme.paddings.small) + .clip(RoundedCornerShape(MaterialTheme.paddings.small)) .width(200.dp) .aspectRatio(16 / 9f), contentScale = ContentScale.Fit, model = mediaMetadata.artworkUri, contentDescription = null, ) + Column( - verticalArrangement = Arrangement.Bottom, - modifier = Modifier.padding(MaterialTheme.paddings.mini) + modifier = Modifier.padding( + start = MaterialTheme.paddings.mini, + top = MaterialTheme.paddings.small, + end = 72.dp, // baseline + 56dp to not overlap with the settings button + bottom = MaterialTheme.paddings.small, + ) ) { Text( text = mediaMetadata.title?.toString() ?: "No title", @@ -59,11 +80,12 @@ fun MediaMetadataView( maxLines = 1, overflow = TextOverflow.Ellipsis, ) + mediaMetadata.description?.let { Text( text = mediaMetadata.description.toString(), color = Color.White, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -72,9 +94,22 @@ fun MediaMetadataView( } } -@Preview(device = Devices.TV_1080p) @Composable +@Preview(device = Devices.TV_1080p) +@Suppress("MaximumLineLength", "MaxLineLength") private fun MediaMetadataPreview() { - val mediaMetadata = MediaMetadata.Builder().setTitle("Title").setDescription("Description").build() - MediaMetadataView(mediaMetadata = mediaMetadata, modifier = Modifier.fillMaxSize()) + PillarboxTheme { + val mediaMetadata = MediaMetadata.Builder() + .setTitle("Title") + .setDescription("Description") + .setArtworkUri(Uri.parse("https://cdn.prod.swi-services.ch/video-delivery/images/14e4562f-725d-4e41-a200-7fcaa77df2fe/5rwf1Bq_m3GC5secOZcIcgbbrbZPf4nI/16x9)")) + .build() + + MediaMetadataView( + mediaMetadata = mediaMetadata, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) + } } 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 547c5c2dc..904f91397 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 @@ -116,7 +116,7 @@ fun PlayerView( currentChapter?.let { MediaMetadataView( modifier = Modifier - .fillMaxWidth(0.5f) + .fillMaxWidth() .wrapContentHeight() .align(Alignment.BottomStart), mediaMetadata = it.mediaMetadata @@ -151,7 +151,7 @@ fun PlayerView( val currentMediaMetadata by player.currentMediaMetadataAsState() MediaMetadataView( modifier = Modifier - .fillMaxWidth(0.5f) + .fillMaxWidth() .wrapContentHeight() .align(Alignment.BottomStart), mediaMetadata = currentChapter?.mediaMetadata ?: currentMediaMetadata From 9acc80ae77930724a669bd2d02564b1892cb8d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 1 May 2024 11:15:18 +0200 Subject: [PATCH 4/4] Animate chapter change --- .../demo/tv/ui/player/compose/PlayerView.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 904f91397..62bc79f5a 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 @@ -5,9 +5,13 @@ package ch.srgssr.pillarbox.demo.tv.ui.player.compose import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -149,13 +153,19 @@ fun PlayerView( ) val currentMediaMetadata by player.currentMediaMetadataAsState() - MediaMetadataView( + AnimatedContent( + targetState = currentChapter?.mediaMetadata ?: currentMediaMetadata, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .align(Alignment.BottomStart), - mediaMetadata = currentChapter?.mediaMetadata ?: currentMediaMetadata - ) + transitionSpec = { + slideInHorizontally { it } + .togetherWith(slideOutHorizontally { -it }) + } + ) { mediaMetadata -> + MediaMetadataView(mediaMetadata) + } IconButton( onClick = { drawerState.setValue(DrawerValue.Open) },