Skip to content

Commit

Permalink
Improve controls usability in the TV demo (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
MGaetan89 authored May 1, 2024
1 parent 893c340 commit 6a6588a
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 eventType The event type to check before calling the provided actions.
* @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.
Expand All @@ -23,6 +24,7 @@ import androidx.compose.ui.input.key.type
* @param onBack The action to perform when the back button is press.
*/
fun Modifier.onDpadEvent(
eventType: KeyEventType = KeyEventType.KeyDown,
onLeft: () -> Boolean = { false },
onUp: () -> Boolean = { false },
onRight: () -> Boolean = { false },
Expand All @@ -31,7 +33,7 @@ fun Modifier.onDpadEvent(
onBack: () -> Boolean = { false }
): Modifier {
return onPreviewKeyEvent {
if (it.type == KeyEventType.KeyDown) {
if (it.type == eventType) {
when (it.key) {
Key.DirectionLeft,
Key.SystemNavigationLeft,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@
*/
package ch.srgssr.pillarbox.demo.tv.ui.player.compose

import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
Expand All @@ -24,6 +29,7 @@ import androidx.media3.common.MediaMetadata
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme
import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings
import coil.compose.AsyncImage

Expand All @@ -39,18 +45,33 @@ fun MediaMetadataView(
mediaMetadata: MediaMetadata,
modifier: Modifier = Modifier,
) {
Row(modifier.background(color = Color.Black)) {
Row(
modifier = modifier
.background(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black),
),
),
verticalAlignment = Alignment.Bottom,
) {
AsyncImage(
modifier = Modifier
.padding(MaterialTheme.paddings.small)
.clip(RoundedCornerShape(MaterialTheme.paddings.small))
.width(200.dp)
.aspectRatio(16 / 9f),
contentScale = ContentScale.Fit,
model = mediaMetadata.artworkUri,
contentDescription = null,
)

Column(
verticalArrangement = Arrangement.Bottom,
modifier = Modifier.padding(MaterialTheme.paddings.mini)
modifier = Modifier.padding(
start = MaterialTheme.paddings.mini,
top = MaterialTheme.paddings.small,
end = 72.dp, // baseline + 56dp to not overlap with the settings button
bottom = MaterialTheme.paddings.small,
)
) {
Text(
text = mediaMetadata.title?.toString() ?: "No title",
Expand All @@ -59,11 +80,12 @@ fun MediaMetadataView(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

mediaMetadata.description?.let {
Text(
text = mediaMetadata.description.toString(),
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Expand All @@ -72,9 +94,22 @@ fun MediaMetadataView(
}
}

@Preview(device = Devices.TV_1080p)
@Composable
@Preview(device = Devices.TV_1080p)
@Suppress("MaximumLineLength", "MaxLineLength")
private fun MediaMetadataPreview() {
val mediaMetadata = MediaMetadata.Builder().setTitle("Title").setDescription("Description").build()
MediaMetadataView(mediaMetadata = mediaMetadata, modifier = Modifier.fillMaxSize())
PillarboxTheme {
val mediaMetadata = MediaMetadata.Builder()
.setTitle("Title")
.setDescription("Description")
.setArtworkUri(Uri.parse("https://cdn.prod.swi-services.ch/video-delivery/images/14e4562f-725d-4e41-a200-7fcaa77df2fe/5rwf1Bq_m3GC5secOZcIcgbbrbZPf4nI/16x9)"))
.build()

MediaMetadataView(
mediaMetadata = mediaMetadata,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
package ch.srgssr.pillarbox.demo.tv.ui.player.compose

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -24,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.tv.material3.Button
Expand Down Expand Up @@ -88,6 +93,7 @@ fun PlayerView(
modifier = Modifier
.fillMaxSize()
.onDpadEvent(
eventType = KeyEventType.KeyUp,
onEnter = {
visibilityState.show()
true
Expand All @@ -114,7 +120,7 @@ fun PlayerView(
currentChapter?.let {
MediaMetadataView(
modifier = Modifier
.fillMaxWidth(0.5f)
.fillMaxWidth()
.wrapContentHeight()
.align(Alignment.BottomStart),
mediaMetadata = it.mediaMetadata
Expand Down Expand Up @@ -147,13 +153,19 @@ fun PlayerView(
)

val currentMediaMetadata by player.currentMediaMetadataAsState()
MediaMetadataView(
AnimatedContent(
targetState = currentChapter?.mediaMetadata ?: currentMediaMetadata,
modifier = Modifier
.fillMaxWidth(0.5f)
.fillMaxWidth()
.wrapContentHeight()
.align(Alignment.BottomStart),
mediaMetadata = currentChapter?.mediaMetadata ?: currentMediaMetadata
)
transitionSpec = {
slideInHorizontally { it }
.togetherWith(slideOutHorizontally { -it })
}
) { mediaMetadata ->
MediaMetadataView(mediaMetadata)
}

IconButton(
onClick = { drawerState.setValue(DrawerValue.Open) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.IconButton
import androidx.tv.material3.MaterialTheme
import ch.srgssr.pillarbox.demo.tv.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
Expand All @@ -50,13 +51,25 @@ fun PlayerPlaybackRow(
) {
val isPlaying by player.isPlayingAsState()
val focusRequester = remember { FocusRequester() }
val resetAutoHideCallback = remember {
{
state.resetAutoHide()
false
}
}

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

Row(
modifier = modifier,
modifier = modifier.onDpadEvent(
onLeft = resetAutoHideCallback,
onRight = resetAutoHideCallback,
onEnter = resetAutoHideCallback,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline),
) {
val availableCommands by player.availableCommandsAsState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,18 @@ class DelayedVisibilityState internal constructor(
initialVisible: Boolean = true,
initialDuration: Duration = DefaultDuration
) {
internal var state by mutableStateOf(DelayedVisibility(initialVisible, initialDuration))
internal var autoHideResetTrigger by mutableStateOf(false)
private set

/**
* Visible
*/
var isVisible: Boolean
get() = state.visible
set(value) = setVisible(visible = value, duration = duration)
var isVisible by mutableStateOf(initialVisible)

/**
* Duration
*/
var duration: Duration
get() = state.duration
set(value) = setVisible(visible = isVisible, duration = value)

private fun setVisible(visible: Boolean, duration: Duration = DefaultDuration) {
state = DelayedVisibility(visible, duration)
}
var duration by mutableStateOf(initialDuration)

/**
* Toggle
Expand All @@ -95,7 +88,14 @@ class DelayedVisibilityState internal constructor(
* Disable auto hide
*/
fun disableAutoHide() {
duration = ZERO
duration = DisabledDuration
}

/**
* Reset the auto hide countdown
*/
fun resetAutoHide() {
autoHideResetTrigger = !autoHideResetTrigger
}

/**
Expand All @@ -105,11 +105,6 @@ class DelayedVisibilityState internal constructor(
return duration < INFINITE && duration > ZERO
}

internal class DelayedVisibility(
val visible: Boolean = true,
val duration: Duration = DefaultDuration
)

companion object {
/**
* Default duration
Expand Down Expand Up @@ -183,13 +178,11 @@ fun Modifier.toggleable(
* @param delayedVisibilityState the delayed visibility state to link
*/
fun Modifier.maintainVisibleOnFocus(delayedVisibilityState: DelayedVisibilityState): Modifier {
return this.then(
Modifier.onFocusChanged {
if (it.isFocused) {
delayedVisibilityState.show()
}
return onFocusChanged {
if (it.isFocused) {
delayedVisibilityState.show()
}
)
}
}

/**
Expand Down Expand Up @@ -266,7 +259,7 @@ fun rememberDelayedVisibilityState(
delayedVisibilityState.isVisible = visible
}

LaunchedEffect(delayedVisibilityState.state) {
LaunchedEffect(delayedVisibilityState.isVisible, delayedVisibilityState.duration, delayedVisibilityState.autoHideResetTrigger) {
if (delayedVisibilityState.isVisible && delayedVisibilityState.isAutoHideEnabled()) {
delay(delayedVisibilityState.duration)
delayedVisibilityState.hide()
Expand Down

0 comments on commit 6a6588a

Please sign in to comment.