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

Update scaffolding for persistent UI to use movable content #32

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 8 commits into from
May 17, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<Destination : Node> :
PaneSharedTransitionScope<ThreePane, Destination>, MovableSharedElementScope
typealias ThreePaneMovableElementSharedTransitionScope<Destination> =
PaneMovableElementSharedTransitionScope<ThreePane, Destination>

/**
* 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 <Destination : Node> PaneScope<
ThreePane,
Destination
>.rememberPaneMovableElementSharedTransitionScope(
movableSharedElementScope: MovableSharedElementScope = requireMovableSharedElementScope()
): PaneMovableElementSharedTransitionScope<Destination> {
>.rememberThreePaneMovableElementSharedTransitionScope(
movableSharedElementScope: MovableSharedElementScope = requireThreePaneMovableSharedElementScope()
): ThreePaneMovableElementSharedTransitionScope<Destination> {
val paneSharedTransitionScope = rememberPaneSharedTransitionScope(
movableSharedElementScope.sharedTransitionScope
)
return remember {
DelegatingPaneMovableElementSharedTransitionScope(
paneSharedTransitionScope = paneSharedTransitionScope,
movableSharedElementScope = movableSharedElementScope,
)
}
return rememberPaneMovableElementSharedTransitionScope(
paneSharedTransitionScope = paneSharedTransitionScope,
movableSharedElementScope = movableSharedElementScope,
)
}

@Stable
private class DelegatingPaneMovableElementSharedTransitionScope<Destination : Node>(
val paneSharedTransitionScope: PaneSharedTransitionScope<ThreePane, Destination>,
val movableSharedElementScope: MovableSharedElementScope,
) : PaneMovableElementSharedTransitionScope<Destination>,
PaneSharedTransitionScope<ThreePane, Destination> by paneSharedTransitionScope,
MovableSharedElementScope by movableSharedElementScope

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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].
*/
Expand All @@ -51,12 +70,9 @@ fun <NavigationState : Node, Destination : Node>
movableSharedElementHostState: MovableSharedElementHostState<ThreePane, Destination>,
): Transform<ThreePane, NavigationState, Destination> =
RenderTransform { destination, previousTransform ->
val delegate = remember {
PaneMovableSharedElementScope(
paneScope = this,
movableSharedElementHostState = movableSharedElementHostState,
)
}
val delegate = rememberPaneMovableSharedElementScope(
movableSharedElementHostState = movableSharedElementHostState
)
delegate.paneScope = this

val movableSharedElementScope = remember {
Expand All @@ -70,14 +86,18 @@ fun <NavigationState : Node, Destination : Node>
}

/**
* 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 <Destination : Node> PaneScope<
ThreePane,
Destination
>.requireMovableSharedElementScope(): MovableSharedElementScope {
>.requireThreePaneMovableSharedElementScope(): MovableSharedElementScope {
check(this is ThreePaneMovableSharedElementScope) {
"""
The current PaneScope (${this::class.qualifiedName}) is not an instance of
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pane, Destination : Node> :
PaneSharedTransitionScope<Pane, Destination>, 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 <Pane, Destination : Node> rememberPaneMovableElementSharedTransitionScope(
paneSharedTransitionScope: PaneSharedTransitionScope<Pane, Destination>,
movableSharedElementScope: MovableSharedElementScope,
): PaneMovableElementSharedTransitionScope<Pane, Destination> {
return remember {
DelegatingPaneMovableElementSharedTransitionScope(
paneSharedTransitionScope = paneSharedTransitionScope,
movableSharedElementScope = movableSharedElementScope,
)
}
}

@Stable
private class DelegatingPaneMovableElementSharedTransitionScope<Pane, Destination : Node>(
val paneSharedTransitionScope: PaneSharedTransitionScope<Pane, Destination>,
val movableSharedElementScope: MovableSharedElementScope,
) : PaneMovableElementSharedTransitionScope<Pane, Destination>,
PaneSharedTransitionScope<Pane, Destination> by paneSharedTransitionScope,
MovableSharedElementScope by movableSharedElementScope
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -179,6 +181,16 @@ class MovableSharedElementHostState<Pane, Destination : Node>(
}
}

@Composable
fun <Pane, Destination : Node> PaneScope<Pane, Destination>.rememberPaneMovableSharedElementScope(
movableSharedElementHostState: MovableSharedElementHostState<Pane, Destination>
) = remember {
PaneMovableSharedElementScope(
paneScope = this,
movableSharedElementHostState = movableSharedElementHostState
)
}

/**
* An implementation of [MovableSharedElementScope] that ensures shared elements are only rendered
* in an [PaneScope] when it is active.
Expand All @@ -188,9 +200,9 @@ class MovableSharedElementHostState<Pane, Destination : Node>(
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Stable
class PaneMovableSharedElementScope<T, R : Node>(
paneScope: PaneScope<T, R>,
private val movableSharedElementHostState: MovableSharedElementHostState<T, R>,
class PaneMovableSharedElementScope<Pane, Destination : Node> internal constructor(
paneScope: PaneScope<Pane, Destination>,
private val movableSharedElementHostState: MovableSharedElementHostState<Pane, Destination>,
) : MovableSharedElementScope {

override val sharedTransitionScope: SharedTransitionScope
Expand Down Expand Up @@ -229,7 +241,7 @@ class PaneMovableSharedElementScope<T, R : Node>(
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.
Expand All @@ -239,17 +251,33 @@ class PaneMovableSharedElementScope<T, R : Node>(
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()
)
}
}
}
}
}
}

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ 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
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
Expand Down Expand Up @@ -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
Expand All @@ -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<ThreePane, SampleDestination>(
sharedTransitionScope = this
)
}
appState.movableSharedElementHostState = movableSharedElementHostState
MultiPaneDisplay(
modifier = Modifier
.fillMaxSize(),
Expand Down Expand Up @@ -242,6 +241,8 @@ class AppState(
private val navigationRepository: NavigationRepository = NavigationRepository,
) {

internal lateinit var movableSharedElementHostState: MovableSharedElementHostState<ThreePane, SampleDestination>

private val navigationState = mutableStateOf(
navigationRepository.navigationStateFlow.value
)
Expand Down Expand Up @@ -276,14 +277,24 @@ class AppState(
null
)

internal val movableNavigationBar =
movableContentWithReceiverOf<NavigationBarState, Modifier> { modifier ->
PaneNavigationBar(modifier)
}

internal val movableNavigationRail =
movableContentWithReceiverOf<NavigationBarState, Modifier> { modifier ->
PaneNavigationRail(modifier)
}

val filteredPaneOrder: List<ThreePane> by derivedStateOf {
paneRenderOrder.filter { displayScope?.destinationIn(it) != null }
}

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)
}
}

Expand Down
Loading