From 4c4d9eac54d8bd22bf935acfb6bc52a082a9a148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 20 Sep 2023 11:21:12 +0200 Subject: [PATCH] Add a simple ToggleView. (#248) --- .../ui/player/controls/PlayingControls.kt | 36 +- .../srgssr/pillarbox/ui/DelayVisibleState.kt | 81 ----- .../java/ch/srgssr/pillarbox/ui/ToggleView.kt | 308 ++++++++++++++++++ 3 files changed, 319 insertions(+), 106 deletions(-) delete mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/DelayVisibleState.kt create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt 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 index 48dad9295..87f2b393d 100644 --- 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 @@ -4,12 +4,8 @@ */ package ch.srgssr.pillarbox.demo.ui.player.controls -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable 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 @@ -24,13 +20,10 @@ 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.DefaultVisibleDelay +import ch.srgssr.pillarbox.ui.ToggleView import ch.srgssr.pillarbox.ui.currentMediaMetadataAsState -import ch.srgssr.pillarbox.ui.isPlayingAsState import ch.srgssr.pillarbox.ui.playbackStateAsState -import ch.srgssr.pillarbox.ui.rememberDelayVisibleState -import ch.srgssr.pillarbox.ui.toggleState -import kotlin.time.Duration +import ch.srgssr.pillarbox.ui.rememberToggleState /** * Playing controls @@ -59,29 +52,22 @@ fun PlayingControls( optionClicked: (() -> Unit)? = null ) { val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } - val isDragged = interactionSource.collectIsDraggedAsState() - val isPlaying = player.isPlayingAsState() - val durationState = - if (autoHideEnabled && !isDragged.value && isPlaying) { - DefaultVisibleDelay - } else { - Duration.ZERO - } - val delayVisibleState = rememberDelayVisibleState(visible = controlVisible, visibleDelay = durationState) + val toggleState = rememberToggleState( + player = player, + visible = controlVisible, + autoHideEnabled = autoHideEnabled, + interactionSource = interactionSource + ) Box( - modifier = modifier.clickable(role = Role.Switch, onClickLabel = "Toggle controls") { - delayVisibleState.toggleState() - } + 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) } - AnimatedVisibility( + ToggleView( modifier = Modifier.fillMaxSize(), - visibleState = delayVisibleState, - enter = fadeIn(), - exit = fadeOut() + toggleState = toggleState ) { val mediaMetadata = player.currentMediaMetadataAsState() Box(modifier = Modifier.matchParentSize()) { diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/DelayVisibleState.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/DelayVisibleState.kt deleted file mode 100644 index 36f6fda0d..000000000 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/DelayVisibleState.kt +++ /dev/null @@ -1,81 +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.core.MutableTransitionState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import kotlinx.coroutines.delay -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -/** - * Remember delay visible state to use with [AnimatedVisibility] - * - * @param visible initial visibility state - * @param visibleDelay delay after visibility turn off. [Duration.ZERO] to disable. - */ -@Composable -fun rememberDelayVisibleState(visible: Boolean, visibleDelay: Duration = Duration.ZERO): MutableTransitionState { - val visibleState = remember { - MutableTransitionState(visible) - } - LaunchedEffect(visibleDelay, visibleState.targetState) { - if (visibleDelay > Duration.ZERO) { - delay(visibleDelay) - visibleState.targetState = false - } - } - return visibleState -} - -/** - * Toggle target state with not the current state. - */ -fun MutableTransitionState.toggleState() { - targetState = !currentState -} - -@Preview -@Composable -fun Preview() { - val visibleState = rememberDelayVisibleState(visible = true, visibleDelay = Duration.ZERO) - Column() { - AnimatedVisibility( - modifier = Modifier, - visibleState = visibleState, - enter = fadeIn(), - exit = fadeOut() - ) { - BasicText(text = "toolbar") - } - Box(modifier = Modifier.clickable { visibleState.targetState = !visibleState.currentState }) { - BasicText(text = "Toggle Button") - } - - AnimatedVisibility( - visibleState = visibleState, - enter = fadeIn(), - exit = fadeOut() - ) { - BasicText(text = "Footer") - } - } -} - -/** - * Default visible delay (4 seconds) - */ -val DefaultVisibleDelay = 4L.seconds 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 new file mode 100644 index 000000000..6b888d695 --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ToggleView.kt @@ -0,0 +1,308 @@ +/* + * 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()) + } +}