refactor: improve navigation animations
This commit is contained in:
+33
-17
@@ -1,5 +1,10 @@
|
||||
package bzh.ajaury.chombev.player.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -178,30 +183,41 @@ private fun PlayerTopBar(
|
||||
TopAppBar(
|
||||
modifier = Modifier.shadow(elevation = 2.dp),
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = uiState.recordEmoji,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
Column {
|
||||
// Animate the title in as it loads, and between records: the record data isn't there on
|
||||
// the first frame, so fade + rise it into place once uiState carries it.
|
||||
AnimatedContent(
|
||||
targetState = uiState.recordEmoji to uiState.recordTitle,
|
||||
transitionSpec = {
|
||||
fadeIn(tween()) togetherWith
|
||||
fadeOut(tween())
|
||||
},
|
||||
label = "PlayerTitle",
|
||||
) { (emoji, title) ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = uiState.recordTitle.transcription,
|
||||
text = emoji,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
uiState.recordTitle.translation?.let { translation ->
|
||||
Column {
|
||||
Text(
|
||||
text = translation,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
text = title.transcription,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
title.translation?.let { translation ->
|
||||
Text(
|
||||
text = translation,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +69,15 @@ fun SubtitleList(
|
||||
targetIndex = it
|
||||
}
|
||||
} else {
|
||||
currentIndex ?: -1
|
||||
currentIndex ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-follow: re-center the playing line when it changes, unless the user is scrolling.
|
||||
LaunchedEffect(currentIndex) {
|
||||
if (currentIndex != null && !listState.isScrollInProgress) {
|
||||
listState.centerItem(currentIndex)
|
||||
if (!listState.isScrollInProgress) {
|
||||
listState.centerItem(currentIndex ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ private fun LazyListState.getCenteredIndex(): Int {
|
||||
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f
|
||||
info.visibleItemsInfo
|
||||
.minByOrNull {
|
||||
abs((it.offset + it.size / 2f) - viewportCenter)
|
||||
abs((it.offset) - viewportCenter)
|
||||
}?.index
|
||||
?: -1
|
||||
}
|
||||
@@ -147,7 +147,7 @@ private suspend fun LazyListState.centerItem(index: Int) {
|
||||
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2
|
||||
val item = info.visibleItemsInfo.firstOrNull { it.index == index }
|
||||
if (item != null) {
|
||||
this.animateScrollBy((item.offset + item.size / 2 - viewportCenter).toFloat())
|
||||
this.animateScrollBy((item.offset - viewportCenter).toFloat())
|
||||
} else {
|
||||
this.animateScrollToItem(index)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package bzh.ajaury.chombev
|
||||
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.saveable.rememberSerializable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.metadata
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer
|
||||
import bzh.ajaury.chombev.core.coroutines.di.coroutinesModule
|
||||
@@ -70,7 +76,28 @@ fun App() {
|
||||
}
|
||||
}
|
||||
|
||||
is Route.Player -> NavEntry(key) {
|
||||
is Route.Player -> NavEntry(
|
||||
key,
|
||||
metadata = metadata {
|
||||
put(NavDisplay.TransitionKey) {
|
||||
// Slide new content up, keeping the old content in place underneath
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it },
|
||||
animationSpec = tween(400),
|
||||
) togetherWith ExitTransition.KeepUntilTransitionsFinished
|
||||
}
|
||||
put(NavDisplay.PopTransitionKey) {
|
||||
// Slide in from left when navigating back
|
||||
slideInHorizontally(initialOffsetX = { -it }) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it })
|
||||
}
|
||||
put(NavDisplay.PredictivePopTransitionKey) {
|
||||
// Slide in from left when navigating back
|
||||
slideInHorizontally(initialOffsetX = { -it }) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it })
|
||||
}
|
||||
},
|
||||
) {
|
||||
PlayerScreen(
|
||||
recordId = key.recordId,
|
||||
onBackClicked = { backStack.removeLastOrNull() },
|
||||
|
||||
Reference in New Issue
Block a user