这是indexloc提供的服务,不要输入任何密码
Skip to content

Movable shared element semantics #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ kotlin {
iosSimulatorArm64(),
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "treenav-adaptive"
baseName = "treenav-compose"
isStatic = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.tunjid.treenav.Node
import kotlin.jvm.JvmInline

/**
* Scope for adaptive content that can show up in an arbitrary pane.
* Scope for navigation destinations that can show up in an arbitrary pane.
*/
@Stable
interface PaneScope<Pane, Destination : Node> : AnimatedVisibilityScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import androidx.compose.ui.Modifier
import com.tunjid.treenav.Node

/**
* Creates a host for adaptive navigation for panes [Pane] and destinations [Destination].
* Creates a navigation host for destinations [Destination] that can show up
* in arbitrary panes [Pane].
*
* @param state the [PanedNavHostState] producing the [PanedNavHostScope] that provides
* context about the panes in [PanedNavHost].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ class PanedNavHostConfiguration<Pane, NavigationState : Node, Destination : Node

/**
* Provides an [PanedNavHostConfiguration] for configuring an [PanedNavHost] for
* adapting different navigation destinations into different panes from an arbitrary
* showing different navigation destinations into different panes from an arbitrary
* [navigationState].
*
* @param navigationState the navigation state to be adapted into various panes.
* @param navigationState the navigation state to with destinations [Destination] various
* panes [Pane].
* @param destinationTransform a transform of the [navigationState] to its current destination.
* It is read inside a [derivedStateOf] block, so reads of snapshot
* state objects will be observed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
val panesToNodes = configuration.paneMapping()
val saveableStateHolder = rememberSaveableStateHolder()

val adaptiveContentScope = remember {
val panedContentScope = remember {
SavedStatePanedNavHostScope(
panes = panes,
navHostConfiguration = configuration,
Expand All @@ -95,13 +95,13 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
}

LaunchedEffect(navigationState, panesToNodes) {
adaptiveContentScope.onNewNavigationState(
panedContentScope.onNewNavigationState(
navigationState = navigationState,
panesToNodes = panesToNodes
)
}

return adaptiveContentScope
return panedContentScope
}

companion object {
Expand All @@ -122,7 +122,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
init = ::Slot
).toSet()

var adaptiveNavigationState by mutableStateOf(
var panedNavigationState by mutableStateOf(
value = SlotBasedPanedNavigationState.initial<Pane, Destination>(slots = slots)
.adaptTo(
slots = slots,
Expand All @@ -141,17 +141,17 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(

@Composable
override fun Destination(pane: Pane) {
val slot = adaptiveNavigationState.slotFor(pane)
val slot = panedNavigationState.slotFor(pane)
slotsToRoutes[slot]?.invoke()
}

override fun adaptationsIn(
pane: Pane
): Set<Adaptation> = adaptiveNavigationState.adaptationsIn(pane)
): Set<Adaptation> = panedNavigationState.adaptationsIn(pane)

override fun nodeFor(
pane: Pane
): Destination? = adaptiveNavigationState.destinationFor(pane)
): Destination? = panedNavigationState.destinationFor(pane)

fun onNewNavigationState(
navigationState: Node,
Expand All @@ -175,7 +175,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
slot: Slot,
) {
val paneTransition = updateTransition(
targetState = adaptiveNavigationState.paneStateFor(slot),
targetState = panedNavigationState.paneStateFor(slot),
label = "$slot-PaneTransition",
)
paneTransition.AnimatedContent(
Expand All @@ -192,7 +192,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
AnimatedPaneScope(
paneState = targetPaneState,
activeState = derivedStateOf {
val activePaneState = adaptiveNavigationState.paneStateFor(slot)
val activePaneState = panedNavigationState.paneStateFor(slot)
activePaneState.currentDestination?.id == targetPaneState.currentDestination?.id
},
animatedContentScope = this@AnimatedContent,
Expand Down Expand Up @@ -220,7 +220,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(

DisposableEffect(Unit) {
onDispose {
val backstackIds = adaptiveNavigationState.backStackIds
val backstackIds = panedNavigationState.backStackIds
if (!backstackIds.contains(destination.id)) removeState(
destination.id
)
Expand All @@ -231,18 +231,18 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
DisposableEffect(
hostLifecycleState,
scope.isActive,
adaptiveNavigationState,
panedNavigationState,
) {
destinationLifecycleOwner.update(
hostLifecycleState = hostLifecycleState,
paneScope = scope,
adaptiveNavigationState = adaptiveNavigationState
panedNavigationState = panedNavigationState
)
onDispose {
destinationLifecycleOwner.update(
hostLifecycleState = hostLifecycleState,
paneScope = scope,
adaptiveNavigationState = adaptiveNavigationState
panedNavigationState = panedNavigationState
)
}
}
Expand Down Expand Up @@ -276,7 +276,7 @@ class SavedStatePanedNavHostState<Pane, Destination : Node>(
private inline fun updateAdaptiveNavigationState(
block: SlotBasedPanedNavigationState<Pane, Destination>.() -> SlotBasedPanedNavigationState<Pane, Destination>
) {
adaptiveNavigationState = adaptiveNavigationState.block()
panedNavigationState = panedNavigationState.block()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ internal class DestinationLifecycleOwner(
fun update(
hostLifecycleState: State,
paneScope: PaneScope<*, *>,
adaptiveNavigationState: SlotBasedPanedNavigationState<*, *>,
panedNavigationState: SlotBasedPanedNavigationState<*, *>,
) {
val active = paneScope.isActive
val exists = adaptiveNavigationState.backStackIds.contains(
val exists = panedNavigationState.backStackIds.contains(
destination.id
)
val derivedLifecycleState = when {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tunjid.scaffold.treenav.adaptive.moveablesharedelement
package com.tunjid.treenav.compose.moveablesharedelement

import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.ExperimentalSharedTransitionApi
Expand Down Expand Up @@ -36,7 +36,6 @@ import androidx.compose.ui.util.fastRoundToInt
import com.tunjid.treenav.Node
import com.tunjid.treenav.compose.PaneScope
import com.tunjid.treenav.compose.PaneState
import com.tunjid.treenav.compose.moveablesharedelement.BoundsTransformDeferredAnimation
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.first

Expand All @@ -57,15 +56,16 @@ internal class MovableSharedElementState<State, Pane, Destination : Node>(

private var layer: GraphicsLayer? = null
private var targetOffset by mutableStateOf(IntOffset.Zero)
private var boundsAnimInProgress by mutableStateOf(false)
var animInProgress by mutableStateOf(false)
private set

private val canDrawInOverlay get() = boundsAnimInProgress
private val canDrawInOverlay get() = animInProgress
private val panesKeysToSeenCount = mutableStateMapOf<String, Unit>()

private val animatedBounds: Rect?
get() = if (boundsAnimInProgress) boundsAnimation.animatedValue else null
get() = if (animInProgress) boundsAnimation.animatedValue else null

val boundsAnimation = BoundsTransformDeferredAnimation()
private val boundsAnimation = BoundsTransformDeferredAnimation()

val moveableSharedElement: @Composable (Any?, Modifier) -> Unit =
movableContentOf { state, modifier ->
Expand Down Expand Up @@ -118,15 +118,15 @@ internal class MovableSharedElementState<State, Pane, Destination : Node>(
state: MovableSharedElementState<*, Pane, Destination>,
): Modifier {
val coroutineScope = rememberCoroutineScope()
state.isInProgress().also { state.boundsAnimInProgress = it }
state.isInProgress().also { state.animInProgress = it }
val layer = rememberGraphicsLayer().also {
state.layer = it
}
return approachLayout(
isMeasurementApproachInProgress = { lookaheadSize ->
// Update target size, it will serve to know if we expect an approach in progress
state.boundsAnimation.updateTargetSize(lookaheadSize.toSize())
state.boundsAnimInProgress
state.animInProgress
},
isPlacementApproachInProgress = {
state.boundsAnimation.updateTargetOffsetAndAnimate(
Expand All @@ -136,7 +136,7 @@ internal class MovableSharedElementState<State, Pane, Destination : Node>(
includeMotionFrameOfReference = true,
boundsTransform = state.boundsTransform,
)
state.boundsAnimInProgress
state.animInProgress
},
approachMeasure = { measurable, _ ->
// The animated value is null on the first frame as we don't get the full bounds
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tunjid.scaffold.treenav.adaptive.moveablesharedelement
package com.tunjid.treenav.compose.moveablesharedelement

import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.ExperimentalSharedTransitionApi
Expand Down Expand Up @@ -85,6 +85,12 @@ class MovableSharedElementHostState<Pane, Destination : Node>(
fun isCurrentlyShared(key: Any): Boolean =
keysToMovableSharedElements.contains(key)

/**
* Returns true if a movable shared element is animating.
*/
fun isInProgress(key: Any): Boolean =
keysToMovableSharedElements[key]?.animInProgress == 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
Expand Down Expand Up @@ -131,24 +137,31 @@ internal class AdaptiveMovableSharedElementScope<T, R : Node>(
key: Any,
boundsTransform: BoundsTransform,
sharedElement: @Composable (T, Modifier) -> Unit
): @Composable (T, Modifier) -> Unit {
// This pane state may be animating out. Look up the actual current route
// Do not use the shared element if this content is being animated out
if (!paneScope.isActive) return emptyComposable()

return with(movableSharedElementHostState) {
): @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
}
}
}

private fun <T> emptyComposable(): @Composable (T, Modifier) -> Unit = EMPTY_COMPOSABLE

private val EMPTY_COMPOSABLE: @Composable (Any?, Modifier) -> Unit = { _, _ -> }
private val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> }

@OptIn(ExperimentalSharedTransitionApi::class)
private val DefaultBoundsTransform = BoundsTransform { _, _ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.tunjid.scaffold.treenav.adaptive.moveablesharedelement.AdaptiveMovableSharedElementScope
import com.tunjid.scaffold.treenav.adaptive.moveablesharedelement.MovableSharedElementHostState
import com.tunjid.scaffold.treenav.adaptive.moveablesharedelement.MovableSharedElementScope
import com.tunjid.treenav.Node
import com.tunjid.treenav.compose.PanedNavHost
import com.tunjid.treenav.compose.PanedNavHostConfiguration
import com.tunjid.treenav.compose.PaneScope
import com.tunjid.treenav.compose.PaneState
import com.tunjid.treenav.compose.PaneStrategy
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.MovableSharedElementHostState
import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope
import com.tunjid.treenav.compose.threepane.ThreePane


Expand Down Expand Up @@ -84,39 +84,34 @@ private class ThreePaneMovableSharedElementScope<Destination : Node>(
key: Any,
boundsTransform: BoundsTransform,
sharedElement: @Composable (T, Modifier) -> Unit
): @Composable (T, Modifier) -> Unit {
val paneScope = delegate.paneScope
return when (paneScope.paneState.pane) {
null -> throw IllegalArgumentException(
"Shared elements may only be used in non null panes"
)
// Allow shared elements in the primary or transient primary content only
ThreePane.Primary -> when {
// Show a blank space for shared elements between the destinations
paneScope.isPreviewingBack && hostState.isCurrentlyShared(key) -> { _, modifier ->
Box(modifier)
}
// If previewing and it won't be shared, show the item as is
paneScope.isPreviewingBack -> sharedElement
// Share the element
else -> delegate.movableSharedElementOf(
key = key,
boundsTransform = boundsTransform,
sharedElement = sharedElement
)
}
// Share the element when in the transient pane
ThreePane.TransientPrimary -> delegate.movableSharedElementOf(
): @Composable (T, Modifier) -> Unit = when (paneState.pane) {
null -> throw IllegalArgumentException(
"Shared elements may only be used in non null panes"
)
// Allow shared elements in the primary or transient primary content only
ThreePane.Primary -> when {
// Show a blank space for shared elements between the destinations
isPreviewingBack && hostState.isCurrentlyShared(key) -> EmptyElement
// If previewing and it won't be shared, show the item as is
isPreviewingBack -> sharedElement
// Share the element
else -> delegate.movableSharedElementOf(
key = key,
boundsTransform = boundsTransform,
sharedElement = sharedElement
)

// In the other panes use the element as is
ThreePane.Secondary,
ThreePane.Tertiary,
ThreePane.Overlay -> sharedElement
}
// Share the element when in the transient pane
ThreePane.TransientPrimary -> delegate.movableSharedElementOf(
key = key,
boundsTransform = boundsTransform,
sharedElement = sharedElement
)

// In the other panes use the element as is
ThreePane.Secondary,
ThreePane.Tertiary,
ThreePane.Overlay -> sharedElement
}
}

Expand All @@ -125,4 +120,9 @@ fun PaneState<ThreePane, *>?.canAnimateOnStartingFrames() =

private val PaneScope<ThreePane, *>.isPreviewingBack: Boolean
get() = paneState.pane == ThreePane.Primary
&& paneState.adaptations.contains(ThreePane.PrimaryToTransient)
&& paneState.adaptations.contains(ThreePane.PrimaryToTransient)

// An empty element representing blank space
private val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, modifier ->
Box(modifier)
}
Loading
Loading