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

Make MultiPaneDisplay a downstream implementation of NavDisplay and its scenes API #36

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 78 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
e8ba353
Copied classes for nav3 scenes
tunjid May 21, 2025
cc823c3
Update packages
tunjid May 21, 2025
36b6a99
Add MultiPaneDisplayScene, WIP
tunjid May 24, 2025
f8433b3
Only update back stack if it changes
tunjid May 24, 2025
588e9fa
Match up navigation states
tunjid May 25, 2025
3e9eb7b
Clean up code
tunjid May 25, 2025
20b376b
Move utility methods
tunjid May 25, 2025
c5081ec
Update MultiPaneDisplayScene
tunjid May 25, 2025
3986bef
Simplify code
tunjid May 25, 2025
2347781
Tidy up code
tunjid May 25, 2025
f3c4925
Remove unnecessary animations on PaneEntry
tunjid May 25, 2025
379692e
Continue consolidating on MultipaneDisplay
tunjid May 26, 2025
f3399ee
Match changes in upstream navigation 3
tunjid Jun 20, 2025
e7acd14
Delete DecoratedNavEntryMultiPaneDisplayScope
tunjid Jun 20, 2025
ec66b13
Start to streamline APIs
tunjid Jun 21, 2025
b72ca5f
Reallow duplicates in MultipaneDisplay backstack
tunjid Jun 21, 2025
0961952
Reallow duplicates in MultipaneDisplay backstack II
tunjid Jun 21, 2025
001ff09
Delete dangling class
tunjid Jun 21, 2025
05ffc97
Add contentKey to NavDisplay and allow duplicates in backstack
tunjid Jun 21, 2025
bf6a1a3
Better matching for popping backstack
tunjid Jun 21, 2025
afdd8df
Acces modifiers for constants
tunjid Jun 21, 2025
6825a4c
Pass PaneScaffoldState to screens
tunjid Jun 21, 2025
2ec80c5
Pass PaneScaffoldState to screens II
tunjid Jun 21, 2025
849cef2
Got rid of unnecessary destination transform
tunjid Jun 21, 2025
7fcc16b
Bump CMP version
tunjid Jun 22, 2025
65bb1d3
Update arguments of paneSharedElement
tunjid Jun 22, 2025
d1b83bd
Fix crash in SavedStateNavEntryDecorator filed in https://issuetracke…
tunjid Jun 22, 2025
453b003
Update signature of movableShared element API to take a SharedContent…
tunjid Jun 22, 2025
9c91517
Add isPreviewing to PaneScope
tunjid Jun 22, 2025
fa5ac22
Make profileFor synchronous
tunjid Jun 22, 2025
19baf7b
Update shared elements for ChatScreen
tunjid Jun 22, 2025
b8f72d8
Allow shared elements in certain conditions for primary to secondary
tunjid Jun 22, 2025
c6d0221
Add ContentTransform lambda to MultiPaneDisplayState
tunjid Jun 23, 2025
79dcd36
Remove Modifier.composed from Modifier.paneSharedElement
tunjid Jun 23, 2025
c2058d5
Use paneShareElement forr toolbar titles, update shared element in Av…
tunjid Jun 23, 2025
a24b3b7
Encapsulate PaneEntry in MultiPaneDisplayState
tunjid Jun 23, 2025
973804d
Simplify declaration of MultiPaneDisplay
tunjid Jun 23, 2025
4490c63
Support pane level animations
tunjid Jun 23, 2025
fee5646
Pane animation tweaks
tunjid Jun 23, 2025
432e8d9
More rules for adaptive animations
tunjid Jun 23, 2025
d29b28f
More adaptive animation rules
tunjid Jun 23, 2025
57e35e3
Add static NavigationEventDispatcherOwner for desktop and iOS
tunjid Jun 24, 2025
0163ff3
Use viewmodel coroutine scope
tunjid Jun 24, 2025
32f662d
Try to cut down allocations during pane animations
tunjid Jun 24, 2025
3cb1a6a
Cutting down on more allocations
tunjid Jun 24, 2025
f09f75b
More allocation optimizations
tunjid Jun 24, 2025
61b6122
More optimizations
tunjid Jun 24, 2025
37c1c4e
Add identity hashcode method
tunjid Jun 24, 2025
23a5459
Even more optimizations
tunjid Jun 24, 2025
b0faa1c
Annotate MultiPaneDisplayScene as stable
tunjid Jun 24, 2025
8cce71f
Better names for parameters and fields
tunjid Jun 24, 2025
38d4fe3
Clean up redundant static fields
tunjid Jun 24, 2025
d5c2b7d
Remove unused import
tunjid Jun 24, 2025
c261501
Update docs
tunjid Jun 24, 2025
2f5cf89
Update stale animation
tunjid Jun 24, 2025
e147d91
Fix DragToPop and back previews. Expose NavigationEvent APIs
tunjid Jun 24, 2025
fb11d29
Mark MultipaneDisplayState as stable
tunjid Jun 25, 2025
65cb577
More optimizations
tunjid Jun 25, 2025
70f3697
Update definition of isActive
tunjid Jun 25, 2025
3dc690d
Revert AnimatedPaneScope immutability changes. Try to identify laggin…
tunjid Jun 27, 2025
d60724e
Rearranged method
tunjid Jun 28, 2025
2f69e72
Fix twitching animations
tunjid Jun 28, 2025
b7aeb65
Clean up code
tunjid Jun 28, 2025
a3b7b47
More clean up
tunjid Jun 28, 2025
ec56837
More performance optimizations
tunjid Jun 28, 2025
7fe27a6
Better names
tunjid Jun 28, 2025
01fc324
Remove inPredictiveBack from MultiPaneDisplayScope
tunjid Jun 28, 2025
2fb6104
Update kdoc
tunjid Jun 28, 2025
362a644
Tidying up MultiPaneDisplay
tunjid Jun 28, 2025
6ede4f8
Make PaneDestinationMultiPaneDisplayScope a concrete class
tunjid Jun 28, 2025
85e5286
Cleaned up code
tunjid Jun 28, 2025
4b28cb3
Update DecoratedNavEntryProvider to latest
tunjid Jun 28, 2025
6bd74d2
Make implementations of PaneTransform internal
tunjid Jun 28, 2025
c97f37f
Update backStatusState in snapshot
tunjid Jun 28, 2025
456cd6c
Update three pane animations
tunjid Jun 28, 2025
56ecec8
Make parameter name explicit
tunjid Jun 28, 2025
06e5156
Establish generics order of NavigationState, Destination, Pane
tunjid Jun 28, 2025
cce35e2
Easier generic names
tunjid Jun 28, 2025
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
11 changes: 7 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
[versions]
androidGradlePlugin = "8.9.2"
androidxActivity = "1.9.2"
activity-compose = "1.11.0-rc01"
activity-compose = "1.12.0-alpha02"
androidxAppCompat = "1.7.0"
androidxBenchmark = "1.3.4"
androidxCore = "1.16.0"
androidxCollection = "1.5.0"
androidxCompose = "1.7.0"
androidxNavigationEvent = "1.0.0-alpha02"
androidxPaging = "3.3.2"
androidxSavedState = "1.3.0-alpha07"
androidxTestCore = "1.6.1"
androidxTestExt = "1.2.1"
androidxTestRunner = "1.6.2"
androidxTestRules = "1.6.1"
dokka = "1.8.20"
jetbrainsCompose = "1.8.0"
jetbrainsLifecycle = "2.9.0-beta01"
jetbrainsCompose = "1.8.2"
jetbrainsLifecycle = "2.9.1"
jetbrainsMaterial3Adaptive = "1.0.1"
junit4 = "4.13.2"
kotlin = "2.1.20"
kotlinxCoroutines = "1.10.2"
kotlinxDatetime = "0.6.2"
tunjidStateHolder = "1.1.0"
tunjidComposables = "0.0.16"
tunjidComposables = "0.0.19"
junit = "4.13.2"
runner = "1.0.2"
espressoCore = "3.0.2"
Expand All @@ -37,8 +38,10 @@ android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", ver
compose-compiler-plugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity-compose" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
androidx-collection = { group = "androidx.collection", name = "collection", version.ref = "androidxCollection" }
androidx-navigation-event = { group = "androidx.navigationevent", name = "navigationevent", version.ref = "androidxNavigationEvent" }

jetbrains-compose-animation = { group = "org.jetbrains.compose.animation", name = "animation", version.ref = "jetbrainsCompose" }
jetbrains-compose-foundation = { group = "org.jetbrains.compose.foundation", name = "foundation", version.ref = "jetbrainsCompose" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@

package com.tunjid.treenav.compose.threepane

import androidx.compose.animation.EnterExitState
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tunjid.treenav.Node
import com.tunjid.treenav.compose.Adaptation
import com.tunjid.treenav.compose.Adaptation.Swap
import com.tunjid.treenav.compose.MultiPaneDisplay
import com.tunjid.treenav.compose.PaneEntry
Expand All @@ -42,13 +42,6 @@ enum class ThreePane {
*/
Primary,

/**
* A optional pane for placing content from the [Primary] pane, if a preview of the previous
* navigation destinations is occurring. The primary content is rendered here, while
* the previous primary content is rendered in the [Primary] pane.
*/
TransientPrimary,

/**
* An optional pane for displaying a navigation destination alongside the [Primary] pane.
* This is useful for list-detail, or supporting panels flows.
Expand All @@ -66,77 +59,33 @@ enum class ThreePane {
* An optional pane for showing dialogs, or context sheets over existing panes.
*/
Overlay;

companion object {
val PrimaryToSecondary = Swap(
from = Primary,
to = Secondary
)

val SecondaryToPrimary = Swap(
from = Secondary,
to = Primary
)

val PrimaryToTransient = Swap(
from = Primary,
to = TransientPrimary
)
}
}

/**
* A [PaneEntry] for selectively running animations in [ThreePane] [MultiPaneDisplay]. When:
* - A navigation destination moves between the [ThreePane.Primary] and [ThreePane.Secondary]
* panes, the pane animations are not run to provide a seamless movement experience.
* - A navigation destination moves between the [ThreePane.Primary] and
* [ThreePane.TransientPrimary] panes, the pane animations are not run.
* Provides a default [PaneEntry] for selectively running animations in
* [ThreePane] [MultiPaneDisplay].
*
* @param enterTransition the transition to run for the entering pane when permitted.
* @param exitTransition the transition to run for the exiting pane when permitted.
* @param paneMapping the mapping of panes to navigation destinations.
* @param enterTransition the [EnterTransition] used when this [PaneEntry] adapts in the display.
* @param exitTransition the [ExitTransition] used when this [PaneEntry] adapts in the display.
* @param paneMapping the [Destination]s that are shown alongside the [Destination] provided and
* which of the [ThreePane]s they should show up in.
* @param render the Composable for rendering the current destination.
*/
fun <R : Node> threePaneEntry(
enterTransition: PaneScope<ThreePane, R>.() -> EnterTransition = { DefaultFadeIn },
exitTransition: PaneScope<ThreePane, R>.() -> ExitTransition = { DefaultFadeOut },
paneMapping: @Composable (R) -> Map<ThreePane, R?> = {
mapOf(ThreePane.Primary to it)
fun <Destination : Node> threePaneEntry(
enterTransition: PaneScope<ThreePane, Destination>.() -> EnterTransition = {
if (canAnimate()) DefaultFadeIn else EnterTransition.None
},
render: @Composable PaneScope<ThreePane, R>.(R) -> Unit,
) = PaneEntry(
paneTransform = paneMapping,
renderTransform = { destination, original ->
val state = paneState
val shouldAnimate = when (state.pane) {
ThreePane.Primary,
ThreePane.Secondary,
-> when {
ThreePane.PrimaryToSecondary in state.adaptations -> false
ThreePane.SecondaryToPrimary in state.adaptations -> false
else -> true
}

ThreePane.TransientPrimary -> when {
ThreePane.PrimaryToTransient in state.adaptations -> false
else -> true
}

else -> true
}
Box(
modifier =
if (shouldAnimate) Modifier.animateEnterExit(
enter = enterTransition(),
exit = exitTransition()
)
else Modifier,
content = {
original(destination)
}
)

exitTransition: PaneScope<ThreePane, Destination>.() -> ExitTransition = {
if (canAnimate()) DefaultFadeOut else ExitTransition.None
},
paneMapping: @Composable (Destination) -> Map<ThreePane, Destination?> = { destination ->
mapOf(ThreePane.Primary to destination)
},
render: @Composable (PaneScope<ThreePane, Destination>.(Destination) -> Unit),
) = PaneEntry(
enterTransition = enterTransition,
exitTransition = exitTransition,
paneMapping = paneMapping,
content = render
)

Expand All @@ -151,3 +100,29 @@ private val DefaultFadeIn = fadeIn(
private val DefaultFadeOut = fadeOut(
animationSpec = RouteTransitionAnimationSpec,
)

private fun PaneScope<ThreePane, *>.canAnimate() =
when {
transition.targetState == EnterExitState.PostExit -> true
inPredictiveBack && isActive -> true
paneState.adaptations.any { adaptation ->
adaptation is Adaptation.Same
} -> false

paneState.adaptations.any { adaptation ->
adaptation is Adaptation.Pop || adaptation is Adaptation.Change
} && paneState.adaptations.none {
it is Swap<*>
} -> true

else -> when (val pane = paneState.pane) {
ThreePane.Primary,
ThreePane.Secondary,
ThreePane.Tertiary -> paneState.adaptations.any { adaptation ->
adaptation is Swap<*> && adaptation.from == pane
}

ThreePane.Overlay,
null -> true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,64 +65,32 @@ private class ThreePaneSharedTransitionScope<Destination : Node> @OptIn(

@OptIn(ExperimentalSharedTransitionApi::class)
override fun Modifier.paneSharedElement(
key: Any,
sharedContentState: SharedTransitionScope.SharedContentState,
boundsTransform: BoundsTransform,
placeHolderSize: PlaceHolderSize,
renderInOverlayDuringTransition: Boolean,
visible: Boolean?,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: OverlayClip,
): Modifier = composed {

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 {
paneScope.isPreviewingBack -> sharedElementWithCallerManagedVisibility(
sharedContentState = rememberSharedContentState(key),
visible = false,
boundsTransform = boundsTransform,
placeHolderSize = placeHolderSize,
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
zIndexInOverlay = zIndexInOverlay,
clipInOverlayDuringTransition = clipInOverlayDuringTransition,
)
// Share the element
else -> sharedElementWithCallerManagedVisibility(
sharedContentState = rememberSharedContentState(key),
visible = when (visible) {
null -> paneScope.isActive
else -> paneScope.isActive && visible
},
boundsTransform = boundsTransform,
placeHolderSize = placeHolderSize,
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
zIndexInOverlay = zIndexInOverlay,
clipInOverlayDuringTransition = clipInOverlayDuringTransition,
)
}
// Share the element when in the transient pane
ThreePane.TransientPrimary -> sharedElementWithCallerManagedVisibility(
sharedContentState = rememberSharedContentState(key),
visible = paneScope.isActive,
boundsTransform = boundsTransform,
placeHolderSize = placeHolderSize,
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
zIndexInOverlay = zIndexInOverlay,
clipInOverlayDuringTransition = clipInOverlayDuringTransition,
)
): Modifier = 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 -> sharedElement(
sharedContentState = sharedContentState,
animatedVisibilityScope = paneScope,
boundsTransform = boundsTransform,
placeHolderSize = placeHolderSize,
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
zIndexInOverlay = zIndexInOverlay,
clipInOverlayDuringTransition = clipInOverlayDuringTransition,
)

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

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

This file was deleted.

Loading