Skip to content

Commit

Permalink
822 remove toggleablebox (#828)
Browse files Browse the repository at this point in the history
Co-authored-by: Gaëtan Muller <[email protected]>
Co-authored-by: Gaëtan Muller <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2024
1 parent a30acc8 commit 2ee6651
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 649 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,27 @@ fun rememberIsTouchExplorationEnabled(): Boolean {

return isTouchExplorationEnabled
}

/**
* A composable function that returns a boolean indicating whether TalkBack is currently enabled.
*
* This function uses a [DisposableEffect] to register an [AccessibilityManager.AccessibilityStateChangeListener]
* that updates the state of the composable when the accessibility state changes.
*
* @return `true` if TalkBack is enabled, `false` otherwise.
*/
@Composable
fun rememberIsTalkBackEnabled(): Boolean {
val accessibilityManager = LocalContext.current.getSystemService<AccessibilityManager>() ?: return false
val (isTalkBackEnabled, setTalkBackEnabled) = remember {
mutableStateOf(accessibilityManager.isEnabled)
}
DisposableEffect(Unit) {
val l = AccessibilityManager.AccessibilityStateChangeListener(setTalkBackEnabled)
accessibilityManager.addAccessibilityStateChangeListener(l)
onDispose {
accessibilityManager.removeAccessibilityStateChangeListener(l)
}
}
return isTalkBackEnabled
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.shared.ui.player

import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
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.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.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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 kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.seconds

/**
* A class that manages the visibility of controls with a delay.
*
* This class is used to control the visibility of UI elements that should be hidden
* after a certain period of inactivity. The visibility is initially set to [initialVisible]
* and the delay before hiding is set to [initialDelay].
*
* To reset the delay and keep the controls visible, call the [reset] function.
* This will restart the delay timer.
*
* @param initialVisible The initial visibility of the controls.
* @param initialDelay The initial delay before hiding the controls.
*/
class DelayedControlsVisibility internal constructor(initialVisible: Boolean, initialDelay: Duration) {
/**
* Controls visibility.
*/
var visible by mutableStateOf(initialVisible)

/**
* The [delay] after the controls become no more visible.
* Can be reset with [reset] method.
*/
var delay by mutableStateOf(initialDelay)
internal var reset by mutableStateOf<Any?>(null)

/**
* Resets the ongoing delay.
*/
fun reset() {
if (visible && delay > ZERO) {
reset = Any()
}
}
}

/**
* Remembers and controls the visibility of UI elements with a delay.
*
* Initially sets visibility to [initialVisible]. If visible, hides after [initialDelay].
*
* @param initialVisible Initial visibility. Defaults to false.
* @param initialDelay Delay before hiding, if initially visible. Defaults to 3 seconds.
* @return A [DelayedControlsVisibility] instance to control and observe visibility.
*/
@Composable
fun rememberDelayedControlsVisibility(initialVisible: Boolean = false, initialDelay: Duration = DefaultVisibilityDelay): DelayedControlsVisibility {
val visibility = remember(initialVisible, initialDelay) { DelayedControlsVisibility(initialVisible, initialDelay) }
LaunchedEffect(visibility.visible, visibility.delay, visibility.reset) {
if (visibility.visible && visibility.delay > ZERO) {
delay(visibility.delay)
visibility.visible = false
}
}
return visibility
}

/**
* Default visibility delay
*/
val DefaultVisibilityDelay = 3.seconds

@Preview
@Composable
private fun KeepVisibleDelayPreview() {
val visibility = rememberDelayedControlsVisibility(true, 2.seconds)

Column {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16 / 9f)
.clickable { visibility.visible = !visibility.visible },
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Green)
)
androidx.compose.animation.AnimatedVisibility(
visible = visibility.visible,
modifier = Modifier.fillMaxSize(),
enter = fadeIn(),
exit = fadeOut(),
) {
Box(modifier = Modifier.background(color = Color.Black.copy(alpha = 0.5f)), contentAlignment = Alignment.Center) {
BasicText(text = "Text to hide", color = { Color.Red })
}
}
}

Row(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.White),
horizontalArrangement = Arrangement.SpaceAround
) {
BasicText(
text = "Show",
modifier = Modifier.clickable {
visibility.visible = true
visibility.reset()
}
)
BasicText(
text = "Toggle",
modifier = Modifier.clickable {
visibility.visible = !visibility.visible
}
)
BasicText(
text = "Hide",
modifier = Modifier.clickable {
visibility.visible = false
}
)
BasicText(
text = "Disable",
modifier = Modifier.clickable {
visibility.delay = ZERO
}
)
BasicText(
text = "Enable",
modifier = Modifier.clickable {
visibility.delay = 2.seconds
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ 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.shared.ui.localTimeFormatter
import ch.srgssr.pillarbox.demo.shared.ui.player.DefaultVisibilityDelay
import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay
import ch.srgssr.pillarbox.demo.shared.ui.player.rememberDelayedControlsVisibility
import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTalkBackEnabled
import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerError
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerPlaybackRow
Expand All @@ -70,11 +73,9 @@ import ch.srgssr.pillarbox.ui.extension.durationAsState
import ch.srgssr.pillarbox.ui.extension.getCurrentChapterAsState
import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState
import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState
import ch.srgssr.pillarbox.ui.extension.isPlayingAsState
import ch.srgssr.pillarbox.ui.extension.playerErrorAsState
import ch.srgssr.pillarbox.ui.widget.DelayedVisibilityState
import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.rememberDelayedVisibilityState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Instant
Expand All @@ -92,6 +93,8 @@ import kotlin.time.Duration.Companion.seconds
* @param metricsOverlayEnabled
* @param metricsOverlayOptions
*/

@Suppress("CyclomaticComplexMethod")
@Composable
fun PlayerView(
player: PillarboxExoPlayer,
Expand All @@ -100,17 +103,21 @@ fun PlayerView(
metricsOverlayOptions: MetricsOverlayOptions,
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val visibilityState = rememberDelayedVisibilityState(player = player, visible = true)

val talkBackEnabled = rememberIsTalkBackEnabled()
val isPlaying by player.isPlayingAsState()
val keepControlDelay = if (!talkBackEnabled && isPlaying) DefaultVisibilityDelay else ZERO
val controlsVisibilityState = rememberDelayedControlsVisibility(initialVisible = true, keepControlDelay)

LaunchedEffect(drawerState.currentValue) {
when (drawerState.currentValue) {
DrawerValue.Closed -> visibilityState.show()
DrawerValue.Open -> visibilityState.hide()
controlsVisibilityState.visible = when (drawerState.currentValue) {
DrawerValue.Closed -> true
DrawerValue.Open -> false
}
}

BackHandler(enabled = visibilityState.isVisible) {
visibilityState.hide()
BackHandler(enabled = controlsVisibilityState.visible) {
controlsVisibilityState.visible = false
}

PlaybackSettingsDrawer(
Expand All @@ -133,7 +140,7 @@ fun PlayerView(
.onDpadEvent(
eventType = KeyEventType.KeyUp,
onEnter = {
visibilityState.show()
controlsVisibilityState.visible = true
true
},
)
Expand All @@ -146,7 +153,7 @@ fun PlayerView(
Column {
ChapterInfo(
player = player,
visibilityState = visibilityState,
controlsVisible = controlsVisibilityState.visible,
)

if (metricsOverlayEnabled) {
Expand All @@ -166,25 +173,27 @@ fun PlayerView(
}
}

if (!visibilityState.isVisible && currentCredit != null) {
if (!controlsVisibilityState.visible && currentCredit != null) {
SkipButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(MaterialTheme.paddings.baseline),
onClick = { player.seekTo(currentCredit?.end ?: 0L) },
)
}

AnimatedVisibility(
visible = visibilityState.isVisible,
visible = controlsVisibilityState.visible,
modifier = Modifier
.fillMaxSize()
.maintainVisibleOnFocus(delayedVisibilityState = visibilityState),
.onFocusChanged {
if (it.isFocused) {
controlsVisibilityState.reset()
}
},
) {
Box {
PlayerPlaybackRow(
player = player,
state = visibilityState,
modifier = Modifier.align(Alignment.Center),
)

Expand Down Expand Up @@ -221,7 +230,7 @@ fun PlayerView(
PlayerTimeRow(
player = player,
onSeek = { value ->
visibilityState.resetAutoHide()
controlsVisibilityState.reset()
player.seekTo(value)
},
)
Expand All @@ -236,7 +245,7 @@ fun PlayerView(
@Composable
private fun ChapterInfo(
player: Player,
visibilityState: DelayedVisibilityState,
controlsVisible: Boolean,
modifier: Modifier = Modifier,
) {
val currentMediaMetadata by player.currentMediaMetadataAsState()
Expand All @@ -255,7 +264,7 @@ private fun ChapterInfo(
}

AnimatedVisibility(
visible = visibilityState.isVisible || showChapterInfo,
visible = controlsVisible || showChapterInfo,
modifier = modifier,
enter = expandVertically(),
exit = shrinkVertically(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,51 +24,33 @@ 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.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
import ch.srgssr.pillarbox.player.extension.canSeekToNext
import ch.srgssr.pillarbox.player.extension.canSeekToPrevious
import ch.srgssr.pillarbox.ui.extension.availableCommandsAsState
import ch.srgssr.pillarbox.ui.extension.isPlayingAsState
import ch.srgssr.pillarbox.ui.widget.DelayedVisibilityState

/**
* Tv playback row
*
* @param player
* @param state
* @param modifier
*/
@Composable
fun PlayerPlaybackRow(
player: Player,
state: DelayedVisibilityState,
modifier: Modifier = Modifier,
) {
val isPlaying by player.isPlayingAsState()
val focusRequester = remember { FocusRequester() }
val resetAutoHideCallback = remember {
{
state.resetAutoHide()
false
}
}

LaunchedEffect(state.isVisible) {
if (state.isVisible) {
focusRequester.requestFocus()
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}

Row(
modifier = modifier.onDpadEvent(
onLeft = resetAutoHideCallback,
onRight = resetAutoHideCallback,
onDown = resetAutoHideCallback,
onEnter = resetAutoHideCallback,
),
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline),
) {
val availableCommands by player.availableCommandsAsState()
Expand Down
Loading

0 comments on commit 2ee6651

Please sign in to comment.