From acce422216b115dbe6c6299661b8fd16e025a97b Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Wed, 6 Nov 2024 10:32:31 -0800 Subject: [PATCH 1/8] Consider children when clearing ViewModels --- .../compose/SavedStatePanedNavHostState.kt | 35 ++++++++++---- .../DestinationViewModelStoreCreator.kt | 48 ++++++++++++------- .../kotlin/com/tunjid/treenav/Node.kt | 5 +- .../com/tunjid/demo/common/ui/DemoApp.kt | 10 ---- 4 files changed, 61 insertions(+), 37 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt index 648fc5c..36b48d5 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt @@ -20,6 +20,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.currentStateAsState import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner @@ -64,8 +67,22 @@ interface PanedNavHostScope { } /** - * An implementation of an [PanedNavHostState] that provides a [SaveableStateHolder] for each - * navigation destination that shows up in its panes. + * An implementation of an [PanedNavHostState] that provides the following for each + * navigation [Destination] that shows up in its panes: + * + * - A single [SaveableStateHolder] for each navigation [Destination] that shows up in its panes. + * [SaveableStateHolder.SaveableStateProvider] is keyed on the [Destination]s [Node.id]. + * + * - A [ViewModelStoreOwner] for each [Destination] via [LocalViewModelStoreOwner]. + * Once present in the navigation tree, a [Destination] will always use the same + * [ViewModelStoreOwner], regardless of where in the tree it is, until its is removed from the tree. + * [Destination]s are unique based on their [Node.id]. + * + * - A [LifecycleOwner] for each [Destination] via [LocalLifecycleOwner]. This [LifecycleOwner] + * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the + * backstack. This is defined by [PaneScope.isActive], which is a function of the backing + * [AnimatedContent] for each [Pane] displayed and if the current [Destination] + * matches [PanedNavHostScope.nodeFor] in the visible [Pane]. * * @param panes a list of panes that is possible to show in the [PanedNavHost] in all * possible configurations. The panes should consist of enum class instances, or a sealed class @@ -113,10 +130,6 @@ class SavedStatePanedNavHostState( val navHostConfiguration: PanedNavHostConfiguration, ) : PanedNavHostScope, SaveableStateHolder by saveableStateHolder { - private val destinationViewModelStoreCreator = DestinationViewModelStoreCreator( - rootNodeProvider = navHostConfiguration.navigationState::value - ) - val slots = List( size = panes.size, init = ::Slot @@ -131,6 +144,10 @@ class SavedStatePanedNavHostState( ) ) + private val destinationViewModelStoreCreator = DestinationViewModelStoreCreator( + validNodeIdsReader = { panedNavigationState.backStackIds + panedNavigationState.destinationIdsAnimatingOut } + ) + private val slotsToRoutes = mutableStateMapOf Unit>().also { map -> map[null] = {} @@ -208,8 +225,10 @@ class SavedStatePanedNavHostState( val destinationLifecycleOwner = rememberDestinationLifecycleOwner( destination ) - val destinationViewModelOwner = destinationViewModelStoreCreator - .viewModelStoreOwnerFor(destination) + val destinationViewModelOwner = remember(destination.id) { + destinationViewModelStoreCreator + .viewModelStoreOwnerFor(destination) + } CompositionLocalProvider( LocalLifecycleOwner provides destinationLifecycleOwner, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationViewModelStoreCreator.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationViewModelStoreCreator.kt index b18205f..538eb66 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationViewModelStoreCreator.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationViewModelStoreCreator.kt @@ -17,42 +17,54 @@ package com.tunjid.treenav.compose.lifecycle import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import com.tunjid.treenav.Node import com.tunjid.treenav.Order -import com.tunjid.treenav.flatten +import com.tunjid.treenav.traverse +/** + * A class that lazily loads a [ViewModelStoreOwner] for each destination in the navigation graph. + * Each unique destination can only have a single [ViewModelStore], regardless of how many times + * it appears in the navigation graph, or its depth at any point. + */ @Stable internal class DestinationViewModelStoreCreator( - private val rootNodeProvider: () -> Node + private val validNodeIdsReader: () -> Set ) { - private val nodeIdsToViewModelStoreOwner = mutableMapOf() + private val nodeIdsToViewModelStoreOwner = mutableStateMapOf() /** * Creates a [ViewModelStoreOwner] for a given [Node] */ fun viewModelStoreOwnerFor( node: Node - ): ViewModelStoreOwner = nodeIdsToViewModelStoreOwner.getOrPut( - node.id - ) { - object : ViewModelStoreOwner { - override val viewModelStore: ViewModelStore = ViewModelStore() + ): ViewModelStoreOwner { + val existingIds = validNodeIdsReader() + check(existingIds.contains(node.id)) { + """ + Attempted to create a ViewModelStoreOwner for Node $node, but the Node is not + present in the navigation tree + """.trimIndent() + } + return nodeIdsToViewModelStoreOwner.getOrPut( + node.id + ) { + object : ViewModelStoreOwner { + override val viewModelStore: ViewModelStore = ViewModelStore() + } } } fun clearStoreFor(childNode: Node) { - val rootNode = rootNodeProvider() - val existingNodeIds = rootNode.flatten(Order.BreadthFirst).mapTo( - destination = mutableSetOf(), - transform = Node::id - ) - if (existingNodeIds.contains(childNode.id)) { - return + val existingIds = validNodeIdsReader() + childNode.traverse(Order.BreadthFirst) { + if (!existingIds.contains(it.id)) { + nodeIdsToViewModelStoreOwner.remove(it.id) + ?.viewModelStore + ?.clear() + } } - nodeIdsToViewModelStoreOwner.remove(childNode.id) - ?.viewModelStore - ?.clear() } } \ No newline at end of file diff --git a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/Node.kt b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/Node.kt index 0979b06..ab75335 100644 --- a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/Node.kt +++ b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/Node.kt @@ -17,7 +17,10 @@ package com.tunjid.treenav /** - * A representation of a navigation node with child children [children] + * A representation of a navigation node in a navigation tree. + * + * [Node] instances are identified by their [Node.id]. The same [Node] can appear multiple times + * within the tree. To traverse the navigation tree use [Node.traverse], specifying the [Order]. */ interface Node { val id: String 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 4337a12..50f9f0e 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 @@ -23,7 +23,6 @@ import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState @@ -61,7 +60,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp @@ -164,14 +162,6 @@ fun SampleApp( ) .background(surfaceColor, RoundedCornerShape(16.dp)) else Modifier - .border(16.dp, when(paneState.pane) { - ThreePane.Primary -> Color.Cyan - ThreePane.TransientPrimary -> Color.Transparent - ThreePane.Secondary -> Color.Magenta - ThreePane.Tertiary -> Color.Yellow - ThreePane.Overlay -> Color.Transparent - null -> Color.Transparent - }) .fillMaxSize() } .threePanedNavHostConfiguration( From 0dd4382efa97008bc16a9db16431bc81c16b9b6b Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Thu, 14 Nov 2024 08:49:28 -0800 Subject: [PATCH 2/8] Add updatedMovableSharedElementOf --- .../MovableSharedElements.kt | 24 +++++++++++++++++++ .../tunjid/demo/common/ui/chat/ChatScreen.kt | 17 +++++++------ .../demo/common/ui/profile/ProfileScreen.kt | 15 ++++++------ 3 files changed, 39 insertions(+), 17 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 080dc78..c772277 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 @@ -26,6 +26,7 @@ internal interface SharedElementOverlay { * Creates movable shared elements that may be shared amongst different [PaneScope] * instances. */ +@Stable interface MovableSharedElementScope { /** @@ -46,6 +47,29 @@ interface MovableSharedElementScope { ): @Composable (T, Modifier) -> Unit } +/** + * Convenience method for [MovableSharedElementScope.movableSharedElementOf] that invokes the + * movable shared element with the latest values of [state] and [modifier]. + * + * @see [MovableSharedElementScope.movableSharedElementOf]. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun MovableSharedElementScope.updatedMovableSharedElementOf( + key: Any, + state: T, + modifier: Modifier = Modifier, + boundsTransform: BoundsTransform = DefaultBoundsTransform, + sharedElement: @Composable (T, Modifier) -> Unit +) = movableSharedElementOf( + key = key, + boundsTransform = boundsTransform, + sharedElement = sharedElement +).invoke( + state, + modifier, +) + /** * State for managing movable shared elements within a single [PanedNavHost]. */ diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt index 7040366..e3481c0 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt @@ -53,6 +53,7 @@ import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.data.Message import com.tunjid.demo.common.ui.data.Profile import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -153,12 +154,6 @@ fun Message( Row(modifier = spaceBetweenAuthors) { if (isLastMessageByAuthor) { - val sharedImage = movableSharedElementScope.movableSharedElementOf( - key = item.sender.name, - sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> - ProfilePhoto(args, innerModifier) - } - ) // Avatar Box( modifier = Modifier @@ -180,14 +175,18 @@ fun Message( } }, ) { - sharedImage( - ProfilePhotoArgs( + movableSharedElementScope.updatedMovableSharedElementOf( + key = item.sender.name, + state = ProfilePhotoArgs( profileName = item.sender.name, contentScale = ContentScale.Crop, cornerRadius = 42.dp, contentDescription = null, ), - Modifier.matchParentSize(), + modifier = Modifier.matchParentSize(), + sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> + ProfilePhoto(args, innerModifier) + } ) } } else { diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt index e3d8951..f911855 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt @@ -42,6 +42,7 @@ import com.tunjid.demo.common.ui.ProfilePhotoArgs import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.rememberAppBarCollapsingHeaderState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf import kotlin.math.roundToInt @Composable @@ -113,19 +114,17 @@ private fun ProfilePhoto( ) { val profileName = state.profileName ?: state.profile?.name if (profileName != null) { - val sharedImage = movableSharedElementScope.movableSharedElementOf( + movableSharedElementScope.updatedMovableSharedElementOf( key = profileName, - sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> - ProfilePhoto(args, innerModifier) - } - ) - sharedImage( - ProfilePhotoArgs( + state = ProfilePhotoArgs( profileName = profileName, contentScale = ContentScale.Crop, contentDescription = null, ), - modifier, + modifier = modifier, + sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> + ProfilePhoto(args, innerModifier) + } ) } } From 86937d6646015b69701fd52e132857a4da321111 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 09:19:56 -0800 Subject: [PATCH 3/8] Use sharedElementWithCallerManagedVisibility for movable shared elements --- .../MovableSharedElementState.kt | 114 +------------- .../MovableSharedElements.kt | 142 ++++++++++-------- .../MovableSharedElementConfiguration.kt | 17 ++- .../com/tunjid/demo/common/ui/DemoApp.kt | 8 +- 4 files changed, 99 insertions(+), 182 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt index 9be768f..44f3ef0 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt @@ -1,6 +1,5 @@ package com.tunjid.treenav.compose.moveablesharedelement -import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.runtime.Composable @@ -9,78 +8,28 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.graphics.drawscope.translate -import androidx.compose.ui.graphics.layer.GraphicsLayer -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.unit.toOffset -import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PaneState -import com.tunjid.treenav.compose.utilities.AnimatedBoundsState -import com.tunjid.treenav.compose.utilities.AnimatedBoundsState.Companion.animateBounds -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.first @Stable +internal class MovableSharedElementState @OptIn(ExperimentalSharedTransitionApi::class) -internal class MovableSharedElementState( - paneScope: PaneScope, - sharedTransitionScope: SharedTransitionScope, +constructor( + val sharedContentState: SharedTransitionScope.SharedContentState, sharedElement: @Composable (State, Modifier) -> Unit, - onRemoved: () -> Unit, - boundsTransform: BoundsTransform, - private val canAnimateOnStartingFrames: PaneState.() -> Boolean -) : SharedElementOverlay, SharedTransitionScope by sharedTransitionScope { - - var paneScope by mutableStateOf(paneScope) + onRemoved: () -> Unit +) { private var composedRefCount by mutableIntStateOf(0) - private var layer: GraphicsLayer? = null - var animInProgress by mutableStateOf(false) - private set - - private val canDrawInOverlay get() = animInProgress - private val panesKeysToSeenCount = mutableStateMapOf() - - private val animatedBoundsState = AnimatedBoundsState( - lookaheadScope = this, - boundsTransform = boundsTransform, - inProgress = { animInProgress } - ) - val moveableSharedElement: @Composable (Any?, Modifier) -> Unit = movableContentOf { state, modifier -> - animInProgress = isInProgress() - val layer = rememberGraphicsLayer().also { - this.layer = it - } @Suppress("UNCHECKED_CAST") sharedElement( // The shared element composable will be created by the first screen and reused by // subsequent screens. This updates the state from other screens so changes are seen. state as State, modifier - .animateBounds( - state = animatedBoundsState - ) - .drawWithContent { - layer.record { - this@drawWithContent.drawContent() - } - if (!canDrawInOverlay) { - drawLayer(layer) - } - }, ) DisposableEffect(Unit) { @@ -90,58 +39,5 @@ internal class MovableSharedElementState( } } } - - override fun ContentDrawScope.drawInOverlay() { - if (!canDrawInOverlay) return - val overlayLayer = layer ?: return - val (x, y) = animatedBoundsState.targetOffset.toOffset() - translate(x, y) { - drawLayer(overlayLayer) - } - } - - private fun updatePaneStateSeen( - paneState: PaneState<*, *> - ) { - panesKeysToSeenCount[paneState.key] = Unit - } - - private val hasBeenShared get() = panesKeysToSeenCount.size > 1 - - companion object { - - @Composable - private fun MovableSharedElementState<*, Pane, Destination>.isInProgress(): Boolean { - val paneState = paneScope.paneState.also(::updatePaneStateSeen) - - val (laggingScopeKey, animationInProgressTillFirstIdle) = produceState( - initialValue = Pair( - paneState.key, - paneState.canAnimateOnStartingFrames() - ), - key1 = paneState.key - ) { - value = Pair( - paneState.key, - paneState.canAnimateOnStartingFrames() - ) - value = snapshotFlow { animatedBoundsState.isIdle } - .debounce { if (it) 10 else 0 } - .first(true::equals) - .let { value.first to false } - }.value - - - if (!hasBeenShared) return false - - val isLagging = laggingScopeKey != paneScope.paneState.key - val canAnimateOnStartingFrames = paneScope.paneState.canAnimateOnStartingFrames() - - if (isLagging) return canAnimateOnStartingFrames - - return animationInProgressTillFirstIdle - } - } } -private val PaneState<*, *>.key get() = "${currentDestination?.id}-$pane" 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 c772277..9d6dcff 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 @@ -3,6 +3,9 @@ package com.tunjid.treenav.compose.moveablesharedelement import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.SharedContentState +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -10,18 +13,15 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PaneState import com.tunjid.treenav.compose.PanedNavHost import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform -internal interface SharedElementOverlay { - fun ContentDrawScope.drawInOverlay() -} - /** * Creates movable shared elements that may be shared amongst different [PaneScope] * instances. @@ -42,7 +42,9 @@ interface MovableSharedElementScope { @OptIn(ExperimentalSharedTransitionApi::class) fun movableSharedElementOf( key: Any, - boundsTransform: BoundsTransform = DefaultBoundsTransform, + boundsTransform: BoundsTransform, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: OverlayClip, sharedElement: @Composable (T, Modifier) -> Unit ): @Composable (T, Modifier) -> Unit } @@ -60,10 +62,14 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( state: T, modifier: Modifier = Modifier, boundsTransform: BoundsTransform = DefaultBoundsTransform, + zIndexInOverlay: Float = 0f, + clipInOverlayDuringTransition: OverlayClip = ParentClip, sharedElement: @Composable (T, Modifier) -> Unit ) = movableSharedElementOf( key = key, boundsTransform = boundsTransform, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, sharedElement = sharedElement ).invoke( state, @@ -77,28 +83,10 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( @Stable class MovableSharedElementHostState( private val sharedTransitionScope: SharedTransitionScope, - private val canAnimateOnStartingFrames: (PaneState) -> Boolean, -) { - - // TODO: This should be unnecessary. Figure out a way to participate arbitrarily in the - // overlays already implemented in [SharedTransitionScope]. - /** - * A [Modifier] for drawing the movable shared element in overlays over existing content. - */ - val modifier = Modifier.drawWithContent { - drawContent() - overlays.forEach { overlay -> - with(overlay) { - drawInOverlay() - } - } - } - - private val overlays: Collection - get() = keysToMovableSharedElements.values +) : SharedTransitionScope by sharedTransitionScope { private val keysToMovableSharedElements = - mutableStateMapOf>() + mutableStateMapOf>() /** * Returns true is a given shared element under a given key is currently being shared. @@ -107,31 +95,31 @@ class MovableSharedElementHostState( keysToMovableSharedElements.contains(key) /** - * Returns true if a movable shared element is animating. + * Returns true if a movable shared element has its shared element match found. + * + * @see [SharedContentState.isMatchFound] */ - fun isInProgress(key: Any): Boolean = - keysToMovableSharedElements[key]?.animInProgress == true + fun isMatchFound(key: Any): Boolean = + keysToMovableSharedElements[key]?.sharedContentState?.isMatchFound == true /** * Provides a movable shared element that can be rendered in a given [PaneScope]. * It is the callers responsibility to perform other verifications on the ability * of the calling [PaneScope] to render the movable shared element. */ - fun PaneScope.createOrUpdateSharedElement( + @Suppress("UnusedReceiverParameter") + fun MovableSharedElementScope.createOrUpdateSharedElement( key: Any, - boundsTransform: BoundsTransform, + sharedContentState: SharedContentState, sharedElement: @Composable (S, Modifier) -> Unit, ): @Composable (S, Modifier) -> Unit { val movableSharedElementState = keysToMovableSharedElements.getOrPut(key) { MovableSharedElementState( - paneScope = this, - sharedTransitionScope = sharedTransitionScope, + sharedContentState = sharedContentState, sharedElement = sharedElement, - boundsTransform = boundsTransform, - canAnimateOnStartingFrames = canAnimateOnStartingFrames, onRemoved = { keysToMovableSharedElements.remove(key) } ) - }.also { it.paneScope = this } + } // Can't really guarantee that the caller will use the same key for the right type return movableSharedElementState.moveableSharedElement @@ -146,7 +134,7 @@ class MovableSharedElementHostState( * movable shared element implementations. */ @Stable -internal class AdaptiveMovableSharedElementScope( +class PanedMovableSharedElementScope( paneScope: PaneScope, private val movableSharedElementHostState: MovableSharedElementHostState, ) : MovableSharedElementScope { @@ -157,29 +145,63 @@ internal class AdaptiveMovableSharedElementScope( override fun movableSharedElementOf( key: Any, boundsTransform: BoundsTransform, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: OverlayClip, sharedElement: @Composable (T, Modifier) -> Unit - ): @Composable (T, Modifier) -> Unit = when { - paneScope.isActive -> with(movableSharedElementHostState) { - paneScope.createOrUpdateSharedElement( - key = key, - boundsTransform = boundsTransform, - sharedElement = sharedElement - ) - } - // This pane state is be transitioning out. Check if it should be displayed without - // shared element semantics. - else -> when { - movableSharedElementHostState.isCurrentlyShared(key) -> - // The element is being shared in its new destination, stop showing it - // in the in active one - if (movableSharedElementHostState.isInProgress(key)) EmptyElement - // The element is not being shared in its new destination, allow it run its exit - // transition - else sharedElement - // Element isn't being shared anymore, show the element as is without sharing. - else -> sharedElement + ): @Composable (T, Modifier) -> Unit = { state, modifier -> + with(movableSharedElementHostState) { + val sharedContentState = rememberSharedContentState(key) + Box( + modifier + .sharedElementWithCallerManagedVisibility( + sharedContentState = sharedContentState, + visible = paneScope.isActive, + boundsTransform = boundsTransform, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, + ) + ) { + when { + paneScope.isActive -> + createOrUpdateSharedElement( + key = key, + sharedContentState = sharedContentState, + sharedElement = sharedElement + )(state, Modifier.matchParentSize()) + + // This pane state is be transitioning out. Check if it should be displayed without + // shared element semantics. + else -> when { + movableSharedElementHostState.isCurrentlyShared(key) -> + // The element is being shared in its new destination, stop showing it + // in the in active one + if (movableSharedElementHostState.isMatchFound(key)) EmptyElement( + state, + Modifier.matchParentSize() + ) + // The element is not being shared in its new destination, allow it run its exit + // transition + else sharedElement(state, Modifier.matchParentSize()) + // Element isn't being shared anymore, show the element as is without sharing. + else -> sharedElement(state, Modifier.matchParentSize()) + } + } + } } } } -private val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> } \ No newline at end of file +private val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> } + +@ExperimentalSharedTransitionApi +private val ParentClip: OverlayClip = + object : OverlayClip { + override fun getClipPath( + state: SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density + ): Path? { + return state.parentSharedContentState?.clipPathInOverlay + } + } \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt index 8d5fd38..2ca91a3 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt @@ -2,6 +2,7 @@ package com.tunjid.treenav.compose.threepane.configurations import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope.OverlayClip import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -9,11 +10,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PaneState import com.tunjid.treenav.compose.PanedNavHost import com.tunjid.treenav.compose.PanedNavHostConfiguration import com.tunjid.treenav.compose.delegated -import com.tunjid.treenav.compose.moveablesharedelement.AdaptiveMovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.PanedMovableSharedElementScope import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope import com.tunjid.treenav.compose.threepane.ThreePane @@ -37,7 +37,7 @@ fun PanedNavHostConfiguration< originalStrategy.delegated( render = { paneDestination -> val delegate = remember { - AdaptiveMovableSharedElementScope( + PanedMovableSharedElementScope( paneScope = this, movableSharedElementHostState = movableSharedElementHostState, ) @@ -71,13 +71,15 @@ fun PaneScope.movableSharedElementS @Stable private class ThreePaneMovableSharedElementScope( private val hostState: MovableSharedElementHostState, - private val delegate: AdaptiveMovableSharedElementScope, + private val delegate: PanedMovableSharedElementScope, ) : MovableSharedElementScope, PaneScope by delegate.paneScope { @OptIn(ExperimentalSharedTransitionApi::class) override fun movableSharedElementOf( key: Any, boundsTransform: BoundsTransform, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: OverlayClip, sharedElement: @Composable (T, Modifier) -> Unit ): @Composable (T, Modifier) -> Unit = when (paneState.pane) { null -> throw IllegalArgumentException( @@ -93,6 +95,8 @@ private class ThreePaneMovableSharedElementScope( else -> delegate.movableSharedElementOf( key = key, boundsTransform = boundsTransform, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, sharedElement = sharedElement ) } @@ -100,6 +104,8 @@ private class ThreePaneMovableSharedElementScope( ThreePane.TransientPrimary -> delegate.movableSharedElementOf( key = key, boundsTransform = boundsTransform, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, sharedElement = sharedElement ) @@ -110,9 +116,6 @@ private class ThreePaneMovableSharedElementScope( } } -fun PaneState?.canAnimateOnStartingFrames() = - this?.pane != ThreePane.TransientPrimary - private val PaneScope.isPreviewingBack: Boolean get() = paneState.pane == ThreePane.Primary && paneState.adaptations.contains(ThreePane.PrimaryToTransient) 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 50f9f0e..06b48e4 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 @@ -76,7 +76,6 @@ import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.me.mePaneStrategy import com.tunjid.demo.common.ui.profile.profilePaneStrategy import com.tunjid.treenav.MultiStackNav -import com.tunjid.treenav.compose.PaneState import com.tunjid.treenav.compose.PanedNavHost import com.tunjid.treenav.compose.PanedNavHostConfiguration import com.tunjid.treenav.compose.SavedStatePanedNavHostState @@ -85,7 +84,6 @@ import com.tunjid.treenav.compose.configurations.paneModifierConfiguration import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.panedNavHostConfiguration import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.configurations.canAnimateOnStartingFrames import com.tunjid.treenav.compose.threepane.configurations.predictiveBackConfiguration import com.tunjid.treenav.compose.threepane.configurations.threePanedMovableSharedElementConfiguration import com.tunjid.treenav.compose.threepane.configurations.threePanedNavHostConfiguration @@ -136,9 +134,8 @@ fun SampleApp( ) val density = LocalDensity.current val movableSharedElementHostState = remember { - MovableSharedElementHostState( - sharedTransitionScope = this, - canAnimateOnStartingFrames = PaneState::canAnimateOnStartingFrames + MovableSharedElementHostState( + sharedTransitionScope = this ) } @@ -200,7 +197,6 @@ fun SampleApp( state = splitLayoutState, modifier = Modifier .fillMaxSize() - then movableSharedElementHostState.modifier then sharedTransitionModifier, itemSeparators = { paneIndex, offset -> PaneSeparator( From abfb6ab7dbe7d518e763dda631a4777a2dc0edd4 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 21:20:30 -0800 Subject: [PATCH 4/8] Add alternateOutgoingSharedElement --- .../MovableSharedElementState.kt | 9 +-- .../MovableSharedElements.kt | 66 ++++++++++++++----- .../MovableSharedElementConfiguration.kt | 12 +++- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt index 44f3ef0..05ba0cf 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt @@ -8,18 +8,19 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -@Stable -internal class MovableSharedElementState @OptIn(ExperimentalSharedTransitionApi::class) -constructor( - val sharedContentState: SharedTransitionScope.SharedContentState, +@Stable +internal class MovableSharedElementState( + sharedContentState: SharedTransitionScope.SharedContentState, sharedElement: @Composable (State, Modifier) -> Unit, onRemoved: () -> Unit ) { + var sharedContentState by mutableStateOf(sharedContentState) private var composedRefCount by mutableIntStateOf(0) val moveableSharedElement: @Composable (Any?, Modifier) -> Unit = 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 9d6dcff..85fd88f 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 @@ -4,6 +4,8 @@ import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize import androidx.compose.animation.SharedTransitionScope.SharedContentState import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable @@ -35,16 +37,38 @@ interface MovableSharedElementScope { * NOTE: It is an error to compose the movable shared element in different locations * simultaneously, and the behavior of the shared element is undefined in this case. * - * @param key the shared element key to identify the movable shared element. - * @param sharedElement a factory function to create the shared element if it does not + * @param key The shared element key to identify the movable shared element. + * @param boundsTransform Allows for customizing the animation for the bounds of + * the [sharedElement]. + * @param placeHolderSize Allows for adjusting the reported size to the parent layout during + * the transition. + * @param renderInOverlayDuringTransition Is true by default. In some rare use cases, there may + * be no clipping or layer transform (fade, scale, etc) in the application that prevents + * shared elements from transitioning from one bounds to another without any clipping or + * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified + * to false. + * @param zIndexInOverlay Can be specified to allow shared elements to render in a + * different order than their placement/zOrder when not in the overlay. + * @param clipInOverlayDuringTransition Can be used to specify the clipping for when the + * shared element is going through an active transition towards a new target bounds. + * @param alternateOutgoingSharedElement By default, a separate instance of the + * [sharedElement] is rendered when content is being animated out. When specified, this + * is rendered instead. This is useful for shared elements that can only be reasonable + * rendered in one place at any one time like video. + * @param sharedElement A factory function to create the movable shared element if it does not * currently exist. + * + * @see [SharedTransitionScope.sharedElementWithCallerManagedVisibility] */ @OptIn(ExperimentalSharedTransitionApi::class) fun movableSharedElementOf( key: Any, boundsTransform: BoundsTransform, + placeHolderSize: PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, zIndexInOverlay: Float, clipInOverlayDuringTransition: OverlayClip, + alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)?, sharedElement: @Composable (T, Modifier) -> Unit ): @Composable (T, Modifier) -> Unit } @@ -62,14 +86,20 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( state: T, modifier: Modifier = Modifier, boundsTransform: BoundsTransform = DefaultBoundsTransform, + placeHolderSize: PlaceHolderSize = contentSize, + renderInOverlayDuringTransition: Boolean = true, zIndexInOverlay: Float = 0f, clipInOverlayDuringTransition: OverlayClip = ParentClip, + alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)? = null, sharedElement: @Composable (T, Modifier) -> Unit ) = movableSharedElementOf( key = key, boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, clipInOverlayDuringTransition = clipInOverlayDuringTransition, + alternateOutgoingSharedElement = alternateOutgoingSharedElement, sharedElement = sharedElement ).invoke( state, @@ -119,7 +149,7 @@ class MovableSharedElementHostState( sharedElement = sharedElement, onRemoved = { keysToMovableSharedElements.remove(key) } ) - } + }.also { it.sharedContentState = sharedContentState } // Can't really guarantee that the caller will use the same key for the right type return movableSharedElementState.moveableSharedElement @@ -145,8 +175,11 @@ class PanedMovableSharedElementScope( override fun movableSharedElementOf( key: Any, boundsTransform: BoundsTransform, + placeHolderSize: PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, zIndexInOverlay: Float, clipInOverlayDuringTransition: OverlayClip, + alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)?, sharedElement: @Composable (T, Modifier) -> Unit ): @Composable (T, Modifier) -> Unit = { state, modifier -> with(movableSharedElementHostState) { @@ -157,6 +190,8 @@ class PanedMovableSharedElementScope( sharedContentState = sharedContentState, visible = paneScope.isActive, boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, clipInOverlayDuringTransition = clipInOverlayDuringTransition, ) @@ -172,18 +207,19 @@ class PanedMovableSharedElementScope( // This pane state is be transitioning out. Check if it should be displayed without // shared element semantics. else -> when { - movableSharedElementHostState.isCurrentlyShared(key) -> - // The element is being shared in its new destination, stop showing it - // in the in active one - if (movableSharedElementHostState.isMatchFound(key)) EmptyElement( - state, - Modifier.matchParentSize() - ) - // The element is not being shared in its new destination, allow it run its exit - // transition - else sharedElement(state, Modifier.matchParentSize()) - // Element isn't being shared anymore, show the element as is without sharing. - else -> sharedElement(state, Modifier.matchParentSize()) + // The element is being shared in its new destination, stop showing it + // in the in active one + movableSharedElementHostState.isCurrentlyShared(key) + && movableSharedElementHostState.isMatchFound(key) -> EmptyElement( + state, + Modifier.matchParentSize() + ) + // The element is not being shared in its new destination, allow it run its exit + // transition + isTransitionActive -> (alternateOutgoingSharedElement ?: sharedElement)( + state, + Modifier.matchParentSize() + ) } } } diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt index 2ca91a3..a43c83a 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt @@ -3,6 +3,7 @@ package com.tunjid.treenav.compose.threepane.configurations import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -78,8 +79,11 @@ private class ThreePaneMovableSharedElementScope( override fun movableSharedElementOf( key: Any, boundsTransform: BoundsTransform, + placeHolderSize: PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, zIndexInOverlay: Float, clipInOverlayDuringTransition: OverlayClip, + alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)?, sharedElement: @Composable (T, Modifier) -> Unit ): @Composable (T, Modifier) -> Unit = when (paneState.pane) { null -> throw IllegalArgumentException( @@ -95,8 +99,11 @@ private class ThreePaneMovableSharedElementScope( else -> delegate.movableSharedElementOf( key = key, boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, clipInOverlayDuringTransition = clipInOverlayDuringTransition, + alternateOutgoingSharedElement = alternateOutgoingSharedElement, sharedElement = sharedElement ) } @@ -104,15 +111,18 @@ private class ThreePaneMovableSharedElementScope( ThreePane.TransientPrimary -> delegate.movableSharedElementOf( key = key, boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, clipInOverlayDuringTransition = clipInOverlayDuringTransition, + alternateOutgoingSharedElement = alternateOutgoingSharedElement, sharedElement = sharedElement ) // In the other panes use the element as is ThreePane.Secondary, ThreePane.Tertiary, - ThreePane.Overlay -> sharedElement + ThreePane.Overlay -> alternateOutgoingSharedElement ?: sharedElement } } From 04a67298d7b43d2c7785e5731b39ac3825c7165b Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 21:21:06 -0800 Subject: [PATCH 5/8] Update kdocs --- .../MovableSharedElements.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 85fd88f..bd68a1a 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 @@ -78,6 +78,27 @@ interface MovableSharedElementScope { * movable shared element with the latest values of [state] and [modifier]. * * @see [MovableSharedElementScope.movableSharedElementOf]. + * + * @param key The shared element key to identify the movable shared element. + * @param boundsTransform Allows for customizing the animation for the bounds of + * the [sharedElement]. + * @param placeHolderSize Allows for adjusting the reported size to the parent layout during + * the transition. + * @param renderInOverlayDuringTransition Is true by default. In some rare use cases, there may + * be no clipping or layer transform (fade, scale, etc) in the application that prevents + * shared elements from transitioning from one bounds to another without any clipping or + * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified + * to false. + * @param zIndexInOverlay Can be specified to allow shared elements to render in a + * different order than their placement/zOrder when not in the overlay. + * @param clipInOverlayDuringTransition Can be used to specify the clipping for when the + * shared element is going through an active transition towards a new target bounds. + * @param alternateOutgoingSharedElement By default, a separate instance of the + * [sharedElement] is rendered when content is being animated out. When specified, this + * is rendered instead. This is useful for shared elements that can only be reasonable + * rendered in one place at any one time like video. + * @param sharedElement A factory function to create the movable shared element if it does not + * currently exist. */ @OptIn(ExperimentalSharedTransitionApi::class) @Composable From e41151f022c796a5e5129d3ba5502c71d4e13608 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 21:25:45 -0800 Subject: [PATCH 6/8] Don't use isTransitionActive in PanedMovableSharedElementScope --- .../compose/moveablesharedelement/MovableSharedElements.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bd68a1a..9f05642 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 @@ -237,7 +237,7 @@ class PanedMovableSharedElementScope( ) // The element is not being shared in its new destination, allow it run its exit // transition - isTransitionActive -> (alternateOutgoingSharedElement ?: sharedElement)( + else -> (alternateOutgoingSharedElement ?: sharedElement)( state, Modifier.matchParentSize() ) From 8ce6f279e746bf11c0ced0d22c90b1f3e94b9fad Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 22:06:50 -0800 Subject: [PATCH 7/8] Allow for retrieving the ViewModelStoreOwner for an arbitrary destination --- .../compose/SavedStatePanedNavHostState.kt | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt index 36b48d5..164da9a 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt @@ -103,7 +103,7 @@ class SavedStatePanedNavHostState( val saveableStateHolder = rememberSaveableStateHolder() val panedContentScope = remember { - SavedStatePanedNavHostScope( + NavHostScope( panes = panes, navHostConfiguration = configuration, initialPanesToNodes = panesToNodes, @@ -123,19 +123,19 @@ class SavedStatePanedNavHostState( companion object { @Stable - private class SavedStatePanedNavHostScope( + class NavHostScope internal constructor( panes: List, initialPanesToNodes: Map, saveableStateHolder: SaveableStateHolder, val navHostConfiguration: PanedNavHostConfiguration, ) : PanedNavHostScope, SaveableStateHolder by saveableStateHolder { - val slots = List( + private val slots = List( size = panes.size, init = ::Slot ).toSet() - var panedNavigationState by mutableStateOf( + private var panedNavigationState by mutableStateOf( value = SlotBasedPanedNavigationState.initial(slots = slots) .adaptTo( slots = slots, @@ -156,6 +156,19 @@ class SavedStatePanedNavHostState( } } + /** + * Retrieves the a [ViewModelStoreOwner] for a given [destination]. All destinations + * with the same [Node.id] share the same [ViewModelStoreOwner]. + * + * The [destination] must be present in the navigation tree, otherwise an + * [IllegalStateException] will be thrown. + * + * @param destination The destination for which the [ViewModelStoreOwner] should + * be retrieved. + */ + fun viewModelStoreOwnerFor(destination: Destination): ViewModelStoreOwner = + destinationViewModelStoreCreator.viewModelStoreOwnerFor(destination) + @Composable override fun Destination(pane: Pane) { val slot = panedNavigationState.slotFor(pane) @@ -170,7 +183,7 @@ class SavedStatePanedNavHostState( pane: Pane ): Destination? = panedNavigationState.destinationFor(pane) - fun onNewNavigationState( + internal fun onNewNavigationState( navigationState: Node, panesToNodes: Map, ) { @@ -305,3 +318,13 @@ class SavedStatePanedNavHostState( } } } + +fun PanedNavHostScope< + Pane, + Destination + >.requireSavedStatePanedNavHostScope(): SavedStatePanedNavHostState.Companion.NavHostScope { + check(this is SavedStatePanedNavHostState.Companion.NavHostScope) { + "This PanedNavHostScope instance is not a SavedStatePanedNavHostScope" + } + return this +} From 96d635d805bc173139a85f639df91bd5f1d25ffc Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 17 Nov 2024 22:10:19 -0800 Subject: [PATCH 8/8] Bump jetbrains compose --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c81a3b..5715b74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,11 +12,11 @@ androidxTestExt = "1.2.1" androidxTestRunner = "1.6.2" androidxTestRules = "1.6.1" dokka = "1.8.20" -jetbrainsCompose = "1.7.0" -jetbrainsLifecycle = "2.8.3" +jetbrainsCompose = "1.7.1" +jetbrainsLifecycle = "2.8.4" jetbrainsMaterial3Adaptive = "1.0.0" junit4 = "4.13.2" -kotlin = "2.0.20" +kotlin = "2.0.21" kotlinxCoroutines = "1.9.0" kotlinxDatetime = "0.6.1" lifecycle-runtime = "2.8.6"