diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt index f5139520f..4aefa284b 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt @@ -30,11 +30,6 @@ import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush 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.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalFocusManager @@ -59,6 +54,7 @@ import androidx.tv.material3.Text import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.ui.NavigationRoutes +import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import coil.compose.AsyncImage @@ -212,11 +208,16 @@ private fun ExamplesSection( columns = TvGridCells.Fixed(columnCount), modifier = Modifier .focusRestorer() - .onPreviewKeyEvent { - if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && isOnFirstRow) { - focusedIndex = -1 - focusManager.moveFocus(FocusDirection.Up) - } else if (it.key == Key.Back && it.type == KeyEventType.KeyDown) { + .onDpadEvent( + onUp = { + if (isOnFirstRow) { + focusedIndex = -1 + focusManager.moveFocus(FocusDirection.Up) + } else { + false + } + }, + onBack = { if (!isOnFirstRow) { focusedIndex = 0 @@ -232,10 +233,8 @@ private fun ExamplesSection( } else { false } - } else { - false } - }, + ), state = scrollState, contentPadding = PaddingValues(vertical = MaterialTheme.paddings.baseline), verticalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline), diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt new file mode 100644 index 000000000..69bb0ae64 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/extension/ModifierExtensions.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.extension + +import androidx.compose.ui.Modifier +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 + +/** + * This [Modifier] allows you to define actions to perform when a button of the D-pad or the back button is press. Each action returns a [Boolean] + * to indicate if the event was handled or not. + * + * @param onLeft The action to perform when the left button is press. + * @param onUp The action to perform when the up button is press. + * @param onRight The action to perform when the right button is press. + * @param onDown The action to perform when the down button is press. + * @param onEnter The action to perform when the enter button is press. + * @param onBack The action to perform when the back button is press. + */ +fun Modifier.onDpadEvent( + onLeft: () -> Boolean = { false }, + onUp: () -> Boolean = { false }, + onRight: () -> Boolean = { false }, + onDown: () -> Boolean = { false }, + onEnter: () -> Boolean = { false }, + onBack: () -> Boolean = { false } +): Modifier { + return onPreviewKeyEvent { + if (it.type == KeyEventType.KeyDown) { + when (it.key) { + Key.DirectionLeft, + Key.SystemNavigationLeft, + -> onLeft() + + Key.DirectionUp, + Key.SystemNavigationUp, + -> onUp() + + Key.DirectionRight, + Key.SystemNavigationRight, + -> onRight() + + Key.DirectionDown, + Key.SystemNavigationDown, + -> onDown() + + Key.Enter, + Key.DirectionCenter, + Key.NumPadEnter, + -> onEnter() + + Key.Back -> onBack() + + else -> false + } + } else { + false + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt index 9e5a1c146..565dd5e32 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt @@ -28,8 +28,8 @@ import androidx.tv.material3.IconButton import androidx.tv.material3.MaterialTheme import androidx.tv.material3.rememberDrawerState import ch.srgssr.pillarbox.demo.shared.R +import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings -import ch.srgssr.pillarbox.ui.extension.handleDPadKeyEvents import ch.srgssr.pillarbox.ui.extension.playerErrorAsState import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface @@ -70,9 +70,12 @@ fun TvPlayerView( player = player, modifier = Modifier .fillMaxSize() - .handleDPadKeyEvents(onEnter = { - visibilityState.show() - }) + .onDpadEvent( + onEnter = { + visibilityState.show() + true + } + ) .focusable(true) ) AnimatedVisibility( diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/TVDemoTopBar.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/TVDemoTopBar.kt index 5f0cf7d3d..711486d5e 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/TVDemoTopBar.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/TVDemoTopBar.kt @@ -18,11 +18,6 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer -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.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -34,6 +29,7 @@ import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.Text import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination +import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings @@ -68,16 +64,14 @@ fun TVDemoTopBar( selectedTabIndex = focusedTabIndex, modifier = modifier .focusRestorer() - .onPreviewKeyEvent { - if (it.key == Key.DirectionRight && it.type == KeyEventType.KeyDown) { + .onDpadEvent( + onRight = { if (focusedTabIndex < destinations.lastIndex) { focusManager.moveFocus(FocusDirection.Right) } true - } else { - false } - } + ) ) { destinations.forEachIndexed { index, destination -> key(index) { diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/ListsHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/ListsHome.kt index 408eb3398..d48234a01 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/ListsHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/ListsHome.kt @@ -35,11 +35,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow -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.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext @@ -93,6 +88,7 @@ import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ContentListSecti import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListFactories import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections import ch.srgssr.pillarbox.demo.tv.R +import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.player.PlayerActivity import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings @@ -285,8 +281,8 @@ private fun ListsSection( columns = TvGridCells.Fixed(columnCount), modifier = Modifier .focusRestorer() - .onKeyEvent( - onUpPress = { + .onDpadEvent( + onUp = { if (isOnFirstRow) { focusedIndex = -1 focusManager.moveFocus(FocusDirection.Up) @@ -294,7 +290,7 @@ private fun ListsSection( false } }, - onBackPress = { + onBack = { if (!isOnFirstRow) { focusedIndex = 0 @@ -451,8 +447,8 @@ private fun ListsSectionContent( columns = TvGridCells.Fixed(columnCount), modifier = modifier .focusRestorer() - .onKeyEvent( - onUpPress = { + .onDpadEvent( + onUp = { if (isOnFirstRow) { focusedIndex = -1 focusManager.moveFocus(FocusDirection.Up) @@ -460,7 +456,7 @@ private fun ListsSectionContent( false } }, - onBackPress = { + onBack = { if (!isOnFirstRow) { focusedIndex = 0 @@ -703,23 +699,6 @@ private fun ListsSectionError( } } -private fun Modifier.onKeyEvent( - onUpPress: () -> Boolean, - onBackPress: () -> Boolean -): Modifier { - return this then Modifier.onPreviewKeyEvent { keyEvent -> - if (keyEvent.type == KeyEventType.KeyDown) { - when (keyEvent.key) { - Key.DirectionUp -> onUpPress() - Key.Back -> onBackPress() - else -> false - } - } else { - false - } - } -} - @Preview @Composable private fun ContentListsViewPreview() { diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/SearchView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/SearchView.kt index 867125645..7d45113c1 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/SearchView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/integrationLayer/SearchView.kt @@ -27,11 +27,6 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -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.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -52,6 +47,7 @@ import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.bus +import ch.srgssr.pillarbox.demo.tv.extension.onDpadEvent import ch.srgssr.pillarbox.demo.tv.player.PlayerActivity import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings @@ -132,14 +128,12 @@ private fun SearchRow( query = query, modifier = Modifier .fillMaxWidth() - .onPreviewKeyEvent { - if (it.key == Key.Back && it.type == KeyEventType.KeyDown) { + .onDpadEvent( + onBack = { focusManager.moveFocus(FocusDirection.Up) true - } else { - false } - }, + ), onQueryChange = onQueryChange ) diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt deleted file mode 100644 index 499a8654d..000000000 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/DPadExtensions.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.ui.extension - -import androidx.compose.ui.Modifier -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 - -private val DPadEventsKeyCodes = listOf( - Key.DirectionLeft, - Key.SystemNavigationLeft, - Key.DirectionUp, - Key.SystemNavigationUp, - Key.DirectionRight, - Key.SystemNavigationRight, - Key.DirectionDown, - Key.SystemNavigationDown, - Key.DirectionCenter, - Key.Enter, - Key.NumPadEnter, -) - -/** - * Handle d pad key events - * - * @param onLeft action when left button is pressed. - * @param onUp action when up button is pressed. - * @param onRight action when right button is pressed. - * @param onDown action when down button is pressed. - * @param onEnter action when enter button is pressed. - */ -fun Modifier.handleDPadKeyEvents( - onLeft: (() -> Unit)? = null, - onUp: (() -> Unit)? = null, - onRight: (() -> Unit)? = null, - onDown: (() -> Unit)? = null, - onEnter: (() -> Unit)? = null, -) = onPreviewKeyEvent { - if (it.type != KeyEventType.KeyUp || it.key !in DPadEventsKeyCodes) { - return@onPreviewKeyEvent false - } - - when (it.key) { - Key.DirectionLeft, - Key.SystemNavigationLeft, - -> onLeft?.let { - it() - return@onPreviewKeyEvent true - } - - Key.DirectionUp, - Key.SystemNavigationUp, - -> onUp?.let { - it() - return@onPreviewKeyEvent true - } - - Key.DirectionRight, - Key.SystemNavigationRight, - -> onRight?.let { - it() - return@onPreviewKeyEvent true - } - - Key.DirectionDown, - Key.SystemNavigationDown, - -> onDown?.let { - it() - return@onPreviewKeyEvent true - } - - Key.Enter, - Key.DirectionCenter, - Key.NumPadEnter, - -> onEnter?.let { - it() - return@onPreviewKeyEvent true - } - } - - 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 index 40ad3a9d8..699de61f7 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/DelayedVisibilityState.kt @@ -23,11 +23,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.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.handleDPadKeyEvents import ch.srgssr.pillarbox.ui.extension.playbackStateAsState import kotlinx.coroutines.delay import kotlin.time.Duration @@ -131,7 +135,7 @@ fun Modifier.toggleable( role: Role? = Role.Switch, delayedVisibilityState: DelayedVisibilityState ): Modifier = composed { - Modifier.toggleable( + toggleable( enabled = enabled, role = role, interactionSource = remember { @@ -169,9 +173,7 @@ fun Modifier.toggleable( delayedVisibilityState.isVisible = it } ) - .handleDPadKeyEvents(onEnter = { - delayedVisibilityState.toggle() - }) + .onEnterPressed(delayedVisibilityState::toggle) .focusable(enabled = enabled) ) @@ -273,3 +275,17 @@ fun rememberDelayedVisibilityState( 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/ToggleableBox.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/ToggleableBox.kt index 55c8c2458..f8f7689fe 100644 --- 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 @@ -38,7 +38,7 @@ import kotlin.time.Duration * Toggle view * * @param visibilityState A state that holds the current visibility and auto hide delay mode. - * @param toggleableContent Content to show or hide based on the value of [ToggleVisibilityState.isDisplayed]. + * @param toggleableContent Content to show or hide based on the value of [DelayedVisibilityState.isVisible]. * @param modifier modifier for the Layout created to contain the content. * @param toggleable content is toggleable. * @param contentAlignment - The default alignment inside the Box.