From 0f648e295a8c304bfc02dd0c6dc0087eae1c55a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 10 Dec 2024 14:11:30 +0100 Subject: [PATCH 1/8] Remove ToggleableBox --- .../demo/shared/ui/TalkBackEnabled.kt | 37 +++ .../demo/ui/player/BlockedTimeRangeWarning.kt | 100 ------ .../pillarbox/demo/ui/player/PlayerView.kt | 182 +++++++---- .../ui/widget/DelayedVisibilityState.kt | 291 ------------------ .../pillarbox/ui/widget/KeepVisibleDelay.kt | 175 +++++++++++ .../pillarbox/ui/widget/ToggleableBox.kt | 160 ---------- 6 files changed, 338 insertions(+), 607 deletions(-) create mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt delete mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt delete mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt delete mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/ToggleableBox.kt diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt new file mode 100644 index 000000000..f660849b8 --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui + +import android.view.accessibility.AccessibilityManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.getSystemService + +/** + * 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() ?: 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 +} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt deleted file mode 100644 index 3e82ee2ee..000000000 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/BlockedTimeRangeWarning.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.ui.player - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -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.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.media3.common.Player -import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme -import ch.srgssr.pillarbox.demo.ui.theme.paddings -import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange -import kotlinx.coroutines.delay -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -/** - * Display a message when the player reaches a blocked time range. - * - * @param player - * @param modifier - * @param visibilityDelay - */ -@Composable -fun BlockedTimeRangeWarning( - player: Player, - modifier: Modifier = Modifier, - visibilityDelay: Duration = 5.seconds, -) { - var currentBlockedTimeRange: BlockedTimeRange? by remember(player) { - mutableStateOf(null) - } - DisposableEffect(player) { - val listener = object : PillarboxPlayer.Listener { - override fun onBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) { - currentBlockedTimeRange = blockedTimeRange - } - } - player.addListener(listener) - onDispose { - player.removeListener(listener) - } - } - LaunchedEffect(currentBlockedTimeRange) { - if (currentBlockedTimeRange != null) { - delay(visibilityDelay) - currentBlockedTimeRange = null - } - } - AnimatedVisibility( - modifier = modifier, - visible = currentBlockedTimeRange != null - ) { - currentBlockedTimeRange?.let { - BlockedTimeRangeInfo(modifier = Modifier.fillMaxWidth(), blockedTimeRange = it) - } - } -} - -@Composable -private fun BlockedTimeRangeInfo( - blockedTimeRange: BlockedTimeRange, - modifier: Modifier = Modifier -) { - Text( - modifier = modifier - .background(color = Color.Blue.copy(0.8f)) - .padding(MaterialTheme.paddings.baseline), - text = "Reached a blocked segment! ${blockedTimeRange.reason}", - color = Color.White, - style = MaterialTheme.typography.labelSmall - ) -} - -@Preview(showBackground = true) -@Composable -private fun BlockedTimeRangeInfoPreview() { - val blockedTimeRange = BlockedTimeRange(start = 0, end = 0, reason = "GeoBlock") - PillarboxTheme { - BlockedTimeRangeInfo( - modifier = Modifier.fillMaxWidth(), - blockedTimeRange = blockedTimeRange - ) - } -} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 0112777d5..eceda9df0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -4,28 +4,40 @@ */ package ch.srgssr.pillarbox.demo.ui.player +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription -import androidx.compose.ui.zIndex import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay +import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTalkBackEnabled import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerError @@ -34,18 +46,21 @@ import ch.srgssr.pillarbox.demo.ui.player.controls.SkipButton import ch.srgssr.pillarbox.demo.ui.player.controls.rememberProgressTrackerState import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.ui.ProgressTrackerState import ch.srgssr.pillarbox.ui.ScaleMode import ch.srgssr.pillarbox.ui.exoplayer.ExoPlayerSubtitleView import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState import ch.srgssr.pillarbox.ui.extension.getPeriodicallyCurrentMetricsAsState import ch.srgssr.pillarbox.ui.extension.hasMediaItemsAsState +import ch.srgssr.pillarbox.ui.extension.isPlayingAsState import ch.srgssr.pillarbox.ui.extension.playbackStateAsState import ch.srgssr.pillarbox.ui.extension.playerErrorAsState -import ch.srgssr.pillarbox.ui.widget.ToggleableBox +import ch.srgssr.pillarbox.ui.widget.DefaultKeepDelay import ch.srgssr.pillarbox.ui.widget.keepScreenOn import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface -import ch.srgssr.pillarbox.ui.widget.rememberDelayedVisibilityState +import ch.srgssr.pillarbox.ui.widget.rememberKeepVisibleDelay +import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds /** @@ -101,36 +116,30 @@ fun PlayerView( MutableInteractionSource() } val isSliderDragged by interactionSource.collectIsDraggedAsState() - val visibilityState = rememberDelayedVisibilityState( - player = player, - autoHideEnabled = !isSliderDragged, - visible = controlsVisible - ) - val currentCredit by player.getCurrentCreditAsState() - val controlsStateDescription = if (visibilityState.isVisible) { + val talkBackEnabled = rememberIsTalkBackEnabled() + val isPlaying by player.isPlayingAsState() + val keepControlDelay = if (!talkBackEnabled && !isSliderDragged && isPlaying) DefaultKeepDelay else ZERO + var controlsVisibility by rememberKeepVisibleDelay(initialVisibility = controlsVisible, keepVisibleDelay = keepControlDelay) + val playbackState by player.playbackStateAsState() + val isBuffering = playbackState == Player.STATE_BUFFERING + val controlsStateDescription = if (controlsVisibility) { stringResource(R.string.controls_visible) } else { stringResource(R.string.controls_hidden) } - - ToggleableBox( - modifier = modifier.semantics { - stateDescription = controlsStateDescription - }, - toggleable = controlsToggleable, - visibilityState = visibilityState, - toggleableContent = { - PlayerControls( - player = player, - interactionSource = interactionSource, - progressTracker = progressTracker, - credit = currentCredit, - content = content + Box( + modifier = modifier + .toggleable( + value = controlsVisibility, + enabled = controlsToggleable, + onValueChange = { + controlsVisibility = !controlsVisibility + } ) - } + .semantics { + stateDescription = controlsStateDescription + } ) { - val playbackState by player.playbackStateAsState() - val isBuffering = playbackState == Player.STATE_BUFFERING PlayerSurface( modifier = Modifier .fillMaxSize() @@ -138,27 +147,19 @@ fun PlayerView( player = player, scaleMode = scaleMode ) { - if (isBuffering && !isSliderDragged) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White) - } - } - ExoPlayerSubtitleView(player = player) - if (overlayEnabled && player is PillarboxExoPlayer) { - val currentMetrics by player.getPeriodicallyCurrentMetricsAsState(500.milliseconds) - currentMetrics?.let { - MetricsOverlay( - modifier = Modifier - .fillMaxSize() - .align(Alignment.TopStart), - playbackMetrics = it, - overlayOptions = overlayOptions, - ) - } - } + SurfaceOverlay( + player = player, + displayBuffering = isBuffering && !isSliderDragged, + overlayEnabled = overlayEnabled, + overlayOptions = overlayOptions, + ) } - - if (currentCredit != null && !visibilityState.isVisible) { + val currentCredit by player.getCurrentCreditAsState() + AnimatedVisibility( + visible = currentCredit != null && !controlsVisibility, + enter = fadeIn(), + exit = fadeOut(), + ) { SkipButton( modifier = Modifier .align(Alignment.BottomEnd) @@ -167,19 +168,88 @@ fun PlayerView( ) } - BlockedTimeRangeWarning( - player = player, + DemoControls( modifier = Modifier - .align(Alignment.TopStart) - .zIndex(2f), + .matchParentSize() + .onFocusChanged { if (it.isFocused) controlsVisibility = true } + .onEnterPressed { + controlsVisibility = true + }, + controlsVisible = controlsVisibility, + player = player, + progressTracker = progressTracker, + interactionSource = interactionSource, + currentCredit = currentCredit, + content = content, ) + } +} - ChapterInfo( +@Composable +private fun BoxScope.SurfaceOverlay( + player: Player, + displayBuffering: Boolean, + overlayEnabled: Boolean, + overlayOptions: MetricsOverlayOptions, +) { + AnimatedVisibility( + displayBuffering, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White) + } + } + ExoPlayerSubtitleView(player = player) + if (overlayEnabled && player is PillarboxExoPlayer) { + val currentMetrics by player.getPeriodicallyCurrentMetricsAsState(500.milliseconds) + currentMetrics?.let { + MetricsOverlay( + modifier = Modifier + .fillMaxSize() + .align(Alignment.TopStart), + playbackMetrics = it, + overlayOptions = overlayOptions, + ) + } + } +} + +@Composable +private fun DemoControls( + modifier: Modifier, + controlsVisible: Boolean, + player: Player, + progressTracker: ProgressTrackerState, + interactionSource: MutableInteractionSource, + currentCredit: Credit?, + content: @Composable ColumnScope.() -> Unit +) { + AnimatedVisibility( + modifier = modifier, + visible = controlsVisible + ) { + PlayerControls( player = player, - modifier = Modifier - .align(Alignment.BottomStart) - .zIndex(2f), - visible = !visibilityState.isVisible + interactionSource = interactionSource, + progressTracker = progressTracker, + credit = currentCredit, + content = content ) } } + +private fun Modifier.onEnterPressed(action: () -> Unit): Modifier { + return this then Modifier.onPreviewKeyEvent { + val isEnterKey = it.key == Key.Enter || it.key == Key.DirectionCenter || it.key == Key.NumPadEnter + val isKeyUp = it.type == KeyEventType.KeyUp + + if (isEnterKey && isKeyUp) { + action() + true + } else { + false + } + } +} 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 deleted file mode 100644 index aec4cce17..000000000 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.ui.widget - -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.Interaction -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.focus.onFocusChanged -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type -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.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 - -/** - * Represents a state that manages visibility, with an optional delayed auto-hiding functionality. - * - * @param initialVisible The initial visibility state of the component. - * @param initialDuration The initial duration before auto-hiding. - */ -@Stable -class DelayedVisibilityState internal constructor( - initialVisible: Boolean = true, - initialDuration: Duration = DefaultDuration -) { - internal var autoHideResetTrigger by mutableStateOf(false) - private set - - /** - * Represents the visibility state of the component. - * - * This property is observable and changes to this property will trigger recomposition. - */ - var isVisible by mutableStateOf(initialVisible) - - /** - * Represents the delay until auto-hide is performed. - * - * This property is observable and changes to this property will trigger recomposition. - */ - var duration by mutableStateOf(initialDuration) - - /** - * Toggles the visibility of the component. - */ - fun toggle() { - this.isVisible = !isVisible - } - - /** - * Makes the component visible. - */ - fun show() { - this.isVisible = true - } - - /** - * Makes the component invisible. - */ - fun hide() { - this.isVisible = false - } - - /** - * Disables the auto-hide behavior of the component. - */ - fun disableAutoHide() { - duration = DisabledDuration - } - - /** - * Resets the auto-hide countdown. - */ - fun resetAutoHide() { - autoHideResetTrigger = !autoHideResetTrigger - } - - /** - * Checks if the auto-hide feature is enabled. - * - * @return `true` if auto-hide is enabled, `false` otherwise. - */ - fun isAutoHideEnabled(): Boolean { - return duration < INFINITE && duration > ZERO - } - - @Suppress("UndocumentedPublicClass") - companion object { - /** - * Default auto-hide duration. - */ - val DefaultDuration = 3.seconds - - /** - * Disabled auto-hide duration. - */ - val DisabledDuration = ZERO - } -} - -/** - * Makes a Composable toggleable, controlling the visibility state of a [DelayedVisibilityState]. - * - * @param enabled Whether to handle input events and appear enabled for semantics purposes. - * @param role The type of UI element. Accessibility services might use this to describe the element. - * @param delayedVisibilityState The [DelayedVisibilityState] instance to control the visibility of the component. - */ -fun Modifier.toggleable( - enabled: Boolean = true, - role: Role? = Role.Switch, - delayedVisibilityState: DelayedVisibilityState -): Modifier = toggleable( - enabled = enabled, - role = role, - interactionSource = null, - delayedVisibilityState = delayedVisibilityState, -) - -/** - * Makes a Composable toggleable, controlling the visibility state of a [DelayedVisibilityState]. - * - * @param enabled Whether to handle input events and appear enabled for semantics purposes. - * @param role The type of UI element. Accessibility services might use this to describe the element. - * @param indication Indication to be shown when the Composable is pressed. If `null`, no indication will be shown. - * @param interactionSource The [MutableInteractionSource] that will be used to dispatch [Interaction]s when this toggleable component is being - * interacted with. - * @param delayedVisibilityState The [DelayedVisibilityState] instance to control the visibility of the component. - */ -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 - } - ) - .onEnterPressed(delayedVisibilityState::toggle) - .focusable(enabled = enabled) -) - -/** - * Maintains a Composable visible when it gains focus. - * - * @param delayedVisibilityState The [DelayedVisibilityState] instance to control the visibility of the component. - */ -fun Modifier.maintainVisibleOnFocus(delayedVisibilityState: DelayedVisibilityState): Modifier { - return onFocusChanged { - if (it.isFocused) { - delayedVisibilityState.show() - } - } -} - -/** - * Remembers a [DelayedVisibilityState] for the provided [Player] - * - * @param player The [Player] to associate with the [DelayedVisibilityState]. - * @param visible Whether the component is initially visible. - * @param autoHideEnabled Whether auto-hiding is enabled. Auto-hiding is always disabled when accessibility is on. - * @param duration The duration to wait before hiding the component. - * - * @return A [DelayedVisibilityState] instance. - */ -@Composable -fun rememberDelayedVisibilityState( - player: Player, - visible: Boolean = true, - autoHideEnabled: Boolean = true, - duration: Duration = DelayedVisibilityState.DefaultDuration -): DelayedVisibilityState { - val playWhenReadyFlow = remember(player) { - player.playWhenReadyAsFlow() - } - val playbackState by player.playbackStateAsState() - val stateReady = playbackState == Player.STATE_READY || playbackState == Player.STATE_BUFFERING - val playWhenReady by playWhenReadyFlow.collectAsState(initial = player.playWhenReady) - return rememberDelayedVisibilityState(visible = visible, autoHideEnabled && playWhenReady && stateReady, duration) -} - -/** - * Remembers a [DelayedVisibilityState]. - * - * @param visible Whether the component is initially visible. - * @param autoHideEnabled Whether auto-hiding is enabled. Auto-hiding is always disabled when accessibility is on. - * @param duration The duration to wait before hiding the component. - * - * @return A [DelayedVisibilityState] instance. - */ -@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 { 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.isVisible, delayedVisibilityState.duration, delayedVisibilityState.autoHideResetTrigger) { - if (delayedVisibilityState.isVisible && delayedVisibilityState.isAutoHideEnabled()) { - delay(delayedVisibilityState.duration) - delayedVisibilityState.hide() - } - } - - return delayedVisibilityState -} - -private fun Modifier.onEnterPressed(action: () -> Unit): Modifier { - return this then Modifier.onPreviewKeyEvent { - val isEnterKey = it.key == Key.Enter || it.key == Key.DirectionCenter || it.key == Key.NumPadEnter - val isKeyUp = it.type == KeyEventType.KeyUp - - if (isEnterKey && isKeyUp) { - action() - true - } else { - false - } - } -} diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt new file mode 100644 index 000000000..98cfc7a36 --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui.widget + +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.MutableState +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.platform.AccessibilityManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlin.time.Duration +import kotlin.time.Duration.Companion.ZERO +import kotlin.time.Duration.Companion.seconds + +/** + * Default auto-hide duration. + */ +val DefaultKeepDelay = 3.seconds + +/** + * For users that needs accessibility, [keepVisibleDelay] should be increased or set to [Duration.ZERO] to disable auto hide. + * + * + * + * @param initialVisibility + * @param keepVisibleDelay + * @return + */ + +/** + * Creates and remembers a [MutableState] of Boolean representing visibility, with a delay before hiding. + * + * This composable function manages the visibility state of an element, initially set to `initialVisibility`. + * It utilizes the [KeepVisibleDelay] extension function to introduce a delay before hiding the element, + * ensuring it remains visible for at least the specified `keepVisibleDelay` duration. + * + * @param initialVisibility The initial visibility state of the element. + * @param keepVisibleDelay The duration for which the element should remain visible after a visibility trigger. + * + * @return A [MutableState] of Boolean representing the current visibility state of the element. + */ +@Composable +fun rememberKeepVisibleDelay(initialVisibility: Boolean, keepVisibleDelay: Duration): MutableState { + val controlsVisible = remember(initialVisibility) { mutableStateOf(initialVisibility) } + controlsVisible.KeepVisibleDelay(keepVisibleDelay) + return controlsVisible +} + +/** + * Keeps the value of a [MutableState]<[Boolean]> true for a specified duration. + * + * This composable function is designed to be used with a boolean state variable + * that you want to temporarily set to true and then automatically revert to + * false after a given delay. + * + * @param keepVisibleDelay The duration for which the state value should remain true. + * If this duration is zero or negative, the function does nothing. + * + * [AccessibilityManager.calculateRecommendedTimeoutMillis] can help choosing the right delay for accessibility users. + * + * Usage Example: + * ```kotlin + * var isVisible by remember { mutableStateOf(false) } + * + * // ... Some event triggers isVisible to become true ... + * isVisible = true + * + * isVisible.KeepVisibleDelay(Duration.milliseconds(500)) + * + * // After 500 milliseconds, isVisible will automatically be set back to false. + * ``` + */ +@Composable +fun MutableState.KeepVisibleDelay(keepVisibleDelay: Duration) { + if (keepVisibleDelay <= ZERO) return + LaunchedEffect(value, keepVisibleDelay) { + if (value) { + delay(keepVisibleDelay) + value = false + } + } +} + +@Preview +@Composable +private fun KeepVisibleDelayPreview() { + var duration by remember { mutableStateOf(DefaultKeepDelay) } + var controlsVisible by rememberKeepVisibleDelay(initialVisibility = true, keepVisibleDelay = duration) + + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16 / 9f) + .clickable { controlsVisible = !controlsVisible }, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color.Green) + ) + androidx.compose.animation.AnimatedVisibility( + visible = controlsVisible, + 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.spacedBy(4.dp) + ) { + BasicText( + text = "Show", + modifier = Modifier.clickable { + controlsVisible = true + } + ) + BasicText( + text = "Toggle", + modifier = Modifier.clickable { + controlsVisible = !controlsVisible + } + ) + BasicText( + text = "Hide", + modifier = Modifier.clickable { + controlsVisible = false + } + ) + BasicText( + text = "Disable", + modifier = Modifier.clickable { + duration = ZERO + } + ) + BasicText( + text = "Enable", + modifier = Modifier.clickable { + duration = DefaultKeepDelay + } + ) + } + } +} diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/ToggleableBox.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/ToggleableBox.kt deleted file mode 100644 index 3a56a1e4d..000000000 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/ToggleableBox.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.ui.widget - -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 - -/** - * A Composable function that provides a container with toggleable content. - * - * @param visibilityState The [DelayedVisibilityState] managing the visibility of the toggleable content. - * @param toggleableContent The content to show or hidde based on [DelayedVisibilityState.isVisible]. - * @param modifier The [Modifier] to apply to the layout. - * @param toggleable Whether the content is toggleable or not. - * @param contentAlignment The alignment of the content within the layout. - * @param propagateMinConstraints Whether the incoming minimum constraints should be passed to the content. - * @param enter The `enter` transition to be used when [toggleableContent] becomes visible. - * @param exit The `exit` transition to be used when [toggleableContent] becomes hidden. - * @param content The content to be displayed underneath [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 - } - } - ) - } - } -} From 448e2ce1306424ebcdcd8a79a9e7733b615ae599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 10 Dec 2024 15:24:33 +0100 Subject: [PATCH 2/8] Reset auto hide timer when focus changed --- .../demo/tv/ui/player/compose/PlayerView.kt | 65 +++++++++++++------ .../compose/controls/PlayerPlaybackRow.kt | 24 +------ 2 files changed, 49 insertions(+), 40 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 f958a86bc..b21bf2035 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 @@ -54,6 +54,7 @@ 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.metrics.MetricsOverlay +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 @@ -70,16 +71,16 @@ 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.DefaultKeepDelay 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 import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -92,6 +93,8 @@ import kotlin.time.Duration.Companion.seconds * @param metricsOverlayEnabled * @param metricsOverlayOptions */ + +@Suppress("CyclomaticComplexMethod") @Composable fun PlayerView( player: PillarboxExoPlayer, @@ -100,17 +103,34 @@ 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) DefaultKeepDelay else ZERO + var controlsVisibleState by remember(keepControlDelay) { mutableStateOf(VisibilityState.Visible(keepControlDelay)) } + val controlsVisible = controlsVisibleState is VisibilityState.Visible + LaunchedEffect(controlsVisibleState) { + when (controlsVisibleState) { + is VisibilityState.Visible -> { + if ((controlsVisibleState as VisibilityState.Visible).duration > ZERO) { + delay(duration = keepControlDelay) + controlsVisibleState = VisibilityState.Hidden + } + } + + is VisibilityState.Hidden -> Unit + } + } LaunchedEffect(drawerState.currentValue) { - when (drawerState.currentValue) { - DrawerValue.Closed -> visibilityState.show() - DrawerValue.Open -> visibilityState.hide() + controlsVisibleState = when (drawerState.currentValue) { + DrawerValue.Closed -> VisibilityState.Visible(keepControlDelay) + DrawerValue.Open -> VisibilityState.Hidden } } - BackHandler(enabled = visibilityState.isVisible) { - visibilityState.hide() + BackHandler(enabled = controlsVisible) { + controlsVisibleState = VisibilityState.Hidden } PlaybackSettingsDrawer( @@ -133,7 +153,7 @@ fun PlayerView( .onDpadEvent( eventType = KeyEventType.KeyUp, onEnter = { - visibilityState.show() + controlsVisibleState = VisibilityState.Visible(keepControlDelay) true }, ) @@ -146,7 +166,7 @@ fun PlayerView( Column { ChapterInfo( player = player, - visibilityState = visibilityState, + controlsVisible = controlsVisible, ) if (metricsOverlayEnabled) { @@ -166,7 +186,7 @@ fun PlayerView( } } - if (!visibilityState.isVisible && currentCredit != null) { + if (!controlsVisible && currentCredit != null) { SkipButton( modifier = Modifier .align(Alignment.BottomEnd) @@ -174,17 +194,19 @@ fun PlayerView( onClick = { player.seekTo(currentCredit?.end ?: 0L) }, ) } - AnimatedVisibility( - visible = visibilityState.isVisible, + visible = controlsVisible, modifier = Modifier .fillMaxSize() - .maintainVisibleOnFocus(delayedVisibilityState = visibilityState), + .onFocusChanged { + if (it.isFocused) { + controlsVisibleState = VisibilityState.Visible(keepControlDelay) + } + }, ) { Box { PlayerPlaybackRow( player = player, - state = visibilityState, modifier = Modifier.align(Alignment.Center), ) @@ -221,7 +243,7 @@ fun PlayerView( PlayerTimeRow( player = player, onSeek = { value -> - visibilityState.resetAutoHide() + controlsVisibleState = VisibilityState.Visible(keepControlDelay) player.seekTo(value) }, ) @@ -236,7 +258,7 @@ fun PlayerView( @Composable private fun ChapterInfo( player: Player, - visibilityState: DelayedVisibilityState, + controlsVisible: Boolean, modifier: Modifier = Modifier, ) { val currentMediaMetadata by player.currentMediaMetadataAsState() @@ -255,7 +277,7 @@ private fun ChapterInfo( } AnimatedVisibility( - visible = visibilityState.isVisible || showChapterInfo, + visible = controlsVisible || showChapterInfo, modifier = modifier, enter = expandVertically(), exit = shrinkVertically(), @@ -350,3 +372,8 @@ private fun PlayerTimeRow( onSeekForward = { onSeekProxy(positionMs + player.seekBackIncrement) }, ) } + +internal sealed interface VisibilityState { + class Visible(val duration: Duration = ZERO) : VisibilityState + data object Hidden : VisibilityState +} 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 814e40f61..ca4a6f77d 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,6 @@ 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 @@ -32,43 +31,26 @@ 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() From 385fd11a92a933eb23d4f0622483e45ae90b54ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 11 Dec 2024 15:10:07 +0100 Subject: [PATCH 3/8] Handle controls visibility in demo no more in the library --- .../ui/player/DelayedControlsVisibility.kt | 119 ++++++++---------- .../demo/tv/ui/player/compose/PlayerView.kt | 48 +++---- .../pillarbox/demo/ui/player/PlayerView.kt | 23 ++-- 3 files changed, 81 insertions(+), 109 deletions(-) rename pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt => pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt (50%) diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt similarity index 50% rename from pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt index 98cfc7a36..f4d4a61d8 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/KeepVisibleDelay.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt @@ -2,7 +2,7 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.ui.widget +package ch.srgssr.pillarbox.demo.shared.ui.player import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -18,7 +18,6 @@ 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.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,7 +25,6 @@ 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.platform.AccessibilityManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay @@ -35,86 +33,78 @@ import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.seconds /** - * Default auto-hide duration. - */ -val DefaultKeepDelay = 3.seconds - -/** - * For users that needs accessibility, [keepVisibleDelay] should be increased or set to [Duration.ZERO] to disable auto hide. + * 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 initialVisibility - * @param keepVisibleDelay - * @return + * @param initialVisible The initial visibility of the controls. + * @param initialDelay The initial delay before hiding the controls, in milliseconds. */ +class DelayedControlsVisibility internal constructor(initialVisible: Boolean, initialDelay: Duration) { + /** + * Controls visibility. + */ + var visible by mutableStateOf(initialVisible) -/** - * Creates and remembers a [MutableState] of Boolean representing visibility, with a delay before hiding. - * - * This composable function manages the visibility state of an element, initially set to `initialVisibility`. - * It utilizes the [KeepVisibleDelay] extension function to introduce a delay before hiding the element, - * ensuring it remains visible for at least the specified `keepVisibleDelay` duration. - * - * @param initialVisibility The initial visibility state of the element. - * @param keepVisibleDelay The duration for which the element should remain visible after a visibility trigger. - * - * @return A [MutableState] of Boolean representing the current visibility state of the element. - */ -@Composable -fun rememberKeepVisibleDelay(initialVisibility: Boolean, keepVisibleDelay: Duration): MutableState { - val controlsVisible = remember(initialVisibility) { mutableStateOf(initialVisibility) } - controlsVisible.KeepVisibleDelay(keepVisibleDelay) - return controlsVisible + /** + * 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(null) + + /** + * Resets the ongoing delay. + */ + fun reset() { + if (visible && delay > ZERO) { + reset = Any() + } + } } /** - * Keeps the value of a [MutableState]<[Boolean]> true for a specified duration. - * - * This composable function is designed to be used with a boolean state variable - * that you want to temporarily set to true and then automatically revert to - * false after a given delay. - * - * @param keepVisibleDelay The duration for which the state value should remain true. - * If this duration is zero or negative, the function does nothing. - * - * [AccessibilityManager.calculateRecommendedTimeoutMillis] can help choosing the right delay for accessibility users. + * Remembers and controls the visibility of UI elements with a delay. * - * Usage Example: - * ```kotlin - * var isVisible by remember { mutableStateOf(false) } + * Initially sets visibility to [initialVisible]. If visible, hides after [initialDelay]. * - * // ... Some event triggers isVisible to become true ... - * isVisible = true - * - * isVisible.KeepVisibleDelay(Duration.milliseconds(500)) - * - * // After 500 milliseconds, isVisible will automatically be set back to false. - * ``` + * @param initialVisible Initial visibility. Defaults to false. + * @param initialDelay Delay before hiding, if initially visible. Defaults to 2 seconds. + * @return A [DelayedControlsVisibility] instance to control and observe visibility. */ @Composable -fun MutableState.KeepVisibleDelay(keepVisibleDelay: Duration) { - if (keepVisibleDelay <= ZERO) return - LaunchedEffect(value, keepVisibleDelay) { - if (value) { - delay(keepVisibleDelay) - value = false +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() { - var duration by remember { mutableStateOf(DefaultKeepDelay) } - var controlsVisible by rememberKeepVisibleDelay(initialVisibility = true, keepVisibleDelay = duration) + val visibility = rememberDelayedControlsVisibility(true, 2.seconds) Column { Box( modifier = Modifier .fillMaxWidth() .aspectRatio(16 / 9f) - .clickable { controlsVisible = !controlsVisible }, + .clickable { visibility.visible = !visibility.visible }, ) { Box( modifier = Modifier @@ -122,7 +112,7 @@ private fun KeepVisibleDelayPreview() { .background(color = Color.Green) ) androidx.compose.animation.AnimatedVisibility( - visible = controlsVisible, + visible = visibility.visible, modifier = Modifier .fillMaxSize(), enter = fadeIn(), @@ -143,31 +133,32 @@ private fun KeepVisibleDelayPreview() { BasicText( text = "Show", modifier = Modifier.clickable { - controlsVisible = true + visibility.visible = true + visibility.reset() } ) BasicText( text = "Toggle", modifier = Modifier.clickable { - controlsVisible = !controlsVisible + visibility.visible = !visibility.visible } ) BasicText( text = "Hide", modifier = Modifier.clickable { - controlsVisible = false + visibility.visible = false } ) BasicText( text = "Disable", modifier = Modifier.clickable { - duration = ZERO + visibility.delay = ZERO } ) BasicText( text = "Enable", modifier = Modifier.clickable { - duration = DefaultKeepDelay + visibility.delay = 2.seconds } ) } 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 b21bf2035..7ff404739 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 @@ -53,7 +53,9 @@ 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 @@ -73,14 +75,12 @@ 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.DefaultKeepDelay import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -106,31 +106,18 @@ fun PlayerView( val talkBackEnabled = rememberIsTalkBackEnabled() val isPlaying by player.isPlayingAsState() - val keepControlDelay = if (!talkBackEnabled && isPlaying) DefaultKeepDelay else ZERO - var controlsVisibleState by remember(keepControlDelay) { mutableStateOf(VisibilityState.Visible(keepControlDelay)) } - val controlsVisible = controlsVisibleState is VisibilityState.Visible - LaunchedEffect(controlsVisibleState) { - when (controlsVisibleState) { - is VisibilityState.Visible -> { - if ((controlsVisibleState as VisibilityState.Visible).duration > ZERO) { - delay(duration = keepControlDelay) - controlsVisibleState = VisibilityState.Hidden - } - } - - is VisibilityState.Hidden -> Unit - } - } + val keepControlDelay = if (!talkBackEnabled && isPlaying) DefaultVisibilityDelay else ZERO + val controlsVisibilityState = rememberDelayedControlsVisibility(initialVisible = true, keepControlDelay) LaunchedEffect(drawerState.currentValue) { - controlsVisibleState = when (drawerState.currentValue) { - DrawerValue.Closed -> VisibilityState.Visible(keepControlDelay) - DrawerValue.Open -> VisibilityState.Hidden + controlsVisibilityState.visible = when (drawerState.currentValue) { + DrawerValue.Closed -> true + DrawerValue.Open -> false } } - BackHandler(enabled = controlsVisible) { - controlsVisibleState = VisibilityState.Hidden + BackHandler(enabled = controlsVisibilityState.visible) { + controlsVisibilityState.visible = false } PlaybackSettingsDrawer( @@ -153,7 +140,7 @@ fun PlayerView( .onDpadEvent( eventType = KeyEventType.KeyUp, onEnter = { - controlsVisibleState = VisibilityState.Visible(keepControlDelay) + controlsVisibilityState.visible = true true }, ) @@ -166,7 +153,7 @@ fun PlayerView( Column { ChapterInfo( player = player, - controlsVisible = controlsVisible, + controlsVisible = controlsVisibilityState.visible, ) if (metricsOverlayEnabled) { @@ -186,7 +173,7 @@ fun PlayerView( } } - if (!controlsVisible && currentCredit != null) { + if (!controlsVisibilityState.visible && currentCredit != null) { SkipButton( modifier = Modifier .align(Alignment.BottomEnd) @@ -195,12 +182,12 @@ fun PlayerView( ) } AnimatedVisibility( - visible = controlsVisible, + visible = controlsVisibilityState.visible, modifier = Modifier .fillMaxSize() .onFocusChanged { if (it.isFocused) { - controlsVisibleState = VisibilityState.Visible(keepControlDelay) + controlsVisibilityState.reset() } }, ) { @@ -243,7 +230,7 @@ fun PlayerView( PlayerTimeRow( player = player, onSeek = { value -> - controlsVisibleState = VisibilityState.Visible(keepControlDelay) + controlsVisibilityState.reset() player.seekTo(value) }, ) @@ -372,8 +359,3 @@ private fun PlayerTimeRow( onSeekForward = { onSeekProxy(positionMs + player.seekBackIncrement) }, ) } - -internal sealed interface VisibilityState { - class Visible(val duration: Duration = ZERO) : VisibilityState - data object Hidden : VisibilityState -} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index eceda9df0..7bf7f2968 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged @@ -36,7 +35,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.R +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.ui.player.controls.PlayerControls @@ -56,10 +57,8 @@ import ch.srgssr.pillarbox.ui.extension.hasMediaItemsAsState import ch.srgssr.pillarbox.ui.extension.isPlayingAsState import ch.srgssr.pillarbox.ui.extension.playbackStateAsState import ch.srgssr.pillarbox.ui.extension.playerErrorAsState -import ch.srgssr.pillarbox.ui.widget.DefaultKeepDelay import ch.srgssr.pillarbox.ui.widget.keepScreenOn import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface -import ch.srgssr.pillarbox.ui.widget.rememberKeepVisibleDelay import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds @@ -118,11 +117,11 @@ fun PlayerView( val isSliderDragged by interactionSource.collectIsDraggedAsState() val talkBackEnabled = rememberIsTalkBackEnabled() val isPlaying by player.isPlayingAsState() - val keepControlDelay = if (!talkBackEnabled && !isSliderDragged && isPlaying) DefaultKeepDelay else ZERO - var controlsVisibility by rememberKeepVisibleDelay(initialVisibility = controlsVisible, keepVisibleDelay = keepControlDelay) + val keepControlDelay = if (!talkBackEnabled && !isSliderDragged && isPlaying) DefaultVisibilityDelay else ZERO + val controlsVisibility = rememberDelayedControlsVisibility(initialVisible = controlsVisible, initialDelay = keepControlDelay) val playbackState by player.playbackStateAsState() val isBuffering = playbackState == Player.STATE_BUFFERING - val controlsStateDescription = if (controlsVisibility) { + val controlsStateDescription = if (controlsVisibility.visible) { stringResource(R.string.controls_visible) } else { stringResource(R.string.controls_hidden) @@ -130,10 +129,10 @@ fun PlayerView( Box( modifier = modifier .toggleable( - value = controlsVisibility, + value = controlsVisibility.visible, enabled = controlsToggleable, onValueChange = { - controlsVisibility = !controlsVisibility + controlsVisibility.visible = !controlsVisibility.visible } ) .semantics { @@ -156,7 +155,7 @@ fun PlayerView( } val currentCredit by player.getCurrentCreditAsState() AnimatedVisibility( - visible = currentCredit != null && !controlsVisibility, + visible = currentCredit != null && !controlsVisibility.visible, enter = fadeIn(), exit = fadeOut(), ) { @@ -171,11 +170,11 @@ fun PlayerView( DemoControls( modifier = Modifier .matchParentSize() - .onFocusChanged { if (it.isFocused) controlsVisibility = true } + .onFocusChanged { if (it.isFocused) controlsVisibility.reset() } .onEnterPressed { - controlsVisibility = true + controlsVisibility.visible = true }, - controlsVisible = controlsVisibility, + controlsVisible = controlsVisibility.visible, player = player, progressTracker = progressTracker, interactionSource = interactionSource, From 7ea528381a70a654222e36610e36c8b1f4a20958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 11 Dec 2024 16:03:00 +0100 Subject: [PATCH 4/8] Remove unused dependencies --- pillarbox-ui/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/pillarbox-ui/build.gradle.kts b/pillarbox-ui/build.gradle.kts index d6de4fffe..8dd6d96af 100644 --- a/pillarbox-ui/build.gradle.kts +++ b/pillarbox-ui/build.gradle.kts @@ -14,8 +14,6 @@ dependencies { api(project(":pillarbox-player")) implementation(libs.androidx.annotation) - api(libs.androidx.compose.animation) - implementation(libs.androidx.compose.animation.core) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.foundation) api(libs.androidx.compose.foundation.layout) From 439cf312be9bc1fbc21b4d42c82fb22fd551310e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 16 Dec 2024 09:18:58 +0100 Subject: [PATCH 5/8] Improve Kdoc --- .../demo/shared/ui/player/DelayedControlsVisibility.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt index f4d4a61d8..e002b841f 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/DelayedControlsVisibility.kt @@ -26,7 +26,6 @@ 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.delay import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO @@ -43,7 +42,7 @@ import kotlin.time.Duration.Companion.seconds * This will restart the delay timer. * * @param initialVisible The initial visibility of the controls. - * @param initialDelay The initial delay before hiding the controls, in milliseconds. + * @param initialDelay The initial delay before hiding the controls. */ class DelayedControlsVisibility internal constructor(initialVisible: Boolean, initialDelay: Duration) { /** @@ -74,7 +73,7 @@ class DelayedControlsVisibility internal constructor(initialVisible: Boolean, in * 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 2 seconds. + * @param initialDelay Delay before hiding, if initially visible. Defaults to 3 seconds. * @return A [DelayedControlsVisibility] instance to control and observe visibility. */ @Composable @@ -113,8 +112,7 @@ private fun KeepVisibleDelayPreview() { ) androidx.compose.animation.AnimatedVisibility( visible = visibility.visible, - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), enter = fadeIn(), exit = fadeOut(), ) { @@ -128,7 +126,7 @@ private fun KeepVisibleDelayPreview() { modifier = Modifier .fillMaxWidth() .background(color = Color.White), - horizontalArrangement = Arrangement.spacedBy(4.dp) + horizontalArrangement = Arrangement.SpaceAround ) { BasicText( text = "Show", From c411e470df23acd4ce5cc3e73971845597c652fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Mon, 16 Dec 2024 11:24:00 +0100 Subject: [PATCH 6/8] Remove duplicate extension --- .../pillarbox/demo/ui/player/PlayerView.kt | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 7bf7f2968..47256891d 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -25,15 +25,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.player.DefaultVisibilityDelay import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay @@ -171,9 +167,10 @@ fun PlayerView( modifier = Modifier .matchParentSize() .onFocusChanged { if (it.isFocused) controlsVisibility.reset() } - .onEnterPressed { - controlsVisibility.visible = true - }, + .onDpadEvent(onEnter = { + controlsVisibility.visible = !controlsVisibility.visible + true + }), controlsVisible = controlsVisibility.visible, player = player, progressTracker = progressTracker, @@ -238,17 +235,3 @@ private fun DemoControls( ) } } - -private fun Modifier.onEnterPressed(action: () -> Unit): Modifier { - return this then Modifier.onPreviewKeyEvent { - val isEnterKey = it.key == Key.Enter || it.key == Key.DirectionCenter || it.key == Key.NumPadEnter - val isKeyUp = it.type == KeyEventType.KeyUp - - if (isEnterKey && isKeyUp) { - action() - true - } else { - false - } - } -} From 8d7e6847ab1a982184f2d3dd55ffc541d2691f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Mon, 16 Dec 2024 15:11:26 +0100 Subject: [PATCH 7/8] fix detect --- .../main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 47256891d..fbf83c026 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.media3.common.Player -import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.R +import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent 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 From 34b2a38758f268c41a8d4b1f02c9e6a06f6d8aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Mon, 16 Dec 2024 16:39:55 +0100 Subject: [PATCH 8/8] Move to better location --- .../pillarbox/demo/shared/ui/TalkBack.kt | 24 ++++++++++++ .../demo/shared/ui/TalkBackEnabled.kt | 37 ------------------- 2 files changed, 24 insertions(+), 37 deletions(-) delete mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt index 6e48ee580..6bcae4c21 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt @@ -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() ?: 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 +} diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt deleted file mode 100644 index f660849b8..000000000 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBackEnabled.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.shared.ui - -import android.view.accessibility.AccessibilityManager -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.getSystemService - -/** - * 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() ?: 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 -}