From 7760ce9456d13ad2353e545d0eabb30a85aed432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 6 Oct 2023 09:30:05 +0200 Subject: [PATCH] 259 compose toggleable view for tv and mobile (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Samuel Défago --- gradle/libs.versions.toml | 2 +- .../demo/tv/player/compose/TvPlaybackRow.kt | 16 +- .../demo/tv/player/compose/TvPlayerView.kt | 41 +-- .../demo/ui/player/DemoPlaybackControls.kt | 46 --- .../pillarbox/demo/ui/player/DemoPlayer.kt | 3 +- .../demo/ui/player/SimplePlayerView.kt | 100 +++++- .../ui/player/controls/PlayingControls.kt | 107 ------ .../ui/player/playlist/PlaylistPlayerView.kt | 9 +- .../ui/showcases/multiplayer/MultiPlayer.kt | 20 +- .../pillarbox/player/PlayerCallbackFlow.kt | 13 + .../java/ch/srgssr/pillarbox/ui/ToggleView.kt | 308 ------------------ .../pillarbox/ui/extension/DPadExtensions.kt | 87 +++++ .../ui/layout/DelayedVisibilityState.kt | 279 ++++++++++++++++ .../pillarbox/ui/layout/ToggleableBox.kt | 160 +++++++++ 14 files changed, 657 insertions(+), 534 deletions(-) delete mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlaybackControls.kt delete mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayingControls.kt delete mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/DelayedVisibilityState.kt create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/ToggleableBox.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a650a341..31a1cf793 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.33.0-alpha" activityCompose = "1.7.2" -androidGradlePlugin = "8.1.1" +androidGradlePlugin = "8.1.2" dataProvider = "0.5.0" kotlinPlugin = "1.9.0" detekt = "1.22.0" # https://github.com/detekt/detekt diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt index 593d3eb98..fcb9572d4 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt @@ -30,25 +30,28 @@ import ch.srgssr.pillarbox.player.canSeekToNext import ch.srgssr.pillarbox.player.canSeekToPrevious import ch.srgssr.pillarbox.ui.availableCommandsAsState import ch.srgssr.pillarbox.ui.isPlayingAsState +import ch.srgssr.pillarbox.ui.layout.DelayedVisibilityState /** * Tv playback row * * @param player + * @param state * @param modifier */ @OptIn(ExperimentalTvMaterial3Api::class) @Composable fun TvPlaybackRow( player: Player, + state: DelayedVisibilityState, modifier: Modifier = Modifier, ) { val isPlaying = player.isPlayingAsState() - val focusRequester = remember { - FocusRequester() - } - LaunchedEffect(Unit) { - focusRequester.requestFocus() + val focusRequester = remember { FocusRequester() } + LaunchedEffect(state.isVisible) { + if (state.isVisible) { + focusRequester.requestFocus() + } } Row( modifier = modifier, @@ -75,8 +78,7 @@ fun TvPlaybackRow( } IconButton( - modifier = Modifier - .focusRequester(focusRequester), + modifier = Modifier.focusRequester(focusRequester), onClick = { player.playWhenReady = !player.playWhenReady }, diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt index 8c9f3cc3b..bef366990 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt @@ -4,7 +4,6 @@ */ package ch.srgssr.pillarbox.demo.tv.player.compose -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -12,9 +11,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.media3.common.Player import ch.srgssr.pillarbox.ui.PlayerSurface -import ch.srgssr.pillarbox.ui.ToggleView -import ch.srgssr.pillarbox.ui.rememberToggleState -import kotlin.time.Duration.Companion.seconds +import ch.srgssr.pillarbox.ui.layout.ToggleableBox +import ch.srgssr.pillarbox.ui.layout.rememberDelayedVisibilityState /** * Tv player view @@ -27,27 +25,20 @@ fun TvPlayerView( player: Player, modifier: Modifier = Modifier ) { - Box(modifier = modifier) { - PlayerSurface( - player = player, - modifier = Modifier.fillMaxSize() - ) { - val toggleState = rememberToggleState(visible = true, duration = 4.seconds) - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .clickable { - toggleState.toggleVisible() - } - ) { - ToggleView(toggleState = toggleState) { - TvPlaybackRow( - player = player, - modifier = Modifier.matchParentSize() - ) - } + val visibilityState = rememberDelayedVisibilityState(player = player, visible = true) + ToggleableBox( + modifier = modifier, + visibilityState = visibilityState, + toggleableContent = { + Box(modifier = Modifier, contentAlignment = Alignment.Center) { + TvPlaybackRow(player = player, state = visibilityState) } + }, + content = { + PlayerSurface( + player = player, + modifier = Modifier.fillMaxSize() + ) } - } + ) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlaybackControls.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlaybackControls.kt deleted file mode 100644 index 6bfd7f7ee..000000000 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlaybackControls.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023. SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.ui.player - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.media3.common.Player -import ch.srgssr.pillarbox.demo.ui.player.controls.PlayingControls - -/** - * Demo playback controls - * - * @param player The [StatefulPlayer] to observe. - * @param modifier The modifier to be applied to the layout. - * @param controlVisible The control visibility. - * @param autoHideEnabled To enable or not auto hide of the controls. - * @param fullScreenEnabled The fullscreen state. - * @param fullScreenClicked The fullscreen button action. If null no button. - * @param pictureInPictureClicked The picture in picture button action. If null no button. - * @param optionClicked action when settings is clicked - */ -@Composable -fun DemoPlaybackControls( - player: Player, - modifier: Modifier = Modifier, - controlVisible: Boolean = true, - autoHideEnabled: Boolean = true, - fullScreenEnabled: Boolean = false, - fullScreenClicked: ((Boolean) -> Unit)? = null, - pictureInPictureClicked: (() -> Unit)? = null, - optionClicked: (() -> Unit)? = null -) { - if (controlVisible) { - PlayingControls( - modifier = modifier, - player = player, - autoHideEnabled = autoHideEnabled, - fullScreenEnabled = fullScreenEnabled, - fullScreenClicked = fullScreenClicked, - pictureInPictureClicked = pictureInPictureClicked, - optionClicked = optionClicked - ) - } -} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayer.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayer.kt index 7c60484ea..0dd2aa3e7 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayer.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayer.kt @@ -109,7 +109,8 @@ private fun PlayerContent( SimplePlayerView( modifier = Modifier.fillMaxSize(), player = player, - controlVisible = !pictureInPicture, + controlsToggleable = !pictureInPicture, + controlsVisible = !pictureInPicture, fullScreenEnabled = fullScreenState, fullScreenClicked = fullScreenToggle, pictureInPictureClicked = pictureInPictureClick, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerView.kt index f6226b526..4f8737627 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerView.kt @@ -5,8 +5,13 @@ package ch.srgssr.pillarbox.demo.ui.player import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -17,14 +22,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalView import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerBottomToolbar import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerError +import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerPlaybackRow +import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerTimeSlider import ch.srgssr.pillarbox.ui.ScaleMode +import ch.srgssr.pillarbox.ui.currentMediaMetadataAsState import ch.srgssr.pillarbox.ui.hasMediaItemsAsState import ch.srgssr.pillarbox.ui.isPlayingAsState +import ch.srgssr.pillarbox.ui.layout.ToggleableBox +import ch.srgssr.pillarbox.ui.layout.rememberDelayedVisibilityState +import ch.srgssr.pillarbox.ui.playbackStateAsState import ch.srgssr.pillarbox.ui.playerErrorAsState /** @@ -32,7 +45,8 @@ import ch.srgssr.pillarbox.ui.playerErrorAsState * * @param player The [Player] to observe. * @param modifier The modifier to be applied to the layout. - * @param controlVisible The control visibility. + * @param controlsVisible The control visibility. + * @param controlsToggleable The controls are toggleable. * @param fullScreenEnabled The fullscreen state. * @param fullScreenClicked The fullscreen button action. If null no button. * @param pictureInPictureClicked The picture in picture button action. If null no button. @@ -42,7 +56,8 @@ import ch.srgssr.pillarbox.ui.playerErrorAsState fun SimplePlayerView( player: Player, modifier: Modifier = Modifier, - controlVisible: Boolean = true, + controlsVisible: Boolean = true, + controlsToggleable: Boolean = true, fullScreenEnabled: Boolean = false, fullScreenClicked: ((Boolean) -> Unit)? = null, pictureInPictureClicked: (() -> Unit)? = null, @@ -64,13 +79,13 @@ fun SimplePlayerView( ) } } - return } var pinchScaleMode by remember { mutableStateOf(ScaleMode.Fit) } - val surfaceModifier = if (fullScreenEnabled) { + + val scalableModifier = if (fullScreenEnabled) { modifier.then( Modifier.pointerInput(pinchScaleMode) { var lastZoomValue = 1.0f @@ -83,22 +98,71 @@ fun SimplePlayerView( } else { modifier } - LocalView.current.keepScreenOn = player.isPlayingAsState() - DemoPlayerSurface( - modifier = surfaceModifier, + val isPlaying = player.isPlayingAsState() + LocalView.current.keepScreenOn = isPlaying + val interactionSource = remember { + MutableInteractionSource() + } + val isDragged = interactionSource.collectIsDraggedAsState().value + val visibilityState = rememberDelayedVisibilityState( player = player, - scaleMode = if (fullScreenEnabled) pinchScaleMode else ScaleMode.Fit + autoHideEnabled = !isDragged, + visible = controlsVisible + ) + + ToggleableBox( + modifier = scalableModifier, + toggleable = controlsToggleable, + visibilityState = visibilityState, + toggleableContent = { + val mediaMetadata = player.currentMediaMetadataAsState() + Box( + modifier = Modifier + .fillMaxSize() + .drawBehind { + drawRect(color = Color.Black.copy(0.5f)) + }, + contentAlignment = Alignment.Center + ) { + Text( + modifier = Modifier.align(Alignment.TopStart), + text = mediaMetadata.title.toString(), color = Color.Gray + ) + PlayerPlaybackRow( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + player = player + ) + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + ) { + PlayerTimeSlider( + modifier = Modifier, + player = player, + interactionSource = interactionSource + ) + PlayerBottomToolbar( + modifier = Modifier + .fillMaxWidth(), + fullScreenEnabled = fullScreenEnabled, + fullScreenClicked = fullScreenClicked, + pictureInPictureClicked = pictureInPictureClicked, + optionClicked = optionClicked + ) + } + } + } ) { - DemoPlaybackControls( - modifier = Modifier - .matchParentSize(), + DemoPlayerSurface( + modifier = Modifier.fillMaxSize(), player = player, - controlVisible = controlVisible, - autoHideEnabled = true, - fullScreenEnabled = fullScreenEnabled, - fullScreenClicked = fullScreenClicked, - pictureInPictureClicked = pictureInPictureClicked, - optionClicked = optionClicked - ) + scaleMode = if (fullScreenEnabled) pinchScaleMode else ScaleMode.Fit + ) { + if (player.playbackStateAsState() == Player.STATE_BUFFERING) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White) + } + } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayingControls.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayingControls.kt deleted file mode 100644 index 87f2b393d..000000000 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayingControls.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2023. SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.ui.player.controls - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.Role -import androidx.media3.common.Player -import ch.srgssr.pillarbox.ui.ToggleView -import ch.srgssr.pillarbox.ui.currentMediaMetadataAsState -import ch.srgssr.pillarbox.ui.playbackStateAsState -import ch.srgssr.pillarbox.ui.rememberToggleState - -/** - * Playing controls - * - * Playing controls - * The view to display when something is ready to play. - * - * @param player The [Player] to observe. - * @param modifier The modifier to be applied to the layout. - * @param controlVisible The control visibility. - * @param autoHideEnabled To enable or not auto hide of the controls. - * @param fullScreenEnabled The fullscreen state. - * @param fullScreenClicked The fullscreen button action. If null no button. - * @param pictureInPictureClicked The picture in picture button action. If null no button. - * @param optionClicked action when settings is clicked - */ -@Composable -fun PlayingControls( - player: Player, - modifier: Modifier = Modifier, - controlVisible: Boolean = true, - autoHideEnabled: Boolean = true, - fullScreenEnabled: Boolean = false, - fullScreenClicked: ((Boolean) -> Unit)? = null, - pictureInPictureClicked: (() -> Unit)? = null, - optionClicked: (() -> Unit)? = null -) { - val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } - val toggleState = rememberToggleState( - player = player, - visible = controlVisible, - autoHideEnabled = autoHideEnabled, - interactionSource = interactionSource - ) - Box( - modifier = modifier.clickable(role = Role.Switch, onClickLabel = "Toggle controls", onClick = toggleState::toggleVisible) - ) { - if (player.playbackStateAsState() == Player.STATE_BUFFERING) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White) - } - - ToggleView( - modifier = Modifier.fillMaxSize(), - toggleState = toggleState - ) { - val mediaMetadata = player.currentMediaMetadataAsState() - Box(modifier = Modifier.matchParentSize()) { - Text(modifier = Modifier.align(Alignment.TopStart), text = mediaMetadata.title.toString(), color = Color.Gray) - PlayerPlaybackRow( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - player = player - ) - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .align(Alignment.BottomCenter), - horizontalAlignment = Alignment.Start - ) { - PlayerTimeSlider( - modifier = Modifier - .fillMaxWidth(), - player = player, - interactionSource = interactionSource - ) - PlayerBottomToolbar( - modifier = modifier - .fillMaxWidth() - .align(Alignment.Start), - fullScreenEnabled = fullScreenEnabled, - fullScreenClicked = fullScreenClicked, - pictureInPictureClicked = pictureInPictureClicked, - optionClicked = optionClicked - ) - } - } - } - } -} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistPlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistPlayerView.kt index f2a2fcbab..d81f7c9d6 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistPlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistPlayerView.kt @@ -21,7 +21,8 @@ import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerView * * @param player The [Player] to observe. * @param modifier The modifier to be applied to the layout. - * @param controlVisible The control visibility. + * @param controlsVisible The control visibility. + * @param controlsToggleable The controls are toggleable. * @param fullScreenEnabled The fullscreen state. * @param fullScreenClicked The fullscreen button action. If null no button. * @param pictureInPictureClicked The picture in picture button action. If null no button. @@ -31,7 +32,8 @@ import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerView fun PlaylistPlayerView( player: Player, modifier: Modifier = Modifier, - controlVisible: Boolean = true, + controlsVisible: Boolean = true, + controlsToggleable: Boolean = true, fullScreenEnabled: Boolean = false, fullScreenClicked: ((Boolean) -> Unit)? = null, pictureInPictureClicked: (() -> Unit)? = null, @@ -50,7 +52,8 @@ fun PlaylistPlayerView( SimplePlayerView( modifier = playerModifier, player = player, - controlVisible = controlVisible, + controlsToggleable = controlsToggleable, + controlsVisible = controlsVisible, fullScreenEnabled = fullScreenEnabled, fullScreenClicked = fullScreenClicked, pictureInPictureClicked = pictureInPictureClicked, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/multiplayer/MultiPlayer.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/multiplayer/MultiPlayer.kt index 505820010..235e308d6 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/multiplayer/MultiPlayer.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/multiplayer/MultiPlayer.kt @@ -23,10 +23,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player -import ch.srgssr.pillarbox.demo.ui.player.DemoPlayerSurface -import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerError -import ch.srgssr.pillarbox.demo.ui.player.controls.PlayingControls -import ch.srgssr.pillarbox.ui.playerErrorAsState +import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerView /** * Demo of 2 player swapping view @@ -84,20 +81,7 @@ fun MultiPlayer() { @Composable private fun PlayerView(player: Player, modifier: Modifier) { - val playbackError = player.playerErrorAsState() - if (playbackError != null) { - PlayerError( - modifier = modifier, - playerError = playbackError - ) { - player.prepare() - player.play() - } - return - } - DemoPlayerSurface(modifier = modifier, player = player) { - PlayingControls(modifier = Modifier.matchParentSize(), player = player, autoHideEnabled = false) - } + SimplePlayerView(modifier = modifier, player = player, controlsToggleable = false) } private const val AspectRatio = 16 / 9f diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt index 6667fafb6..d0126cc8f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt @@ -306,6 +306,19 @@ fun Player.getCurrentTracksAsFlow(): Flow = callbackFlow { addPlayerListener(player = this@getCurrentTracksAsFlow, listener) } +/** + * Play when ready as flow [Player.getPlayWhenReady] + */ +fun Player.playWhenReadyAsFlow(): Flow = callbackFlow { + val listener = object : Player.Listener { + override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { + trySend(playWhenReady) + } + } + trySend(playWhenReady) + addPlayerListener(this@playWhenReadyAsFlow, listener) +} + private suspend fun ProducerScope.addPlayerListener(player: Player, listener: Listener) { player.addListener(listener) awaitClose { diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt deleted file mode 100644 index 6b888d695..000000000 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (c) 2023. SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.media3.common.Player -import kotlinx.coroutines.delay -import kotlin.time.Duration -import kotlin.time.Duration.Companion.ZERO -import kotlin.time.Duration.Companion.seconds - -/** - * Default auto hide delay - */ -val DefaultAutoHideDelay = 3.seconds - -/** - * Toggle state - * - * @property duration The duration after the view becomes hidden. - * @param visible defines whether the content should be visible. - */ -@Suppress("UndocumentedPublicClass", "OutdatedDocumentation") -@Stable -class ToggleState internal constructor( - visible: Boolean, - private val duration: Duration = DefaultAutoHideDelay -) { - private val _visibleDuration = mutableStateOf(duration) - - /** - * Visible state - */ - val visibleState = MutableTransitionState(initialState = visible) - - /** - * Visible duration - */ - val visibleDuration = _visibleDuration - - internal val userInteracting = mutableStateOf(false) - - /** - * Auto hide enable - * - * @param enable Enable auto hide after [duration]. - */ - fun setAutoHideEnable(enable: Boolean) { - _visibleDuration.value = if (enable) duration else Duration.ZERO - } - - /** - * Set visible - * - * @param visible - */ - fun setVisible(visible: Boolean) { - visibleState.targetState = visible - } - - /** - * Toggle visible - */ - fun toggleVisible() { - visibleState.targetState = !visibleState.currentState - } - - /** - * Is visible - * - * @return - */ - fun isVisible(): Boolean { - return visibleState.currentState - } - - /** - * Is auto hide enabled - * - * @return - */ - fun isAutoHideEnabled(): Boolean { - return visibleDuration.value > Duration.ZERO - } - - internal suspend fun autoHide() { - if (isVisible() && isAutoHideEnabled() && !userInteracting.value) { - delay(visibleDuration.value) - visibleState.targetState = false - } - } -} - -/** - * Remember toggle state - * - * @param player The player to listen [Player.isPlaying] to disable auto hide when in pause. - * @param visible Initial visibility. - * @param interactionSource Interaction source to disable auto hide when user is dragging. - * @param duration The duration after the view is hide. - */ -@Composable -fun rememberToggleState( - player: Player, - visible: Boolean, - interactionSource: InteractionSource? = null, - duration: Duration = DefaultAutoHideDelay -): ToggleState { - val isPlaying = player.isPlayingAsState() - val toggleState = rememberToggleState( - visible = visible, - interactionSource = interactionSource, duration - ) - toggleState.setAutoHideEnable(isPlaying) - return toggleState -} - -/** - * Remember toggle state - * - * @param player The player to listen [Player.isPlaying] to disable auto hide when in pause. - * @param visible Initial visibility. - * @param autoHideEnabled Auto hide enabled. - * @param interactionSource Interaction source to disable auto hide when user is dragging. - */ -@Composable -fun rememberToggleState( - player: Player, - visible: Boolean, - autoHideEnabled: Boolean, - interactionSource: InteractionSource? = null, -): ToggleState { - return rememberToggleState( - player = player, - duration = if (autoHideEnabled) DefaultAutoHideDelay else ZERO, - visible = visible, - interactionSource = interactionSource - ) -} - -/** - * Remember toggle state - * - * @param visible Initial visibility. - * @param interactionSource Interaction source to disable auto hide when user is dragging. - * @param duration The duration after the view is hide. - */ -@Composable -fun rememberToggleState( - visible: Boolean, - interactionSource: InteractionSource? = null, - duration: Duration = DefaultAutoHideDelay, -): ToggleState { - val toggleState = remember(visible, duration) { - ToggleState(visible = visible, duration = duration) - } - interactionSource?.let { - toggleState.userInteracting.value = interactionSource.collectIsDraggedAsState().value - } - LaunchedEffect(toggleState.isVisible(), toggleState.isAutoHideEnabled(), toggleState.userInteracting.value) { - toggleState.autoHide() - } - return toggleState -} - -/** - * Remember toggle state - * - * @param visible Initial visibility. - * @param autoHideEnabled Auto hide enabled. - * @param interactionSource Interaction source to disable auto hide when user is dragging. - */ -@Composable -fun rememberToggleState( - visible: Boolean, - autoHideEnabled: Boolean, - interactionSource: InteractionSource? = null, -): ToggleState { - return rememberToggleState( - duration = if (autoHideEnabled) DefaultAutoHideDelay else ZERO, - visible = visible, - interactionSource = interactionSource - ) -} - -/** - * Toggle view - * - * @param toggleState The toggle state for the view [rememberToggleState]. - * @param modifier [Modifier] to apply to this layout node. - * @param enter EnterTransition(s) used for the appearing animation, fading in while expanding by default. - * @param exit ExitTransition(s) used for the disappearing animation, fading out while shrinking by default. - * @param content Content to show or hide based on the value of [toggleState]. - * @receiver - */ -@Composable -fun ToggleView( - toggleState: ToggleState, - modifier: Modifier = Modifier, - enter: EnterTransition = fadeIn(), - exit: ExitTransition = fadeOut(), - content: @Composable() AnimatedVisibilityScope.() -> Unit -) { - AnimatedVisibility( - modifier = modifier, - visibleState = toggleState.visibleState, - enter = enter, - exit = exit, - content = content - ) -} - -@Composable -@Preview -private fun TogglePreview() { - var isVisible by remember { - mutableStateOf(true) - } - Column { - val toggleState = rememberToggleState( - visible = isVisible, - duration = 1.seconds - ) - val duration = toggleState.visibleDuration.value - ToggleView( - toggleState = toggleState, - ) { - Box( - modifier = Modifier - .size(120.dp, 120.dp) - .background(color = Color.Red), - contentAlignment = Alignment.Center - ) { - BasicText(text = "View to auto hide") - } - } - Row { - BasicText( - modifier = Modifier - .clickable { toggleState.setVisible(true) }, - text = "Visible" - ) - BasicText( - modifier = Modifier - .padding(start = 4.dp) - .clickable { toggleState.toggleVisible() }, - text = "Toggle" - ) - BasicText( - modifier = Modifier - .padding(start = 4.dp) - - .clickable { toggleState.setVisible(false) }, - text = "Hide" - ) - } - Row { - BasicText( - modifier = Modifier - .clickable { toggleState.setAutoHideEnable(true) }, - text = "Enable auto hide", - ) - BasicText( - modifier = Modifier - .padding(start = 4.dp) - .clickable { toggleState.setAutoHideEnable(false) }, - text = "Disable auto hide" - ) - } - BasicText( - modifier = Modifier - .padding(start = 4.dp) - .clickable { isVisible = !isVisible }, - text = "Toggle visible" - ) - BasicText(text = duration.toString()) - } -} diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt new file mode 100644 index 000000000..bb6dd1150 --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui.extension + +import android.os.Build +import android.view.KeyEvent +import android.view.KeyEvent.KEYCODE_DPAD_CENTER +import android.view.KeyEvent.KEYCODE_DPAD_LEFT +import android.view.KeyEvent.KEYCODE_DPAD_RIGHT +import android.view.KeyEvent.KEYCODE_ENTER +import android.view.KeyEvent.KEYCODE_NUMPAD_ENTER +import android.view.KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT +import android.view.KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.onPreviewKeyEvent + +private val DPadEventsKeyCodes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + listOf( + KEYCODE_DPAD_LEFT, + KEYCODE_SYSTEM_NAVIGATION_LEFT, + KEYCODE_DPAD_RIGHT, + KEYCODE_SYSTEM_NAVIGATION_RIGHT, + KEYCODE_DPAD_CENTER, + KEYCODE_ENTER, + KEYCODE_NUMPAD_ENTER, + ) +} else { + listOf( + KEYCODE_DPAD_LEFT, + KEYCODE_DPAD_RIGHT, + KEYCODE_DPAD_CENTER, + KEYCODE_ENTER, + KEYCODE_NUMPAD_ENTER, + ) +} + +/** + * Handle d pad key events + * + * @param onLeft action when left button is pressed. + * @param onRight action when right button is pressed. + * @param onEnter action when enter button is pressed. + */ +fun Modifier.handleDPadKeyEvents( + onLeft: (() -> Unit)? = null, + onRight: (() -> Unit)? = null, + onEnter: (() -> Unit)? = null, +) = onPreviewKeyEvent { + fun onActionUp(block: () -> Unit) { + if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) block() + } + + if (!DPadEventsKeyCodes.contains(it.nativeKeyEvent.keyCode)) return@onPreviewKeyEvent false + + when (it.nativeKeyEvent.keyCode) { + KEYCODE_ENTER, + KEYCODE_DPAD_CENTER, + KEYCODE_NUMPAD_ENTER, + -> { + onEnter?.apply { + onActionUp(::invoke) + return@onPreviewKeyEvent true + } + } + + KEYCODE_DPAD_LEFT, + KEYCODE_SYSTEM_NAVIGATION_LEFT, + -> { + onLeft?.apply { + onActionUp(::invoke) + return@onPreviewKeyEvent true + } + } + + KEYCODE_DPAD_RIGHT, + KEYCODE_SYSTEM_NAVIGATION_RIGHT, + -> { + onRight?.apply { + onActionUp(::invoke) + return@onPreviewKeyEvent true + } + } + } + false +} diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/DelayedVisibilityState.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/DelayedVisibilityState.kt new file mode 100644 index 000000000..6ee2798ec --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/DelayedVisibilityState.kt @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui.layout + +import android.content.Context +import android.os.Build +import android.view.accessibility.AccessibilityManager +import androidx.compose.foundation.Indication +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.selection.toggleable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role +import androidx.media3.common.Player +import ch.srgssr.pillarbox.player.playWhenReadyAsFlow +import ch.srgssr.pillarbox.ui.extension.handleDPadKeyEvents +import ch.srgssr.pillarbox.ui.playbackStateAsState +import kotlinx.coroutines.delay +import kotlin.time.Duration +import kotlin.time.Duration.Companion.INFINITE +import kotlin.time.Duration.Companion.ZERO +import kotlin.time.Duration.Companion.seconds + +/** + * Delayed visibility state + * + * @param initialVisible Initial visible + * @param initialDuration Initial duration + */ +@Stable +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) + + /** + * 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) + } + + /** + * Toggle + */ + fun toggle() { + this.isVisible = !isVisible + } + + /** + * Show + */ + fun show() { + this.isVisible = true + } + + /** + * Hide + */ + fun hide() { + this.isVisible = false + } + + /** + * Disable auto hide + */ + fun disableAutoHide() { + duration = ZERO + } + + /** + * Is auto hide enabled + */ + fun isAutoHideEnabled(): Boolean { + return duration < INFINITE && duration > ZERO + } + + internal class DelayedVisibility( + val visible: Boolean = true, + val duration: Duration = DefaultDuration + ) + + companion object { + /** + * Default duration + */ + val DefaultDuration = 3.seconds + + /** + * Disabled duration + */ + val DisabledDuration = ZERO + } +} + +/** + * Toggleable + * + * @param enabled whether or not this toggleable will handle input events and appear enabled for semantics purposes + * @param role the type of user interface element. Accessibility services might use this to describe the element or do customizations + * @param delayedVisibilityState the delayed visibility state to link + */ +fun Modifier.toggleable( + enabled: Boolean = true, + role: Role? = Role.Switch, + delayedVisibilityState: DelayedVisibilityState +): Modifier = composed { + Modifier.toggleable( + enabled = enabled, + role = role, + interactionSource = remember { + MutableInteractionSource() + }, + delayedVisibilityState = delayedVisibilityState + ) +} + +/** + * Toggleable + * + * @param enabled whether or not this toggleable will handle input events and appear enabled for semantics purposes + * @param role the type of user interface element. Accessibility services might use this to describe the element or do customizations + * @param indication indication to be shown when modified element is pressed. Be default, indication from LocalIndication will be used. + * Pass null to show no indication, or current value from LocalIndication to show theme default + * @param interactionSource MutableInteractionSource that will be used to emit PressInteraction.Press when this toggleable is being pressed. + * @param delayedVisibilityState the delayed visibility state to link + */ +fun Modifier.toggleable( + enabled: Boolean = true, + role: Role? = Role.Switch, + indication: Indication? = null, + interactionSource: MutableInteractionSource, + delayedVisibilityState: DelayedVisibilityState +): Modifier = this.then( + Modifier + .toggleable( + value = delayedVisibilityState.isVisible, + enabled = enabled, + indication = indication, + interactionSource = interactionSource, + role = role, + onValueChange = { + delayedVisibilityState.isVisible = it + } + ) + .handleDPadKeyEvents(onEnter = { + delayedVisibilityState.toggle() + }) + .focusable(enabled = enabled) +) + +/** + * Maintain visibility on focus + * + * @param delayedVisibilityState the delayed visibility state to link + */ +fun Modifier.maintainVisibleOnFocus(delayedVisibilityState: DelayedVisibilityState): Modifier { + return this.then( + Modifier.onFocusChanged { + if (it.isFocused) { + delayedVisibilityState.show() + } + } + ) +} + +/** + * Remember delayed visibility state + * + * @param player The player to listen if it is playing or not + * @param visible visibility state of the content + * @param autoHideEnabled true to enable hide after [duration] + * @param duration the duration to wait after hiding the content. + */ +@Composable +fun rememberDelayedVisibilityState( + player: Player, + visible: Boolean = true, + autoHideEnabled: Boolean = true, + duration: Duration = DelayedVisibilityState.DefaultDuration +): DelayedVisibilityState { + val playWhenReadyFlow = remember(player) { + player.playWhenReadyAsFlow() + } + val playbackState = player.playbackStateAsState() + val stateReady = playbackState == Player.STATE_READY || playbackState == Player.STATE_BUFFERING + val playWhenReady = playWhenReadyFlow.collectAsState(initial = player.playWhenReady).value + return rememberDelayedVisibilityState(visible = visible, autoHideEnabled && playWhenReady && stateReady, duration) +} + +/** + * Remember delayed visibility state + * + * @param visible visibility state of the content. + * @param autoHideEnabled true to enable hide after [duration]. Auto hide is always disabled when accessibility is on. + * @param duration the duration to wait after hiding the content. + */ +@Composable +fun rememberDelayedVisibilityState( + visible: Boolean = true, + autoHideEnabled: Boolean = true, + duration: Duration = DelayedVisibilityState.DefaultDuration +): DelayedVisibilityState { + val context = LocalContext.current + val ac = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.getSystemService(AccessibilityManager::class.java) + } else { + context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + } + var isTalkBackEnabled by remember { + mutableStateOf(ac.isEnabled) + } + DisposableEffect(context) { + val l: AccessibilityManager.AccessibilityStateChangeListener = object : AccessibilityManager.AccessibilityStateChangeListener { + override fun onAccessibilityStateChanged(enabled: Boolean) { + isTalkBackEnabled = ac.isEnabled + } + } + ac.addAccessibilityStateChangeListener(l) + onDispose { + ac.removeAccessibilityStateChangeListener(l) + } + } + val autoHideEnabledAccessibility = autoHideEnabled && !isTalkBackEnabled + val delayedVisibilityState = remember() { + DelayedVisibilityState(visible, duration).apply { + if (!autoHideEnabledAccessibility) { + disableAutoHide() + } + } + } + + LaunchedEffect(duration, autoHideEnabledAccessibility) { + if (autoHideEnabledAccessibility) { + delayedVisibilityState.duration = duration + } else { + delayedVisibilityState.disableAutoHide() + } + } + + LaunchedEffect(visible) { + delayedVisibilityState.isVisible = visible + } + + LaunchedEffect(delayedVisibilityState.state) { + if (delayedVisibilityState.isVisible && delayedVisibilityState.isAutoHideEnabled()) { + delay(delayedVisibilityState.duration) + delayedVisibilityState.hide() + } + } + + return delayedVisibilityState +} diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/ToggleableBox.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/ToggleableBox.kt new file mode 100644 index 000000000..2b4eb9d28 --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/layout/ToggleableBox.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui.layout + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +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.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlin.time.Duration + +/** + * Toggle view + * + * @param visibilityState A state that holds the current visibility and auto hide delay mode. + * @param toggleableContent Content to show or hide based on the value of [ToggleVisibilityState.isDisplayed]. + * @param modifier modifier for the Layout created to contain the content. + * @param toggleable content is toggleable. + * @param contentAlignment - The default alignment inside the Box. + * @param propagateMinConstraints - Whether the incoming min constraints should be passed to content. + * @param enter EnterTransition(s) used for the appearing animation, fading in while expanding by default. + * @param exit ExitTransition(s) used for the disappearing animation, fading out while shrinking by default. + * @param content Content displayed under toggleableContent. + */ +@Composable +fun ToggleableBox( + visibilityState: DelayedVisibilityState, + toggleableContent: @Composable AnimatedVisibilityScope.() -> Unit, + modifier: Modifier = Modifier, + toggleable: Boolean = true, + contentAlignment: Alignment = Alignment.TopStart, + propagateMinConstraints: Boolean = false, + enter: EnterTransition = expandVertically { it }, + exit: ExitTransition = shrinkVertically { it }, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier + .toggleable( + enabled = toggleable, + delayedVisibilityState = visibilityState + ), + contentAlignment = contentAlignment, + propagateMinConstraints = propagateMinConstraints + ) { + content(this) + val animatedModifier = if (toggleable) { + Modifier + .matchParentSize() + .maintainVisibleOnFocus(delayedVisibilityState = visibilityState) + } else { + Modifier.matchParentSize() + } + AnimatedVisibility( + modifier = animatedModifier, + visible = visibilityState.isVisible, + enter = enter, + exit = exit, + content = toggleableContent, + ) + } +} + +@Preview +@Composable +private fun TogglePreview() { + var delay by remember { + mutableStateOf(DelayedVisibilityState.DefaultDuration) + } + var toggleable by remember { + mutableStateOf(true) + } + val visibilityState = rememberDelayedVisibilityState(duration = delay) + val coroutineScope = rememberCoroutineScope() + Column { + ToggleableBox( + visibilityState = visibilityState, + modifier = Modifier.aspectRatio(16 / 9f), + toggleable = toggleable, + toggleableContent = { + BasicText(text = "Text to hide", color = { Color.Red }) + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White) + ) + } + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + BasicText( + text = "Show", + modifier = Modifier.clickable { + visibilityState.show() + } + ) + BasicText( + text = "Toggle", + modifier = Modifier.clickable { + visibilityState.toggle() + } + ) + BasicText( + text = "Hide", + modifier = Modifier.clickable { + visibilityState.hide() + } + ) + BasicText( + text = "Disable", + modifier = Modifier.clickable { + delay = Duration.ZERO + } + ) + BasicText( + text = "Enable", + modifier = Modifier.clickable { + coroutineScope.launch { + delay = DelayedVisibilityState.DefaultDuration + } + } + ) + } + Row { + BasicText( + text = "Toggleable", + modifier = Modifier.clickable { + coroutineScope.launch { + toggleable = !toggleable + } + } + ) + } + } +}