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 56% 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 88d899c..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,55 +18,43 @@ 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.threepane.transforms.requireMovableSharedElementScope +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]. * * If one is not provided, one is retrieved from this [PaneScope] using - * [requireMovableSharedElementScope]. + * [requireThreePaneMovableSharedElementScope]. */ @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PaneScope< ThreePane, Destination - >.rememberPaneMovableElementSharedTransitionScope( - movableSharedElementScope: MovableSharedElementScope = requireMovableSharedElementScope() -): PaneMovableElementSharedTransitionScope { + >.rememberThreePaneMovableElementSharedTransitionScope( + movableSharedElementScope: MovableSharedElementScope = requireThreePaneMovableSharedElementScope() +): 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-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..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 @@ -30,11 +30,13 @@ 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 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 @@ -43,6 +45,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]. */ @@ -51,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 { @@ -70,14 +86,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 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/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..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 @@ -13,8 +13,10 @@ 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 androidx.compose.ui.layout.layout import com.tunjid.treenav.Node import com.tunjid.treenav.compose.Defaults import com.tunjid.treenav.compose.MultiPaneDisplay @@ -179,6 +181,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 +200,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 @@ -229,7 +241,7 @@ class PaneMovableSharedElementScope( 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. @@ -239,13 +251,13 @@ class PaneMovableSharedElementScope( 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() ) } } @@ -253,3 +265,19 @@ class PaneMovableSharedElementScope( } } } + +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) + } + } 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..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 @@ -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 @@ -49,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 @@ -89,6 +89,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 +103,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 +241,8 @@ class AppState( private val navigationRepository: NavigationRepository = NavigationRepository, ) { + 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 } } @@ -283,7 +294,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 2be659f..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,74 +14,208 @@ * 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 +import androidx.compose.animation.slideOutVertically import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar 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.rememberPaneMovableElementSharedTransitionScope +import com.tunjid.treenav.compose.threepane.ThreePane +import com.tunjid.treenav.compose.threepane.ThreePaneMovableElementSharedTransitionScope import com.tunjid.treenav.current +@Composable +fun PaneScaffoldState.PaneNavigationBar( + modifier: Modifier = Modifier, + enterTransition: EnterTransition = slideInVertically(initialOffsetY = { it }), + exitTransition: ExitTransition = slideOutVertically(targetOffsetY = { it }), +) = 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) + } +) + +@Composable +fun PaneScaffoldState.PaneNavigationRail( + modifier: Modifier = Modifier, + enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }), + exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }), +) = 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) + } +) -@OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun PaneScaffoldState.PaneBottomAppBar( +internal fun NavigationBarState.PaneNavigationBar( modifier: Modifier = Modifier, ) { val appState = LocalAppState.current - val sharedContentState = rememberSharedContentState(BottomNavSharedElementKey) - NavigationBar( - modifier = modifier - .sharedElement( - sharedContentState = sharedContentState, - animatedVisibilityScope = this, - zIndexInOverlay = BottomNavSharedElementZIndex, - ), - ) { - SampleDestination.NavTabs.entries.forEach { item -> - NavigationBarItem( - icon = { - Icon( - imageVector = item.icon, - contentDescription = item.title, + AnimatedVisibility( + modifier = modifier, + visible = canShow, + 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") @Composable -fun PaneScaffoldState.PaneNavigationRail( +internal fun NavigationBarState.PaneNavigationRail( modifier: Modifier = Modifier, ) { val appState = LocalAppState.current - NavigationRail( + AnimatedVisibility( modifier = modifier, - ) { - SampleDestination.NavTabs.entries.forEach { item -> - NavigationRailItem( - selected = item == appState.currentNavigation.current, - icon = { - Icon( - imageVector = item.icon, - contentDescription = item.title, + visible = canShow, + 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) } - ) + } + } } + ) +} + +@Composable +private fun PaneScaffoldState.withUpdatedPaneScaffoldNavigationState( + enterTransition: EnterTransition, + exitTransition: ExitTransition, + canShow: Boolean, + content: @Composable NavigationBarState.() -> Unit +) { + val appState = LocalAppState.current + + val paneMovableElementSharedTransitionScope = + rememberPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope = this, + movableSharedElementScope = rememberPaneMovableSharedElementScope( + movableSharedElementHostState = appState.movableSharedElementHostState + ), + ) + + val state = remember { + NavigationBarState( + delegate = paneMovableElementSharedTransitionScope, + enterTransition = enterTransition, + exitTransition = exitTransition, + canShow = canShow, + ) + }.also { + it.enterTransition = enterTransition + it.exitTransition = exitTransition + it.canShow = canShow } + + state.content() +} + +@Stable +internal class NavigationBarState( + private val delegate: ThreePaneMovableElementSharedTransitionScope, + enterTransition: EnterTransition, + exitTransition: ExitTransition, + canShow: Boolean, +) : ThreePaneMovableElementSharedTransitionScope by delegate { + 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 +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..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 @@ -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 @@ -56,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 @@ -66,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 @@ -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 = rememberThreePaneMovableElementSharedTransitionScope() + return remember(appState) { + PaneScaffoldState( + appState = appState, + threePaneMovableElementSharedTransitionScope = 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..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,12 +23,13 @@ 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 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(), @@ -67,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 987e5ad..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,11 +22,12 @@ 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 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(), @@ -51,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 32638d2..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,12 +22,13 @@ 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 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(), @@ -53,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 ec07cd4..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,12 +22,13 @@ 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 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(), @@ -63,7 +64,7 @@ fun profilePaneEntry() = threePaneEntry( ) }, navigationBar = { - PaneBottomAppBar() + PaneNavigationBar() }, navigationRail = { PaneNavigationRail()