From 7be5115f6ebe91e3a0aad7d7c75465e0367b54d7 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 08:36:50 -0400 Subject: [PATCH 1/8] Update usages of scaffolding --- .../tunjid/demo/common/ui/PaneNavigation.kt | 94 ++++++++++++------- .../com/tunjid/demo/common/ui/PaneScaffold.kt | 85 ++++++----------- .../tunjid/demo/common/ui/avatar/PaneEntry.kt | 3 +- .../tunjid/demo/common/ui/chat/PaneEntry.kt | 3 +- .../demo/common/ui/chatrooms/PaneEntry.kt | 3 +- .../com/tunjid/demo/common/ui/me/PaneEntry.kt | 3 +- .../demo/common/ui/profile/PaneEntry.kt | 3 +- 7 files changed, 101 insertions(+), 93 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt index 2be659f..7b0f010 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -16,7 +16,14 @@ package com.tunjid.demo.common.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -32,56 +39,77 @@ import com.tunjid.treenav.current @Composable fun PaneScaffoldState.PaneBottomAppBar( modifier: Modifier = Modifier, + enterTransition: EnterTransition = slideInVertically(initialOffsetY = { it }), + exitTransition: ExitTransition = slideOutVertically(targetOffsetY = { it }), ) { val appState = LocalAppState.current - val sharedContentState = rememberSharedContentState(BottomNavSharedElementKey) - NavigationBar( + AnimatedVisibility( modifier = modifier .sharedElement( - sharedContentState = sharedContentState, + sharedContentState = rememberSharedContentState(BottomNavSharedElementKey), animatedVisibilityScope = this, - zIndexInOverlay = BottomNavSharedElementZIndex, + zIndexInOverlay = NavigationSharedElementZIndex, ), - ) { - SampleDestination.NavTabs.entries.forEach { item -> - NavigationBarItem( - icon = { - Icon( - imageVector = item.icon, - contentDescription = item.title, + visible = canShowBottomNavigation, + enter = enterTransition, + exit = exitTransition, + content = { + NavigationBar { + SampleDestination.NavTabs.entries.forEach { item -> + NavigationBarItem( + icon = { + Icon( + imageVector = item.icon, + contentDescription = item.title, + ) + }, + selected = item == appState.currentNavigation.current, + onClick = { appState.setTab(item) } ) - }, - selected = item == appState.currentNavigation.current, - onClick = { appState.setTab(item) } - ) - } - } + } + } + }, + ) } -@Suppress("UnusedReceiverParameter") +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PaneScaffoldState.PaneNavigationRail( modifier: Modifier = Modifier, + enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }), + exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }), ) { val appState = LocalAppState.current - NavigationRail( - modifier = modifier, - ) { - SampleDestination.NavTabs.entries.forEach { item -> - NavigationRailItem( - selected = item == appState.currentNavigation.current, - icon = { - Icon( - imageVector = item.icon, - contentDescription = item.title, + AnimatedVisibility( + modifier = modifier + .sharedElement( + sharedContentState = rememberSharedContentState(NavRailSharedElementKey), + animatedVisibilityScope = this, + zIndexInOverlay = NavigationSharedElementZIndex, + ), + visible = canShowNavRail, + enter = enterTransition, + exit = exitTransition, + content = { + NavigationRail { + SampleDestination.NavTabs.entries.forEach { item -> + NavigationRailItem( + selected = item == appState.currentNavigation.current, + icon = { + Icon( + imageVector = item.icon, + contentDescription = item.title, + ) + }, + onClick = { appState.setTab(item) } ) - }, - onClick = { appState.setTab(item) } - ) + } + } } - } + ) } private data object BottomNavSharedElementKey +private data object NavRailSharedElementKey -private const val BottomNavSharedElementZIndex = 2f +private const val NavigationSharedElementZIndex = 2f diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt index d6db74b..de24fb2 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt @@ -16,21 +16,17 @@ package com.tunjid.demo.common.ui -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.animateBounds import androidx.compose.animation.core.snap import androidx.compose.animation.core.spring -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -45,7 +41,6 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged @@ -93,9 +88,22 @@ class PaneScaffoldState internal constructor( && abs(scaffoldCurrentSize.height - scaffoldTargetSize.height) <= 2 } +@Composable +fun PaneScope.rememberPaneScaffoldState(): PaneScaffoldState { + val appState = LocalAppState.current + val paneMovableElementSharedTransitionScope = rememberPaneMovableElementSharedTransitionScope() + return remember(appState) { + PaneScaffoldState( + appState = appState, + paneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, + ) + } +} + + @OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun PaneScope.PaneScaffold( +fun PaneScaffoldState.PaneScaffold( modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.background, snackBarMessages: List = emptyList(), @@ -108,37 +116,24 @@ fun PaneScope.PaneScaffold( ) { val appState = LocalAppState.current val snackbarHostState = remember { SnackbarHostState() } - val paneMovableElementSharedTransitionScope = rememberPaneMovableElementSharedTransitionScope() - val paneScaffoldState = - remember(appState, paneMovableElementSharedTransitionScope) { - PaneScaffoldState( - appState = appState, - paneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, - ) - } val canAnimatePane = remember { mutableStateOf(true) }.also { it.value = !appState.isInteractingWithPanes() } - RowPaneScaffold( + PaneNavigationRailScaffold( modifier = modifier, navigationRail = { - if (paneScaffoldState.canShowNavRail) Box( - modifier = Modifier - .zIndex(2f), - ) { - paneScaffoldState.navigationRail() - } + navigationRail() }, content = { Scaffold( modifier = Modifier .animateBounds( - lookaheadScope = paneMovableElementSharedTransitionScope, + lookaheadScope = this, boundsTransform = remember { scaffoldBoundsTransform( - paneScaffoldState = paneScaffoldState, + paneScaffoldState = this, canAnimatePane = canAnimatePane::value ) } @@ -147,37 +142,23 @@ fun PaneScope.PaneScaffold( horizontal = if (appState.filteredPaneOrder.size > 1) 8.dp else 0.dp ) .onSizeChanged { - paneScaffoldState.scaffoldCurrentSize = it + scaffoldCurrentSize = it }, containerColor = containerColor, topBar = { - paneScaffoldState.topBar() + topBar() }, floatingActionButton = { - AnimatedVisibility( - visible = paneScaffoldState.canShowFab, - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }), - content = { - paneScaffoldState.floatingActionButton() - }, - ) + floatingActionButton() }, bottomBar = { - AnimatedVisibility( - visible = paneScaffoldState.canShowBottomNavigation, - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }), - content = { - paneScaffoldState.navigationBar() - }, - ) + navigationBar() }, snackbarHost = { SnackbarHost(snackbarHostState) }, content = { paddingValues -> - paneScaffoldState.content(paddingValues) + content(paddingValues) }, ) } @@ -197,7 +178,7 @@ fun PaneScope.PaneScaffold( } @Composable -private inline fun RowPaneScaffold( +private inline fun PaneNavigationRailScaffold( modifier: Modifier = Modifier, navigationRail: @Composable () -> Unit, content: @Composable () -> Unit, @@ -208,9 +189,11 @@ private inline fun RowPaneScaffold( Box( modifier = Modifier .widthIn(max = 80.dp) - ) { - navigationRail() - } + .zIndex(2f), + content = { + navigationRail() + }, + ) Box( modifier = Modifier .fillMaxSize() @@ -223,6 +206,7 @@ private inline fun RowPaneScaffold( ) } + @OptIn(ExperimentalSharedTransitionApi::class) private fun scaffoldBoundsTransform( paneScaffoldState: PaneScaffoldState, @@ -247,12 +231,3 @@ private fun scaffoldBoundsTransform( } } -fun Modifier.paneClip() = - then(PaneClipModifier) - -private val PaneClipModifier = Modifier.clip( - shape = RoundedCornerShape( - topStart = 16.dp, - topEnd = 16.dp, - ) -) \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/PaneEntry.kt index 9ffe5a4..9cf1556 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/PaneEntry.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs +import com.tunjid.demo.common.ui.rememberPaneScaffoldState import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.threePaneEntry @@ -48,7 +49,7 @@ fun avatarPaneEntry() = threePaneEntry( roomName = destination.roomName, ) } - PaneScaffold( + rememberPaneScaffoldState().PaneScaffold( modifier = Modifier .fillMaxSize(), containerColor = Color.Transparent, diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt index ec3861a..f89b981 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt @@ -29,6 +29,7 @@ import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier +import com.tunjid.demo.common.ui.rememberPaneScaffoldState import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.threePaneEntry @@ -48,7 +49,7 @@ fun chatPaneEntry() = threePaneEntry( chat = destination, ) } - PaneScaffold( + rememberPaneScaffoldState().PaneScaffold( modifier = Modifier .predictiveBackBackgroundModifier(this) .fillMaxSize(), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt index 987e5ad..635be5f 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt @@ -27,6 +27,7 @@ import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.ChatsRepository import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier +import com.tunjid.demo.common.ui.rememberPaneScaffoldState import com.tunjid.treenav.compose.threepane.threePaneEntry fun chatRoomPaneEntry( @@ -39,7 +40,7 @@ fun chatRoomPaneEntry( chatsRepository = ChatsRepository ) } - PaneScaffold( + rememberPaneScaffoldState().PaneScaffold( modifier = Modifier .predictiveBackBackgroundModifier(this) .fillMaxSize(), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt index 32638d2..03ca41a 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt @@ -28,6 +28,7 @@ import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.demo.common.ui.profile.ProfileScreen import com.tunjid.demo.common.ui.profile.ProfileViewModel +import com.tunjid.demo.common.ui.rememberPaneScaffoldState import com.tunjid.treenav.compose.threepane.threePaneEntry fun mePaneEntry( @@ -41,7 +42,7 @@ fun mePaneEntry( roomName = null, ) } - PaneScaffold( + rememberPaneScaffoldState().PaneScaffold( modifier = Modifier .predictiveBackBackgroundModifier(this) .fillMaxSize(), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt index ec07cd4..3498ce7 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt @@ -28,6 +28,7 @@ import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier +import com.tunjid.demo.common.ui.rememberPaneScaffoldState import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.threePaneEntry @@ -50,7 +51,7 @@ fun profilePaneEntry() = threePaneEntry( roomName = destination.roomName, ) } - PaneScaffold( + rememberPaneScaffoldState().PaneScaffold( modifier = Modifier .predictiveBackBackgroundModifier(this) .fillMaxSize(), From ebbadf14013c273de89700dd30a9844e8d7713d4 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 08:56:03 -0400 Subject: [PATCH 2/8] Add updating scaffolding for pane entries and improves kdocs for threePanedMovableSharedElementTransform --- ...PaneMovableElementSharedTransitionScope.kt | 6 ++-- .../MovableSharedElementTransform.kt | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt index 88d899c..8854b5e 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt @@ -24,7 +24,7 @@ import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PaneScope import com.tunjid.treenav.compose.PaneSharedTransitionScope import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope -import com.tunjid.treenav.compose.threepane.transforms.requireMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope /** * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] for @@ -41,7 +41,7 @@ interface PaneMovableElementSharedTransitionScope : * [PaneSharedTransitionScope] for this [PaneScope]. * * If one is not provided, one is retrieved from this [PaneScope] using - * [requireMovableSharedElementScope]. + * [requireThreePaneMovableSharedElementScope]. */ @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -49,7 +49,7 @@ fun PaneScope< ThreePane, Destination >.rememberPaneMovableElementSharedTransitionScope( - movableSharedElementScope: MovableSharedElementScope = requireMovableSharedElementScope() + movableSharedElementScope: MovableSharedElementScope = requireThreePaneMovableSharedElementScope() ): PaneMovableElementSharedTransitionScope { val paneSharedTransitionScope = rememberPaneSharedTransitionScope( movableSharedElementScope.sharedTransitionScope diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 71ba894..59ef1cf 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tunjid.treenav.Node import com.tunjid.treenav.compose.MultiPaneDisplay +import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.PaneScope import com.tunjid.treenav.compose.PaneState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState @@ -43,6 +44,23 @@ import com.tunjid.treenav.compose.transforms.Transform * A [Transform] that applies semantics of movable shared elements to * [ThreePane] layouts. * + * It is an opinionated implementation that always shows the movable shared element in + * the [ThreePane.Primary] pane unless: + * + * - The [ThreePane.PrimaryToTransient] adaptation is present and a shared element match is + * found. During this, the movable shared element will be shown in + * the [ThreePane.TransientPrimary] pane. During this, an empty box will be rendered + * in the [ThreePane.Primary] pane. + * + * - The [ThreePane.PrimaryToTransient] adaptation is present and a shared element match is NOT + * found. During this, the element will simply be rendered as is in [ThreePane.Primary], but + * without movable content semantics. + * + * Note: The movable shared element is never rendered in the following panes: + * - [ThreePane.Secondary] + * - [ThreePane.Tertiary] + * - [ThreePane.Overlay] + * * @param movableSharedElementHostState the host state for coordinating movable shared elements. * There should be one instance of this per [MultiPaneDisplay]. */ @@ -70,14 +88,18 @@ fun } /** - * Requires that this [PaneScope] is a [MovableSharedElementScope], and returns it. In the - * case this [PaneScope] is not a [MovableSharedElementScope], an exception will be thrown. + * Requires that this [PaneScope] is a [MovableSharedElementScope] specifically configured for + * [ThreePane] layouts and returns it. This only succeeds if the [MultiPaneDisplayState] has the + * [threePanedMovableSharedElementTransform] applied to it. + * + * In the case this [PaneScope] is not the [MovableSharedElementScope] requested, an exception + * will be thrown. */ @Stable fun PaneScope< ThreePane, Destination - >.requireMovableSharedElementScope(): MovableSharedElementScope { + >.requireThreePaneMovableSharedElementScope(): MovableSharedElementScope { check(this is ThreePaneMovableSharedElementScope) { """ The current PaneScope (${this::class.qualifiedName}) is not an instance of From 5af494d330a2583e8492b246b79f8003049e9e24 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 09:54:46 -0400 Subject: [PATCH 3/8] Add type aliases for PaneMovableElementSharedTransitionScope --- ...aneMovableElementSharedTransitionScope.kt} | 34 +++------- ...PaneMovableElementSharedTransitionScope.kt | 67 +++++++++++++++++++ .../com/tunjid/demo/common/ui/PaneScaffold.kt | 12 ++-- 3 files changed, 84 insertions(+), 29 deletions(-) rename library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/{PaneMovableElementSharedTransitionScope.kt => ThreePaneMovableElementSharedTransitionScope.kt} (62%) create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneMovableElementSharedTransitionScope.kt diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneMovableElementSharedTransitionScope.kt similarity index 62% rename from library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneMovableElementSharedTransitionScope.kt index 8854b5e..fba37ad 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneMovableElementSharedTransitionScope.kt @@ -18,24 +18,23 @@ package com.tunjid.treenav.compose.threepane import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.PaneMovableElementSharedTransitionScope import com.tunjid.treenav.compose.PaneScope import com.tunjid.treenav.compose.PaneSharedTransitionScope import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.rememberPaneMovableElementSharedTransitionScope import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope /** * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] for * a [ThreePane] layout. */ -@Stable -interface PaneMovableElementSharedTransitionScope : - PaneSharedTransitionScope, MovableSharedElementScope +typealias ThreePaneMovableElementSharedTransitionScope = + PaneMovableElementSharedTransitionScope /** - * Remembers a [PaneMovableElementSharedTransitionScope] in the composition. + * Remembers a [ThreePaneMovableElementSharedTransitionScope] in the composition. * * @param movableSharedElementScope The [MovableSharedElementScope] used create a * [PaneSharedTransitionScope] for this [PaneScope]. @@ -48,25 +47,14 @@ interface PaneMovableElementSharedTransitionScope : fun PaneScope< ThreePane, Destination - >.rememberPaneMovableElementSharedTransitionScope( + >.rememberThreePaneMovableElementSharedTransitionScope( movableSharedElementScope: MovableSharedElementScope = requireThreePaneMovableSharedElementScope() -): PaneMovableElementSharedTransitionScope { +): ThreePaneMovableElementSharedTransitionScope { val paneSharedTransitionScope = rememberPaneSharedTransitionScope( movableSharedElementScope.sharedTransitionScope ) - return remember { - DelegatingPaneMovableElementSharedTransitionScope( - paneSharedTransitionScope = paneSharedTransitionScope, - movableSharedElementScope = movableSharedElementScope, - ) - } + return rememberPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope = paneSharedTransitionScope, + movableSharedElementScope = movableSharedElementScope, + ) } - -@Stable -private class DelegatingPaneMovableElementSharedTransitionScope( - val paneSharedTransitionScope: PaneSharedTransitionScope, - val movableSharedElementScope: MovableSharedElementScope, -) : PaneMovableElementSharedTransitionScope, - PaneSharedTransitionScope by paneSharedTransitionScope, - MovableSharedElementScope by movableSharedElementScope - diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneMovableElementSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneMovableElementSharedTransitionScope.kt new file mode 100644 index 0000000..486dd0c --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneMovableElementSharedTransitionScope.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +@file:Suppress("unused") + +package com.tunjid.treenav.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope + +/** + * A type alias for [PaneMovableElementSharedTransitionScope] for usages where the generic types + * are not required. + */ +typealias MovableElementSharedTransitionScope = PaneMovableElementSharedTransitionScope<*, *> + +/** + * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] + * semantics. + */ +@Stable +interface PaneMovableElementSharedTransitionScope : + PaneSharedTransitionScope, MovableSharedElementScope + +/** + * Remembers a [PaneMovableElementSharedTransitionScope] in the composition. + * + * @param paneSharedTransitionScope the backing [PaneSharedTransitionScope] for this [PaneScope]. + * @param movableSharedElementScope the backing [MovableSharedElementScope] for this [PaneScope]. + */ +@Composable +fun rememberPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope: PaneSharedTransitionScope, + movableSharedElementScope: MovableSharedElementScope, +): PaneMovableElementSharedTransitionScope { + return remember { + DelegatingPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope = paneSharedTransitionScope, + movableSharedElementScope = movableSharedElementScope, + ) + } +} + +@Stable +private class DelegatingPaneMovableElementSharedTransitionScope( + val paneSharedTransitionScope: PaneSharedTransitionScope, + val movableSharedElementScope: MovableSharedElementScope, +) : PaneMovableElementSharedTransitionScope, + PaneSharedTransitionScope by paneSharedTransitionScope, + MovableSharedElementScope by movableSharedElementScope diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt index de24fb2..6f7d66b 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt @@ -51,9 +51,9 @@ import androidx.compose.ui.zIndex import com.tunjid.composables.ui.skipIf import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.threepane.PaneMovableElementSharedTransitionScope +import com.tunjid.treenav.compose.threepane.ThreePaneMovableElementSharedTransitionScope import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.rememberPaneMovableElementSharedTransitionScope +import com.tunjid.treenav.compose.threepane.rememberThreePaneMovableElementSharedTransitionScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlin.math.abs @@ -61,8 +61,8 @@ import kotlin.math.abs @Stable class PaneScaffoldState internal constructor( private val appState: AppState, - paneMovableElementSharedTransitionScope: PaneMovableElementSharedTransitionScope, -) : PaneMovableElementSharedTransitionScope by paneMovableElementSharedTransitionScope { + threePaneMovableElementSharedTransitionScope: ThreePaneMovableElementSharedTransitionScope, +) : ThreePaneMovableElementSharedTransitionScope by threePaneMovableElementSharedTransitionScope { internal val canShowBottomNavigation get() = !appState.isMediumScreenWidthOrWider @@ -91,11 +91,11 @@ class PaneScaffoldState internal constructor( @Composable fun PaneScope.rememberPaneScaffoldState(): PaneScaffoldState { val appState = LocalAppState.current - val paneMovableElementSharedTransitionScope = rememberPaneMovableElementSharedTransitionScope() + val paneMovableElementSharedTransitionScope = rememberThreePaneMovableElementSharedTransitionScope() return remember(appState) { PaneScaffoldState( appState = appState, - paneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, + threePaneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, ) } } From 8ff17242edc1a11014529229a920442eb800a61f Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 12:35:06 -0400 Subject: [PATCH 4/8] Use movable shared elements for nav ui elements --- .../MovableSharedElementTransform.kt | 10 +- .../MovableSharedElements.kt | 17 ++- .../com/tunjid/demo/common/ui/DemoApp.kt | 10 +- .../tunjid/demo/common/ui/PaneNavigation.kt | 119 +++++++++++++++--- 4 files changed, 123 insertions(+), 33 deletions(-) diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 59ef1cf..e4c17d4 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -36,6 +36,7 @@ import com.tunjid.treenav.compose.PaneState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope import com.tunjid.treenav.compose.moveablesharedelement.PaneMovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.rememberPaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.transforms.RenderTransform import com.tunjid.treenav.compose.transforms.Transform @@ -69,12 +70,9 @@ fun movableSharedElementHostState: MovableSharedElementHostState, ): Transform = RenderTransform { destination, previousTransform -> - val delegate = remember { - PaneMovableSharedElementScope( - paneScope = this, - movableSharedElementHostState = movableSharedElementHostState, - ) - } + val delegate = rememberPaneMovableSharedElementScope( + movableSharedElementHostState = movableSharedElementHostState + ) delegate.paneScope = this val movableSharedElementScope = remember { diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index 2c3ce1b..99549c9 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.tunjid.treenav.Node @@ -179,6 +180,16 @@ class MovableSharedElementHostState( } } +@Composable +fun PaneScope.rememberPaneMovableSharedElementScope( + movableSharedElementHostState: MovableSharedElementHostState +) = remember { + PaneMovableSharedElementScope( + paneScope = this, + movableSharedElementHostState = movableSharedElementHostState + ) +} + /** * An implementation of [MovableSharedElementScope] that ensures shared elements are only rendered * in an [PaneScope] when it is active. @@ -188,9 +199,9 @@ class MovableSharedElementHostState( */ @OptIn(ExperimentalSharedTransitionApi::class) @Stable -class PaneMovableSharedElementScope( - paneScope: PaneScope, - private val movableSharedElementHostState: MovableSharedElementHostState, +class PaneMovableSharedElementScope internal constructor( + paneScope: PaneScope, + private val movableSharedElementHostState: MovableSharedElementHostState, ) : MovableSharedElementScope { override val sharedTransitionScope: SharedTransitionScope diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index 4d3ab68..e9c99eb 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -89,6 +88,7 @@ import com.tunjid.treenav.compose.transforms.paneModifierTransform import com.tunjid.treenav.requireCurrent import com.tunjid.treenav.pop import com.tunjid.treenav.popToRoot +import com.tunjid.treenav.switch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -102,15 +102,13 @@ fun App( LocalAppState provides appState, ) { SharedTransitionLayout(Modifier.fillMaxSize()) { - val backPreviewSurfaceColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - animateDpAsState(if (appState.isPreviewingBack) 16.dp else 0.dp).value - ) val density = LocalDensity.current val movableSharedElementHostState = remember { MovableSharedElementHostState( sharedTransitionScope = this ) } + appState.movableSharedElementHostState = movableSharedElementHostState MultiPaneDisplay( modifier = Modifier .fillMaxSize(), @@ -242,6 +240,8 @@ class AppState( private val navigationRepository: NavigationRepository = NavigationRepository, ) { + internal lateinit var movableSharedElementHostState : MovableSharedElementHostState + private val navigationState = mutableStateOf( navigationRepository.navigationStateFlow.value ) @@ -283,7 +283,7 @@ class AppState( fun setTab(destination: SampleDestination.NavTabs) { navigationRepository.navigate { if (it.currentIndex == destination.ordinal) it.popToRoot() - else it.copy(currentIndex = destination.ordinal) + else it.switch(toIndex = destination.ordinal) } } diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt index 7b0f010..7553c28 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -30,27 +30,73 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.tunjid.demo.common.ui.data.SampleDestination +import com.tunjid.treenav.compose.moveablesharedelement.rememberPaneMovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.rememberPaneMovableElementSharedTransitionScope +import com.tunjid.treenav.compose.threepane.ThreePaneMovableElementSharedTransitionScope import com.tunjid.treenav.current - @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PaneScaffoldState.PaneBottomAppBar( modifier: Modifier = Modifier, enterTransition: EnterTransition = slideInVertically(initialOffsetY = { it }), exitTransition: ExitTransition = slideOutVertically(targetOffsetY = { it }), +) { + val paneScaffoldNavigationState = rememberUpdatedPaneScaffoldNavigationState( + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShowBottomNavigation + ) + paneScaffoldNavigationState.updatedMovableSharedElementOf( + key = BottomNavSharedElementKey, + modifier = modifier, + zIndexInOverlay = NavigationSharedElementZIndex, + state = paneScaffoldNavigationState, + sharedElement = { state, innerModifier -> + state.PaneBottomAppBar(innerModifier) + } + ) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun PaneScaffoldState.PaneNavigationRail( + modifier: Modifier = Modifier, + enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }), + exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }), +) { + val paneScaffoldNavigationState = rememberUpdatedPaneScaffoldNavigationState( + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShowNavRail + ) + paneScaffoldNavigationState.updatedMovableSharedElementOf( + key = NavRailSharedElementKey, + modifier = modifier, + zIndexInOverlay = NavigationSharedElementZIndex, + state = paneScaffoldNavigationState, + sharedElement = { state, innerModifier -> + state.PaneNavigationRail(innerModifier) + } + ) +} + +@Composable +private fun NavigationBarState.PaneBottomAppBar( + modifier: Modifier = Modifier, ) { val appState = LocalAppState.current AnimatedVisibility( - modifier = modifier - .sharedElement( - sharedContentState = rememberSharedContentState(BottomNavSharedElementKey), - animatedVisibilityScope = this, - zIndexInOverlay = NavigationSharedElementZIndex, - ), - visible = canShowBottomNavigation, + modifier = modifier, + visible = canShow, enter = enterTransition, exit = exitTransition, content = { @@ -72,22 +118,14 @@ fun PaneScaffoldState.PaneBottomAppBar( ) } -@OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun PaneScaffoldState.PaneNavigationRail( +private fun NavigationBarState.PaneNavigationRail( modifier: Modifier = Modifier, - enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }), - exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }), ) { val appState = LocalAppState.current AnimatedVisibility( - modifier = modifier - .sharedElement( - sharedContentState = rememberSharedContentState(NavRailSharedElementKey), - animatedVisibilityScope = this, - zIndexInOverlay = NavigationSharedElementZIndex, - ), - visible = canShowNavRail, + modifier = modifier, + visible = canShow, enter = enterTransition, exit = exitTransition, content = { @@ -109,6 +147,49 @@ fun PaneScaffoldState.PaneNavigationRail( ) } +@Composable +private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( + enterTransition: EnterTransition, + exitTransition: ExitTransition, + canShow: Boolean, +): NavigationBarState { + + val appState = LocalAppState.current + + val paneMovableElementSharedTransitionScope = + rememberPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope = this, + movableSharedElementScope = rememberPaneMovableSharedElementScope( + movableSharedElementHostState = appState.movableSharedElementHostState + ), + ) + + return remember { + NavigationBarState( + movableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShow, + ).also { + it.enterTransition = enterTransition + it.exitTransition = exitTransition + it.canShow = canShow + } + } +} + +@Stable +private class NavigationBarState( + movableElementSharedTransitionScope: ThreePaneMovableElementSharedTransitionScope, + enterTransition: EnterTransition, + exitTransition: ExitTransition, + canShow: Boolean, +) : ThreePaneMovableElementSharedTransitionScope by movableElementSharedTransitionScope { + var enterTransition by mutableStateOf(enterTransition) + var exitTransition by mutableStateOf(exitTransition) + var canShow by mutableStateOf(canShow) +} + private data object BottomNavSharedElementKey private data object NavRailSharedElementKey From 78e598d22eaf243cbd32fbce339026e499bd19cc Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 13:23:40 -0400 Subject: [PATCH 5/8] Fill max constrainsts for movable shared elements --- .../MovableSharedElements.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index 99549c9..c09a956 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout import com.tunjid.treenav.Node import com.tunjid.treenav.compose.Defaults import com.tunjid.treenav.compose.MultiPaneDisplay @@ -240,7 +241,7 @@ class PaneMovableSharedElementScope internal construct key = key, sharedContentState = sharedContentState, sharedElement = sharedElement - )(state, Modifier.matchParentSize()) + )(state, Modifier.fillMaxConstraints()) // This pane state is be transitioning out. Check if it should be displayed without // shared element semantics. @@ -250,13 +251,13 @@ class PaneMovableSharedElementScope internal construct movableSharedElementHostState.isCurrentlyShared(key) && movableSharedElementHostState.isMatchFound(key) -> Defaults.EmptyElement( state, - Modifier.matchParentSize() + Modifier.fillMaxConstraints() ) // The element is not being shared in its new destination, allow it run its exit // transition else -> (alternateOutgoingSharedElement ?: sharedElement)( state, - Modifier.matchParentSize() + Modifier.fillMaxConstraints() ) } } @@ -264,3 +265,19 @@ class PaneMovableSharedElementScope internal construct } } } + +private fun Modifier.fillMaxConstraints() = + layout { measurable, constraints -> + val placeable = measurable.measure( + constraints.copy( + minWidth = constraints.maxWidth, + maxHeight = constraints.maxHeight + ) + ) + layout( + width = placeable.width, + height = placeable.height + ) { + placeable.place(0, 0) + } + } From 206d03b535f3b2f01c283e3c94f2a06815e44c32 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 14:27:27 -0400 Subject: [PATCH 6/8] Fix where values are updated in NavigationBarState --- .../kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt index 7553c28..ec06eec 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -170,11 +170,11 @@ private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( enterTransition = enterTransition, exitTransition = exitTransition, canShow = canShow, - ).also { - it.enterTransition = enterTransition - it.exitTransition = exitTransition - it.canShow = canShow - } + ) + }.also { + it.enterTransition = enterTransition + it.exitTransition = exitTransition + it.canShow = canShow } } From 42747e03b2fbc94fc1ff8ddde35a62d7db6a893e Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 14:41:24 -0400 Subject: [PATCH 7/8] Update scope variable name in NavigationBarState --- .../kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt index ec06eec..45ae8d8 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -166,7 +166,7 @@ private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( return remember { NavigationBarState( - movableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, + delegate = paneMovableElementSharedTransitionScope, enterTransition = enterTransition, exitTransition = exitTransition, canShow = canShow, @@ -180,11 +180,11 @@ private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( @Stable private class NavigationBarState( - movableElementSharedTransitionScope: ThreePaneMovableElementSharedTransitionScope, + delegate: ThreePaneMovableElementSharedTransitionScope, enterTransition: EnterTransition, exitTransition: ExitTransition, canShow: Boolean, -) : ThreePaneMovableElementSharedTransitionScope by movableElementSharedTransitionScope { +) : ThreePaneMovableElementSharedTransitionScope by delegate { var enterTransition by mutableStateOf(enterTransition) var exitTransition by mutableStateOf(exitTransition) var canShow by mutableStateOf(canShow) From 671f953f9f521f5e1b31aa9a3ce468979e37c658 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 17 May 2025 16:17:01 -0400 Subject: [PATCH 8/8] Simplify movable navigation shared elements --- .../com/tunjid/demo/common/ui/DemoApp.kt | 13 +- .../tunjid/demo/common/ui/PaneNavigation.kt | 113 +++++++++++------- .../tunjid/demo/common/ui/chat/PaneEntry.kt | 4 +- .../demo/common/ui/chatrooms/PaneEntry.kt | 4 +- .../com/tunjid/demo/common/ui/me/PaneEntry.kt | 4 +- .../demo/common/ui/profile/PaneEntry.kt | 4 +- 6 files changed, 89 insertions(+), 53 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index e9c99eb..6d79122 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -48,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentWithReceiverOf import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -240,7 +241,7 @@ class AppState( private val navigationRepository: NavigationRepository = NavigationRepository, ) { - internal lateinit var movableSharedElementHostState : MovableSharedElementHostState + internal lateinit var movableSharedElementHostState: MovableSharedElementHostState private val navigationState = mutableStateOf( navigationRepository.navigationStateFlow.value @@ -276,6 +277,16 @@ class AppState( null ) + internal val movableNavigationBar = + movableContentWithReceiverOf { modifier -> + PaneNavigationBar(modifier) + } + + internal val movableNavigationRail = + movableContentWithReceiverOf { modifier -> + PaneNavigationRail(modifier) + } + val filteredPaneOrder: List by derivedStateOf { paneRenderOrder.filter { displayScope?.destinationIn(it) != null } } diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt index 45ae8d8..8c042e6 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -14,12 +14,15 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package com.tunjid.demo.common.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally @@ -38,59 +41,59 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.treenav.compose.moveablesharedelement.rememberPaneMovableSharedElementScope -import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf import com.tunjid.treenav.compose.rememberPaneMovableElementSharedTransitionScope +import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.ThreePaneMovableElementSharedTransitionScope import com.tunjid.treenav.current -@OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun PaneScaffoldState.PaneBottomAppBar( +fun PaneScaffoldState.PaneNavigationBar( modifier: Modifier = Modifier, enterTransition: EnterTransition = slideInVertically(initialOffsetY = { it }), exitTransition: ExitTransition = slideOutVertically(targetOffsetY = { it }), -) { - val paneScaffoldNavigationState = rememberUpdatedPaneScaffoldNavigationState( - enterTransition = enterTransition, - exitTransition = exitTransition, - canShow = canShowBottomNavigation - ) - paneScaffoldNavigationState.updatedMovableSharedElementOf( - key = BottomNavSharedElementKey, - modifier = modifier, - zIndexInOverlay = NavigationSharedElementZIndex, - state = paneScaffoldNavigationState, - sharedElement = { state, innerModifier -> - state.PaneBottomAppBar(innerModifier) - } - ) -} +) = withUpdatedPaneScaffoldNavigationState( + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShowBottomNavigation, + content = { + val finalModifier = modifier + .navigationSharedElement( + sharedContentState = rememberSharedContentState(BottomNavSharedElementKey), + ) + + if (canUseMovableContent) LocalAppState.current.movableNavigationBar( + this, + finalModifier + ) + else PaneNavigationBar(finalModifier) + } +) -@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PaneScaffoldState.PaneNavigationRail( modifier: Modifier = Modifier, enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }), exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }), -) { - val paneScaffoldNavigationState = rememberUpdatedPaneScaffoldNavigationState( - enterTransition = enterTransition, - exitTransition = exitTransition, - canShow = canShowNavRail - ) - paneScaffoldNavigationState.updatedMovableSharedElementOf( - key = NavRailSharedElementKey, - modifier = modifier, - zIndexInOverlay = NavigationSharedElementZIndex, - state = paneScaffoldNavigationState, - sharedElement = { state, innerModifier -> - state.PaneNavigationRail(innerModifier) - } - ) -} +) = withUpdatedPaneScaffoldNavigationState( + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShowNavRail, + content = { + val finalModifier = modifier + .navigationSharedElement( + sharedContentState = rememberSharedContentState(NavRailSharedElementKey), + ) + + if (canUseMovableContent) LocalAppState.current.movableNavigationRail( + this, + finalModifier + ) + else PaneNavigationRail(finalModifier) + } +) @Composable -private fun NavigationBarState.PaneBottomAppBar( +internal fun NavigationBarState.PaneNavigationBar( modifier: Modifier = Modifier, ) { val appState = LocalAppState.current @@ -119,7 +122,7 @@ private fun NavigationBarState.PaneBottomAppBar( } @Composable -private fun NavigationBarState.PaneNavigationRail( +internal fun NavigationBarState.PaneNavigationRail( modifier: Modifier = Modifier, ) { val appState = LocalAppState.current @@ -148,12 +151,12 @@ private fun NavigationBarState.PaneNavigationRail( } @Composable -private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( +private fun PaneScaffoldState.withUpdatedPaneScaffoldNavigationState( enterTransition: EnterTransition, exitTransition: ExitTransition, canShow: Boolean, -): NavigationBarState { - + content: @Composable NavigationBarState.() -> Unit +) { val appState = LocalAppState.current val paneMovableElementSharedTransitionScope = @@ -164,7 +167,7 @@ private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( ), ) - return remember { + val state = remember { NavigationBarState( delegate = paneMovableElementSharedTransitionScope, enterTransition = enterTransition, @@ -176,11 +179,13 @@ private fun PaneScaffoldState.rememberUpdatedPaneScaffoldNavigationState( it.exitTransition = exitTransition it.canShow = canShow } + + state.content() } @Stable -private class NavigationBarState( - delegate: ThreePaneMovableElementSharedTransitionScope, +internal class NavigationBarState( + private val delegate: ThreePaneMovableElementSharedTransitionScope, enterTransition: EnterTransition, exitTransition: ExitTransition, canShow: Boolean, @@ -188,6 +193,26 @@ private class NavigationBarState( var enterTransition by mutableStateOf(enterTransition) var exitTransition by mutableStateOf(exitTransition) var canShow by mutableStateOf(canShow) + + val canUseMovableContent + get() = when { + isActive && isPreviewingBack && paneState.pane == ThreePane.TransientPrimary -> true + isActive && !isPreviewingBack && paneState.pane == ThreePane.Primary -> true + else -> false + } + + private val isPreviewingBack: Boolean + get() = paneState.adaptations.contains(ThreePane.PrimaryToTransient) + + + @OptIn(ExperimentalSharedTransitionApi::class) + fun Modifier.navigationSharedElement( + sharedContentState: SharedTransitionScope.SharedContentState, + ) = sharedElement( + sharedContentState = sharedContentState, + animatedVisibilityScope = delegate, + zIndexInOverlay = NavigationSharedElementZIndex, + ) } private data object BottomNavSharedElementKey diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt index f89b981..975362d 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel -import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationBar import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination @@ -68,7 +68,7 @@ fun chatPaneEntry() = threePaneEntry( } }, navigationBar = { - PaneBottomAppBar() + PaneNavigationBar() }, navigationRail = { PaneNavigationRail() diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt index 635be5f..1eabeba 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel -import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationBar import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.ChatsRepository @@ -52,7 +52,7 @@ fun chatRoomPaneEntry( ) }, navigationBar = { - PaneBottomAppBar() + PaneNavigationBar() }, navigationRail = { PaneNavigationRail() diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt index 03ca41a..07bf1c2 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel -import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationBar import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier @@ -54,7 +54,7 @@ fun mePaneEntry( ) }, navigationBar = { - PaneBottomAppBar() + PaneNavigationBar() }, navigationRail = { PaneNavigationRail() diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt index 3498ce7..8a1bd38 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel -import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationBar import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination @@ -64,7 +64,7 @@ fun profilePaneEntry() = threePaneEntry( ) }, navigationBar = { - PaneBottomAppBar() + PaneNavigationBar() }, navigationRail = { PaneNavigationRail()