11package to.bitkit.ui.nav
22
3+ import androidx.compose.animation.AnimatedContent
4+ import androidx.compose.animation.core.FastOutSlowInEasing
5+ import androidx.compose.animation.core.tween
6+ import androidx.compose.animation.fadeIn
7+ import androidx.compose.animation.fadeOut
8+ import androidx.compose.animation.slideInHorizontally
9+ import androidx.compose.animation.slideOutHorizontally
10+ import androidx.compose.animation.togetherWith
311import androidx.compose.runtime.Composable
412import androidx.navigation3.runtime.NavEntry
513import androidx.navigation3.scene.OverlayScene
@@ -12,19 +20,39 @@ import to.bitkit.ui.components.SheetSize
1220class SheetSceneStrategy <T : Any > : SceneStrategy <T > {
1321
1422 override fun SceneStrategyScope<T>.calculateScene (entries : List <NavEntry <T >>): Scene <T >? {
15- val lastEntry = entries.lastOrNull()
16- val sheetProperties = lastEntry?.metadata?.get(KEY_SHEET ) as ? SheetProperties
17- return sheetProperties?.let { props ->
18- @Suppress(" UNCHECKED_CAST" )
19- SheetScene (
20- key = lastEntry.contentKey as T ,
21- previousEntries = entries.dropLast(1 ),
22- overlaidEntries = entries.dropLast(1 ),
23- entry = lastEntry,
24- sheetSize = props.size,
25- onBack = onBack,
26- )
23+ val lastEntry = entries.lastOrNull() ? : return null
24+
25+ // Find the sheet root (first entry with sheet metadata)
26+ val sheetRootIndex = entries.indexOfFirst {
27+ it.metadata?.get(KEY_SHEET ) != null
2728 }
29+
30+ // No sheet root found - not a sheet flow
31+ if (sheetRootIndex < 0 ) return null
32+
33+ val sheetRootEntry = entries[sheetRootIndex]
34+ val sheetProps = sheetRootEntry.metadata?.get(KEY_SHEET ) as ? SheetProperties ? : return null
35+
36+ // Entries before the sheet root (to be overlaid)
37+ val entriesBeforeSheet = entries.take(sheetRootIndex)
38+
39+ // Number of entries in the sheet flow (for dismiss behavior)
40+ val sheetEntryCount = entries.size - sheetRootIndex
41+
42+ // Current entry's index within the sheet flow (for animation direction)
43+ val currentEntryIndex = entries.size - 1
44+
45+ @Suppress(" UNCHECKED_CAST" )
46+ return SheetScene (
47+ key = lastEntry.contentKey as T ,
48+ previousEntries = entriesBeforeSheet,
49+ overlaidEntries = entriesBeforeSheet,
50+ entry = lastEntry,
51+ sheetSize = sheetProps.size,
52+ onBack = onBack,
53+ sheetEntryCount = sheetEntryCount,
54+ entryIndex = currentEntryIndex,
55+ )
2856 }
2957
3058 companion object {
@@ -40,23 +68,72 @@ data class SheetProperties(
4068 val size : SheetSize = SheetSize .LARGE ,
4169)
4270
71+ private const val MS_ANIM_DURATION = 300
72+
73+ private data class IndexedEntry <T : Any >(
74+ val index : Int ,
75+ val entry : NavEntry <T >,
76+ )
77+
78+ @Suppress(" LongParameterList" )
4379internal class SheetScene <T : Any >(
4480 override val key : T ,
4581 override val previousEntries : List <NavEntry <T >>,
4682 override val overlaidEntries : List <NavEntry <T >>,
4783 private val entry : NavEntry <T >,
4884 private val sheetSize : SheetSize ,
4985 private val onBack : () -> Unit ,
86+ private val sheetEntryCount : Int ,
87+ private val entryIndex : Int ,
5088) : OverlayScene<T> {
5189
5290 override val entries: List <NavEntry <T >> = listOf (entry)
5391
5492 override val content: @Composable (() -> Unit ) = {
5593 SheetHost (
5694 sheetSize = sheetSize,
57- onDismiss = onBack,
95+ onDismiss = {
96+ // Pop all entries in the sheet flow to dismiss entire sheet
97+ repeat(sheetEntryCount) { onBack() }
98+ },
5899 ) {
59- entry.Content ()
100+ AnimatedContent (
101+ targetState = IndexedEntry (entryIndex, entry),
102+ transitionSpec = {
103+ val isForward = targetState.index > initialState.index
104+ if (isForward) {
105+ // Forward navigation: slide in from right, slide out to left
106+ slideInHorizontally(
107+ initialOffsetX = { it },
108+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing )
109+ ) + fadeIn(
110+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing ),
111+ initialAlpha = 0.8f
112+ ) togetherWith slideOutHorizontally(
113+ targetOffsetX = { - it / 3 },
114+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing )
115+ ) + fadeOut(
116+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing ),
117+ targetAlpha = 0.8f
118+ )
119+ } else {
120+ // Backward navigation: slide in from left, slide out to right
121+ slideInHorizontally(
122+ initialOffsetX = { - it / 3 },
123+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing )
124+ ) + fadeIn(
125+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing ),
126+ initialAlpha = 0.8f
127+ ) togetherWith slideOutHorizontally(
128+ targetOffsetX = { it },
129+ animationSpec = tween(MS_ANIM_DURATION , easing = FastOutSlowInEasing )
130+ )
131+ }
132+ },
133+ label = " SheetContentTransition" ,
134+ ) { indexedEntry ->
135+ indexedEntry.entry.Content ()
136+ }
60137 }
61138 }
62139}
0 commit comments