From 28cbdfcf03b4a9ec96a287e20066978c0d94e327 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Tue, 1 Jul 2025 11:29:36 -0400 Subject: [PATCH 1/5] Fix an animation bug in MultiPaneDisplay caused by its entries and updated kdocs --- .../treenav/compose/MultiPaneDisplay.kt | 63 +++++++++++++------ .../compose/SlotBasedPanedNavigationState.kt | 14 ++--- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index eeba64a..1a92ab0 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -43,6 +43,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.tunjid.treenav.Node import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.children +import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.destination import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.id import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.paneEnterTransition import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.paneExitTransition @@ -55,6 +56,7 @@ import com.tunjid.treenav.compose.navigation3.ui.NavigationEventHandler import com.tunjid.treenav.compose.navigation3.ui.Scene import com.tunjid.treenav.compose.navigation3.ui.SceneStrategy import com.tunjid.treenav.compose.navigation3.ui.rememberSceneSetupNavEntryDecorator +import com.tunjid.treenav.compose.panedecorators.PaneDecorator import kotlinx.coroutines.CancellationException /** @@ -63,42 +65,44 @@ import kotlinx.coroutines.CancellationException @Stable interface MultiPaneDisplayScope { + /** + * All possible panes in the [MultiPaneDisplayScope]. + */ val panes: Collection + /** + * Renders the given [Destination] in the provided [Pane]. + */ @Composable fun Destination( pane: Pane, ) + /** + * Provides the set of adaptations in the provided [Pane]. + */ fun adaptationsIn( pane: Pane, ): Set + /** + * Returns the [Destination] in the provided [Pane]. + */ fun destinationIn( pane: Pane, ): Destination? } /** - * A Display that provides the following for each - * navigation [Destination] that shows up in its panes: + * A Display that adapts the [MultiPaneDisplayState.navigationState] to + * the [MultiPaneDisplayState.panes] available depending on the [PaneDecorator]s the + * [MultiPaneDisplayState] has been configured with. * - * - A single [SaveableStateHolder] for each navigation [Destination] that shows up in its panes. - * [SaveableStateHolder.SaveableStateProvider] is keyed on the [Destination]s [Node.id]. - * - * - A [ViewModelStoreOwner] for each [Destination] via [LocalViewModelStoreOwner]. - * Once present in the navigation tree, a [Destination] will always use the same - * [ViewModelStoreOwner], regardless of where in the tree it is, until its is removed from the tree. - * [Destination]s are unique based on their [Node.id]. - * - * - A [LifecycleOwner] for each [Destination] via [LocalLifecycleOwner]. This [LifecycleOwner] - * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the - * backstack. This is defined by [PaneScope.isActive], which is a function of the backing - * [AnimatedContent] for each [Pane] displayed and if the current [Destination] - * matches [MultiPaneDisplayScope.destinationIn] in the visible [Pane]. * * @param state the driving [MultiPaneDisplayState] that applies adaptive semantics and - * transforms for each navigation destination shown in the [MultiPaneDisplay]. + * decorators for each navigation destination shown in the [MultiPaneDisplay]. + * @param modifier optional [Modifier] for the display. + * @param content the content that should be displayed the receiving [MultiPaneDisplayScope]. */ @Composable fun MultiPaneDisplay( @@ -268,7 +272,7 @@ private class MultiPanePaneSceneStrategy @@ -287,8 +291,8 @@ private class MultiPanePaneSceneStrategy( - override val entries: List>, override val previousEntries: List>, + private val allEntries: List>, private val sceneKey: MultiPaneSceneKey, private val destination: Destination, private val slots: Set, @@ -310,8 +314,16 @@ private class MultiPaneDisplayScene( override val key: Any = sceneKey - override val content: @Composable () -> Unit = { + override val entries: List> + // Since the display may adapt, the actual entries to show are a subset of all eligible + // entries that can show. + get() = panedNavigationState.value.let { state -> + allEntries.filter { navEntry -> + state.paneFor(navEntry.destination()) != null + } + } + override val content: @Composable () -> Unit = { currentPanedNavigationState.rememberUpdatedPanedNavigationState( backStackIds = sceneKey.ids, panesToDestinations = panesToDestinations(destination), @@ -408,6 +420,9 @@ private fun MultiPaneDisplayState SlotBasedPanedNavigationState.rememberUpdatedPanedNavigationState( backStackIds: List, @@ -429,6 +444,12 @@ private fun SlotBasedPanedNavigationState, val isPreviewingBack: Boolean, @@ -448,6 +469,10 @@ internal class MultiPaneSceneKey( override fun hashCode(): Int { return idsHash } + + override fun toString(): String { + return "MultiPaneSceneKey(ids = $ids, isPreviewingBack = $isPreviewingBack)" + } } internal sealed class BackStatus { diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt index eec6320..7503e44 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt @@ -49,10 +49,6 @@ internal data class SlotBasedPanedNavigationState( * A set of node ids that may be returned to. */ val backStackIds: List, - /** - * A set of node ids that are animating out. - */ - val destinationIdsAnimatingOut: Set, ) { companion object { internal fun initial( @@ -65,7 +61,6 @@ internal data class SlotBasedPanedNavigationState( keySelector = Slot::toString ), backStackIds = emptyList(), - destinationIdsAnimatingOut = emptySet(), previousPanesToDestinations = emptyMap(), ) } @@ -90,10 +85,10 @@ internal data class SlotBasedPanedNavigationState( panesToDestinations[pane]?.id ] - private fun paneFor( - node: Node, - ): Pane? = panesToDestinations.firstNotNullOfOrNull { (pane, paneRoute) -> - if (paneRoute?.id == node.id) pane else null + internal fun paneFor( + destination: Node, + ): Pane? = panesToDestinations.firstNotNullOfOrNull { (pane, paneDestination) -> + if (paneDestination?.id == destination.id) pane else null } private fun destinationFor( @@ -211,7 +206,6 @@ internal fun SlotBasedPanedNavigationState Date: Tue, 1 Jul 2025 14:30:36 -0400 Subject: [PATCH 2/5] Handle eligible entries depending on predictive back --- .../treenav/compose/MultiPaneDisplay.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index 1a92ab0..d6cdeaf 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -16,7 +16,6 @@ package com.tunjid.treenav.compose -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition @@ -32,15 +31,9 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.tunjid.treenav.Node import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.children import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.destination @@ -272,7 +265,7 @@ private class MultiPanePaneSceneStrategy @@ -292,7 +285,7 @@ private class MultiPanePaneSceneStrategy( override val previousEntries: List>, - private val allEntries: List>, + private val eligibleSceneEntries: List>, private val sceneKey: MultiPaneSceneKey, private val destination: Destination, private val slots: Set, @@ -315,11 +308,17 @@ private class MultiPaneDisplayScene( override val key: Any = sceneKey override val entries: List> - // Since the display may adapt, the actual entries to show are a subset of all eligible - // entries that can show. - get() = panedNavigationState.value.let { state -> - allEntries.filter { navEntry -> - state.paneFor(navEntry.destination()) != null + get() = when { + // Filtering of duplicates is already handled in NavDisplay + sceneKey.isPreviewingBack -> eligibleSceneEntries + // Since the display may adapt, the actual entries to show are a subset of all eligible + // entries that can show. + // This is so destinations animating out are shown by the SceneSetupNavEntryDecorator. + // Otherwise, they will be removed immediately and not animate. + else -> panedNavigationState.value.let { state -> + eligibleSceneEntries.filter { navEntry -> + state.paneFor(navEntry.destination()) != null + } } } From ecab8800c0c6d3cc9d73120f1f684d2eea3df83d Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Tue, 1 Jul 2025 16:48:05 -0400 Subject: [PATCH 3/5] Make currentEntries in PaneDestinationMultiPaneDisplayScope a lambda --- .../kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index d6cdeaf..ce1c4ea 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -301,7 +301,7 @@ private class MultiPaneDisplayScene( @Stable val multiPaneDisplayScope = PaneDestinationMultiPaneDisplayScope( panedNavigationState = panedNavigationState, - entries = entries, + currentEntries = ::entries, backStatus = backStatus, ) @@ -339,7 +339,7 @@ private class MultiPaneDisplayScene( @Stable class PaneDestinationMultiPaneDisplayScope( panedNavigationState: State>, - private val entries: List>, + private val currentEntries: () -> List>, private val backStatus: () -> BackStatus, ) : MultiPaneDisplayScope { @@ -351,7 +351,7 @@ private class MultiPaneDisplayScene( @Composable override fun Destination(pane: Pane) { val id = panedNavigationState.destinationFor(pane)?.id - val entry = entries.firstOrNull { it.id == id } ?: return + val entry = currentEntries().firstOrNull { it.id == id } ?: return val paneState = remember(panedNavigationState.identityHash()) { panedNavigationState.slotFor(pane)?.let(panedNavigationState::paneStateFor) From 21aaa518fa1a56df917e3f437a1c2a596d6ee840 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Tue, 1 Jul 2025 17:04:11 -0400 Subject: [PATCH 4/5] Add usages of updatedMovableStickySharedElementOf --- .../kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.kt | 5 +++-- .../kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt | 3 ++- .../com/tunjid/demo/common/ui/profile/ProfileScreen.kt | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.kt index 150de85..9ed865b 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.kt @@ -29,13 +29,14 @@ import com.tunjid.demo.common.ui.PaneScaffoldState import com.tunjid.demo.common.ui.ProfilePhoto import com.tunjid.demo.common.ui.ProfilePhotoArgs import com.tunjid.demo.common.ui.dragToPop -import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun AvatarScreen( paneScaffoldState: PaneScaffoldState, state: State, + @Suppress("UNUSED_PARAMETER") onAction: (Action) -> Unit, modifier: Modifier = Modifier, ) { @@ -46,7 +47,7 @@ fun AvatarScreen( .fillMaxSize() ) { val profileName = state.profileName ?: state.profile?.name ?: "" - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "${state.roomName}-$profileName-profile" ), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt index baa107f..96f98e9 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt @@ -56,6 +56,7 @@ import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.data.Message import com.tunjid.demo.common.ui.data.Profile import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf import com.tunjid.treenav.compose.threepane.ThreePane import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone @@ -225,7 +226,7 @@ fun Message( } }, ) { - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "$roomName-${item.sender.name}-profile" ), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt index 0feca02..773fd39 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt @@ -43,7 +43,7 @@ import com.tunjid.demo.common.ui.ProfilePhoto import com.tunjid.demo.common.ui.ProfilePhotoArgs import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.rememberAppBarCollapsingHeaderState -import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf import kotlin.math.roundToInt @Composable @@ -127,7 +127,7 @@ private fun ProfilePhoto( ) { val profileName = state.profileName ?: state.profile?.name if (profileName != null) { - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "${state.roomName}-$profileName-profile" ), From 572c4b02532f0557ace13a2e0f8c4526262e0201 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Wed, 2 Jul 2025 00:51:06 -0400 Subject: [PATCH 5/5] Bump treenav to 0.0.41 --- libraryVersion.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraryVersion.properties b/libraryVersion.properties index 620e483..cf2df16 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -14,7 +14,7 @@ # limitations under the License. # groupId=com.tunjid.treenav -treenav_version=0.0.40 -strings_version=0.0.40 -compose_version=0.0.40 -compose-threepane_version=0.0.40 \ No newline at end of file +treenav_version=0.0.41 +strings_version=0.0.41 +compose_version=0.0.41 +compose-threepane_version=0.0.41 \ No newline at end of file