mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Better playback overlay appearance animations (#1130)
## Description Updates the playback overlay so that individual component animate in more deliberately. For example, the logo & clock slide in from the top instead of the middle of the screen. And the trick play preview now expands and shrinks vertically. ### Related issues I thought there was an issue about this, but I can't find it ### Testing Emulator, nvidia shield ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
920733075b
commit
8a37d63d09
2 changed files with 95 additions and 74 deletions
|
|
@ -1,10 +1,17 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.animation.slideIn
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOut
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
|
@ -50,6 +57,9 @@ import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
|
@ -119,17 +129,14 @@ fun PlaybackOverlay(
|
||||||
var seekProgressMs by remember(seekBarFocused) { mutableLongStateOf(playerControls.currentPosition) }
|
var seekProgressMs by remember(seekBarFocused) { mutableLongStateOf(playerControls.currentPosition) }
|
||||||
var seekProgressPercent = (seekProgressMs.toDouble() / playerControls.duration).toFloat()
|
var seekProgressPercent = (seekProgressMs.toDouble() / playerControls.duration).toFloat()
|
||||||
|
|
||||||
val chapterInteractionSources =
|
|
||||||
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val titleHeight =
|
val titleHeight =
|
||||||
remember {
|
remember(item?.title) {
|
||||||
if (item?.title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp
|
if (item?.title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp
|
||||||
}
|
}
|
||||||
val subtitleHeight =
|
val subtitleHeight =
|
||||||
remember {
|
remember(item?.subtitleLong) {
|
||||||
if (item?.subtitleLong.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp
|
if (item?.subtitleLong.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,19 +144,6 @@ fun PlaybackOverlay(
|
||||||
var controllerHeight by remember { mutableStateOf(0.dp) }
|
var controllerHeight by remember { mutableStateOf(0.dp) }
|
||||||
var state by remember { mutableStateOf(OverlayViewState.CONTROLLER) }
|
var state by remember { mutableStateOf(OverlayViewState.CONTROLLER) }
|
||||||
|
|
||||||
// Background scrim for OSD readability
|
|
||||||
val scrimBrush =
|
|
||||||
remember {
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors =
|
|
||||||
listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.5f),
|
|
||||||
Color.Black.copy(alpha = 0.80f),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentAlignment = Alignment.BottomCenter,
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
|
@ -160,6 +154,18 @@ fun PlaybackOverlay(
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier.matchParentSize(),
|
modifier = Modifier.matchParentSize(),
|
||||||
) {
|
) {
|
||||||
|
// Background scrim for OSD readability
|
||||||
|
val scrimBrush =
|
||||||
|
remember {
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors =
|
||||||
|
listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.5f),
|
||||||
|
Color.Black.copy(alpha = 0.80f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -169,10 +175,16 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
state == OverlayViewState.CONTROLLER,
|
visible = controllerViewState.controlsVisible && state == OverlayViewState.CONTROLLER,
|
||||||
enter = slideInVertically() + fadeIn(),
|
enter = slideInVertically { it / 2 } + fadeIn(),
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
if (seekBarFocused) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
seekProgressPercent =
|
||||||
|
(playerControls.currentPosition.toFloat() / playerControls.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
val nextState =
|
val nextState =
|
||||||
if (chapters.isNotEmpty()) {
|
if (chapters.isNotEmpty()) {
|
||||||
OverlayViewState.CHAPTERS
|
OverlayViewState.CHAPTERS
|
||||||
|
|
@ -253,11 +265,13 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
state == OverlayViewState.CHAPTERS,
|
visible = controllerViewState.controlsVisible && state == OverlayViewState.CHAPTERS,
|
||||||
enter = slideInVertically { it / 2 } + fadeIn(),
|
enter = slideInVertically { it / 2 } + fadeIn(),
|
||||||
exit = slideOutVertically { it / 2 } + fadeOut(),
|
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||||
) {
|
) {
|
||||||
if (chapters.isNotEmpty()) {
|
if (chapters.isNotEmpty()) {
|
||||||
|
val chapterInteractionSources =
|
||||||
|
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
val chapterIndex =
|
val chapterIndex =
|
||||||
remember {
|
remember {
|
||||||
|
|
@ -368,7 +382,7 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
state == OverlayViewState.QUEUE,
|
visible = controllerViewState.controlsVisible && state == OverlayViewState.QUEUE,
|
||||||
enter = slideInVertically { it / 2 } + fadeIn(),
|
enter = slideInVertically { it / 2 } + fadeIn(),
|
||||||
exit = slideOutVertically { it / 2 } + fadeOut(),
|
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -441,15 +455,17 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seekBarInteractionSource.collectIsFocusedAsState().value) {
|
// Trickplay
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
seekProgressPercent =
|
|
||||||
(playerControls.currentPosition.toFloat() / playerControls.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
seekProgressPercent >= 0 && seekBarFocused,
|
visible = controllerViewState.controlsVisible && seekProgressPercent >= 0 && seekBarFocused,
|
||||||
|
enter =
|
||||||
|
expandVertically(
|
||||||
|
spring(
|
||||||
|
stiffness = Spring.StiffnessMedium,
|
||||||
|
visibilityThreshold = IntSize.VisibilityThreshold,
|
||||||
|
),
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = shrinkVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -496,9 +512,13 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top
|
||||||
val logoImageUrl = LocalImageUrlService.current.rememberImageUrl(item, ImageType.LOGO)
|
val logoImageUrl = LocalImageUrlService.current.rememberImageUrl(item, ImageType.LOGO)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
!showDebugInfo && logoImageUrl.isNotNullOrBlank() && controllerViewState.controlsVisible,
|
visible = !showDebugInfo && logoImageUrl.isNotNullOrBlank() && controllerViewState.controlsVisible,
|
||||||
|
enter = slideIn { IntOffset(x = -it.width / 2, y = -it.height / 2) } + fadeIn(),
|
||||||
|
exit = slideOut { IntOffset(x = -it.width / 2, y = -it.height / 2) } + fadeOut(),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopStart),
|
.align(Alignment.TopStart),
|
||||||
|
|
@ -514,7 +534,9 @@ fun PlaybackOverlay(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
!showDebugInfo && showClock && controllerViewState.controlsVisible,
|
visible = !showDebugInfo && showClock && controllerViewState.controlsVisible,
|
||||||
|
enter = slideIn { IntOffset(x = it.width / 2, y = -it.height / 2) } + fadeIn(),
|
||||||
|
exit = slideOut { IntOffset(x = it.width / 2, y = -it.height / 2) } + fadeOut(),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopEnd),
|
.align(Alignment.TopEnd),
|
||||||
|
|
@ -522,7 +544,9 @@ fun PlaybackOverlay(
|
||||||
TimeDisplay()
|
TimeDisplay()
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
showDebugInfo && controllerViewState.controlsVisible,
|
visible = showDebugInfo && controllerViewState.controlsVisible,
|
||||||
|
enter = slideInVertically() + fadeIn(),
|
||||||
|
exit = slideOutVertically() + fadeOut(),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopStart),
|
.align(Alignment.TopStart),
|
||||||
|
|
@ -578,6 +602,11 @@ fun Controller(
|
||||||
val verticalOffset by animateDpAsState(
|
val verticalOffset by animateDpAsState(
|
||||||
targetValue = if (seekBarFocused) (-32).dp else 0.dp,
|
targetValue = if (seekBarFocused) (-32).dp else 0.dp,
|
||||||
label = "TitleBumpOffset",
|
label = "TitleBumpOffset",
|
||||||
|
animationSpec =
|
||||||
|
spring(
|
||||||
|
stiffness = Spring.StiffnessMediumLow,
|
||||||
|
visibilityThreshold = Dp.VisibilityThreshold,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import androidx.annotation.Dimension
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -364,44 +362,38 @@ fun PlaybackPageContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
// The playback controls
|
// The playback controls
|
||||||
AnimatedVisibility(
|
|
||||||
controllerViewState.controlsVisible,
|
PlaybackOverlay(
|
||||||
Modifier,
|
modifier =
|
||||||
slideInVertically { it },
|
Modifier
|
||||||
slideOutVertically { it },
|
.padding(WindowInsets.systemBars.asPaddingValues())
|
||||||
) {
|
.fillMaxSize()
|
||||||
PlaybackOverlay(
|
.background(Color.Transparent),
|
||||||
modifier =
|
item = currentPlayback?.item,
|
||||||
Modifier
|
playerControls = player,
|
||||||
.padding(WindowInsets.systemBars.asPaddingValues())
|
controllerViewState = controllerViewState,
|
||||||
.fillMaxSize()
|
showPlay = playPauseState.showPlay,
|
||||||
.background(Color.Transparent),
|
previousEnabled = true,
|
||||||
item = currentPlayback?.item,
|
nextEnabled = playlist.hasNext(),
|
||||||
playerControls = player,
|
seekEnabled = true,
|
||||||
controllerViewState = controllerViewState,
|
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||||
showPlay = playPauseState.showPlay,
|
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||||
previousEnabled = true,
|
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
||||||
nextEnabled = playlist.hasNext(),
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
seekEnabled = true,
|
onClickPlaybackDialogType = { playbackDialog = it },
|
||||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
onSeekBarChange = seekBarState::onValueChange,
|
||||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
showDebugInfo = showDebugInfo,
|
||||||
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
currentPlayback = currentPlayback,
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
chapters = mediaInfo?.chapters ?: listOf(),
|
||||||
onClickPlaybackDialogType = { playbackDialog = it },
|
trickplayInfo = mediaInfo?.trickPlayInfo,
|
||||||
onSeekBarChange = seekBarState::onValueChange,
|
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||||
showDebugInfo = showDebugInfo,
|
playlist = playlist,
|
||||||
currentPlayback = currentPlayback,
|
onClickPlaylist = {
|
||||||
chapters = mediaInfo?.chapters ?: listOf(),
|
viewModel.playItemInPlaylist(it)
|
||||||
trickplayInfo = mediaInfo?.trickPlayInfo,
|
},
|
||||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
currentSegment = currentSegment?.segment,
|
||||||
playlist = playlist,
|
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||||
onClickPlaylist = {
|
)
|
||||||
viewModel.playItemInPlaylist(it)
|
|
||||||
},
|
|
||||||
currentSegment = currentSegment?.segment,
|
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subtitleSettings =
|
val subtitleSettings =
|
||||||
remember(mediaInfo) {
|
remember(mediaInfo) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue