refactor: improve navigation animations
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
package bzh.ajaury.chombev.player.ui
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -178,23 +183,33 @@ private fun PlayerTopBar(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.shadow(elevation = 2.dp),
|
modifier = Modifier.shadow(elevation = 2.dp),
|
||||||
title = {
|
title = {
|
||||||
|
// 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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = uiState.recordEmoji,
|
text = emoji,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = uiState.recordTitle.transcription,
|
text = title.transcription,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
||||||
uiState.recordTitle.translation?.let { translation ->
|
title.translation?.let { translation ->
|
||||||
Text(
|
Text(
|
||||||
text = translation,
|
text = translation,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -205,6 +220,7 @@ private fun PlayerTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackClicked) {
|
IconButton(onClick = onBackClicked) {
|
||||||
|
|||||||
@@ -69,15 +69,15 @@ fun SubtitleList(
|
|||||||
targetIndex = it
|
targetIndex = it
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentIndex ?: -1
|
currentIndex ?: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-follow: re-center the playing line when it changes, unless the user is scrolling.
|
// Auto-follow: re-center the playing line when it changes, unless the user is scrolling.
|
||||||
LaunchedEffect(currentIndex) {
|
LaunchedEffect(currentIndex) {
|
||||||
if (currentIndex != null && !listState.isScrollInProgress) {
|
if (!listState.isScrollInProgress) {
|
||||||
listState.centerItem(currentIndex)
|
listState.centerItem(currentIndex ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ private fun LazyListState.getCenteredIndex(): Int {
|
|||||||
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f
|
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f
|
||||||
info.visibleItemsInfo
|
info.visibleItemsInfo
|
||||||
.minByOrNull {
|
.minByOrNull {
|
||||||
abs((it.offset + it.size / 2f) - viewportCenter)
|
abs((it.offset) - viewportCenter)
|
||||||
}?.index
|
}?.index
|
||||||
?: -1
|
?: -1
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ private suspend fun LazyListState.centerItem(index: Int) {
|
|||||||
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2
|
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2
|
||||||
val item = info.visibleItemsInfo.firstOrNull { it.index == index }
|
val item = info.visibleItemsInfo.firstOrNull { it.index == index }
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
this.animateScrollBy((item.offset + item.size / 2 - viewportCenter).toFloat())
|
this.animateScrollBy((item.offset - viewportCenter).toFloat())
|
||||||
} else {
|
} else {
|
||||||
this.animateScrollToItem(index)
|
this.animateScrollToItem(index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package bzh.ajaury.chombev
|
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.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.saveable.rememberSerializable
|
import androidx.compose.runtime.saveable.rememberSerializable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.navigation3.runtime.NavEntry
|
import androidx.navigation3.runtime.NavEntry
|
||||||
|
import androidx.navigation3.runtime.metadata
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer
|
import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer
|
||||||
import bzh.ajaury.chombev.core.coroutines.di.coroutinesModule
|
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(
|
PlayerScreen(
|
||||||
recordId = key.recordId,
|
recordId = key.recordId,
|
||||||
onBackClicked = { backStack.removeLastOrNull() },
|
onBackClicked = { backStack.removeLastOrNull() },
|
||||||
|
|||||||
Reference in New Issue
Block a user