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

Tj/scaffold navigation #34

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 6 commits into from
May 21, 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
8 changes: 4 additions & 4 deletions libraryVersion.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.
#
groupId=com.tunjid.treenav
treenav_version=0.0.25
strings_version=0.0.25
compose_version=0.0.25
compose-threepane_version=0.0.25
treenav_version=0.0.26
strings_version=0.0.26
compose_version=0.0.26
compose-threepane_version=0.0.26
1 change: 1 addition & 0 deletions sample/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentWithReceiverOf
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
Expand All @@ -67,28 +67,28 @@ import com.tunjid.composables.backpreview.backPreview
import com.tunjid.composables.splitlayout.SplitLayout
import com.tunjid.composables.splitlayout.SplitLayoutState
import com.tunjid.demo.common.ui.AppState.Companion.rememberMultiPaneDisplayState
import com.tunjid.demo.common.ui.avatar.avatarPaneEntry
import com.tunjid.demo.common.ui.chat.chatPaneEntry
import com.tunjid.demo.common.ui.chatrooms.chatRoomPaneEntry
import com.tunjid.demo.common.ui.data.NavigationRepository
import com.tunjid.demo.common.ui.data.SampleDestination
import com.tunjid.demo.common.ui.me.mePaneEntry
import com.tunjid.demo.common.ui.avatar.avatarPaneEntry
import com.tunjid.demo.common.ui.profile.profilePaneEntry
import com.tunjid.treenav.MultiStackNav
import com.tunjid.treenav.compose.MultiPaneDisplay
import com.tunjid.treenav.compose.MultiPaneDisplayScope
import com.tunjid.treenav.compose.MultiPaneDisplayState
import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState
import com.tunjid.treenav.compose.threepane.ThreePane
import com.tunjid.treenav.compose.multiPaneDisplayBackstack
import com.tunjid.treenav.compose.threepane.ThreePane
import com.tunjid.treenav.compose.threepane.transforms.backPreviewTransform
import com.tunjid.treenav.compose.threepane.transforms.threePanedAdaptiveTransform
import com.tunjid.treenav.compose.threepane.transforms.threePanedMovableSharedElementTransform
import com.tunjid.treenav.compose.transforms.Transform
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.requireCurrent
import com.tunjid.treenav.switch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -109,7 +109,6 @@ fun App(
sharedTransitionScope = this
)
}
appState.movableSharedElementHostState = movableSharedElementHostState
MultiPaneDisplay(
modifier = Modifier
.fillMaxSize(),
Expand Down Expand Up @@ -241,8 +240,6 @@ 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 @@ -278,12 +275,12 @@ class AppState(
)

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
package com.tunjid.demo.common.ui

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.snap
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
Expand All @@ -33,189 +34,105 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.tunjid.demo.common.ui.data.SampleDestination
import com.tunjid.treenav.compose.moveablesharedelement.rememberPaneMovableSharedElementScope
import com.tunjid.treenav.compose.rememberPaneMovableElementSharedTransitionScope
import com.tunjid.treenav.compose.threepane.ThreePane
import com.tunjid.treenav.compose.threepane.ThreePaneMovableElementSharedTransitionScope
import com.tunjid.treenav.current

@Composable
fun PaneScaffoldState.PaneNavigationBar(
modifier: Modifier = Modifier,
enterTransition: EnterTransition = slideInVertically(initialOffsetY = { it }),
exitTransition: ExitTransition = slideOutVertically(targetOffsetY = { it }),
) = withUpdatedPaneScaffoldNavigationState(
enterTransition = enterTransition,
exitTransition = exitTransition,
canShow = canShowBottomNavigation,
content = {
val finalModifier = modifier
.navigationSharedElement(
sharedContentState = rememberSharedContentState(BottomNavSharedElementKey),
)

if (canUseMovableContent) LocalAppState.current.movableNavigationBar(
this,
finalModifier
)
else PaneNavigationBar(finalModifier)
}
)

@Composable
fun PaneScaffoldState.PaneNavigationRail(
modifier: Modifier = Modifier,
enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }),
exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }),
) = withUpdatedPaneScaffoldNavigationState(
enterTransition = enterTransition,
exitTransition = exitTransition,
canShow = canShowNavRail,
content = {
val finalModifier = modifier
.navigationSharedElement(
sharedContentState = rememberSharedContentState(NavRailSharedElementKey),
)

if (canUseMovableContent) LocalAppState.current.movableNavigationRail(
this,
finalModifier
)
else PaneNavigationRail(finalModifier)
}
)

@Composable
internal fun NavigationBarState.PaneNavigationBar(
modifier: Modifier = Modifier,
) {
val appState = LocalAppState.current
AnimatedVisibility(
modifier = modifier,
visible = canShow,
modifier = modifier
.sharedElement(
sharedContentState = rememberSharedContentState(NavigationBarSharedElementKey),
animatedVisibilityScope = this,
zIndexInOverlay = NavigationSharedElementZIndex,
),
visible = canShowNavigationBar,
enter = enterTransition,
exit = exitTransition,
content = {
NavigationBar {
SampleDestination.NavTabs.entries.forEach { item ->
NavigationBarItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.title,
)
},
selected = item == appState.currentNavigation.current,
onClick = { appState.setTab(item) }
)
}
}
},
val appState = LocalAppState.current
if (canUseMovableNavigationBar) appState.movableNavigationBar(Modifier)
else appState.PaneNavigationBar(Modifier)
}
)
}

@Composable
internal fun NavigationBarState.PaneNavigationRail(
fun PaneScaffoldState.PaneNavigationRail(
modifier: Modifier = Modifier,
enterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { -it }),
exitTransition: ExitTransition = slideOutHorizontally(targetOffsetX = { -it }),
) {
val appState = LocalAppState.current
AnimatedVisibility(
modifier = modifier,
visible = canShow,
enter = enterTransition,
exit = exitTransition,
content = {
NavigationRail {
SampleDestination.NavTabs.entries.forEach { item ->
NavigationRailItem(
selected = item == appState.currentNavigation.current,
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.title,
)
},
onClick = { appState.setTab(item) }
)
}
}
}
)
modifier = modifier
.sharedElementWithCallerManagedVisibility(
sharedContentState = rememberSharedContentState(NavigationRailSharedElementKey),
visible = canShowNavigationRail,
zIndexInOverlay = NavigationSharedElementZIndex,
boundsTransform = NavigationRailBoundsTransform,
),
visible = canShowNavigationRail,
enter = if (canShowNavigationRail) enterTransition else EnterTransition.None,
exit = if (canShowNavigationRail) exitTransition else ExitTransition.None,
) {
val appState = LocalAppState.current
if (canUseMovableNavigationRail) appState.movableNavigationRail(Modifier)
else appState.PaneNavigationRail(Modifier)
}
}

@Composable
private fun PaneScaffoldState.withUpdatedPaneScaffoldNavigationState(
enterTransition: EnterTransition,
exitTransition: ExitTransition,
canShow: Boolean,
content: @Composable NavigationBarState.() -> Unit
internal fun AppState.PaneNavigationBar(
modifier: Modifier = Modifier
) {
val appState = LocalAppState.current

val paneMovableElementSharedTransitionScope =
rememberPaneMovableElementSharedTransitionScope(
paneSharedTransitionScope = this,
movableSharedElementScope = rememberPaneMovableSharedElementScope(
movableSharedElementHostState = appState.movableSharedElementHostState
),
)

val state = remember {
NavigationBarState(
delegate = paneMovableElementSharedTransitionScope,
enterTransition = enterTransition,
exitTransition = exitTransition,
canShow = canShow,
)
}.also {
it.enterTransition = enterTransition
it.exitTransition = exitTransition
it.canShow = canShow
NavigationBar(
modifier = modifier
) {
SampleDestination.NavTabs.entries.forEach { item ->
NavigationBarItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.title,
)
},
selected = item == currentNavigation.current,
onClick = { setTab(item) }
)
}
}

state.content()
}

@Stable
internal class NavigationBarState(
private val delegate: ThreePaneMovableElementSharedTransitionScope<SampleDestination>,
enterTransition: EnterTransition,
exitTransition: ExitTransition,
canShow: Boolean,
) : ThreePaneMovableElementSharedTransitionScope<SampleDestination> by delegate {
var enterTransition by mutableStateOf(enterTransition)
var exitTransition by mutableStateOf(exitTransition)
var canShow by mutableStateOf(canShow)

val canUseMovableContent
get() = when {
isActive && isPreviewingBack && paneState.pane == ThreePane.TransientPrimary -> true
isActive && !isPreviewingBack && paneState.pane == ThreePane.Primary -> true
else -> false
@Composable
internal fun AppState.PaneNavigationRail(
modifier: Modifier = Modifier
) {
NavigationRail(
modifier = modifier
) {
SampleDestination.NavTabs.entries.forEach { item ->
NavigationRailItem(
selected = item == currentNavigation.current,
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.title,
)
},
onClick = { setTab(item) }
)
}

private val isPreviewingBack: Boolean
get() = paneState.adaptations.contains(ThreePane.PrimaryToTransient)


@OptIn(ExperimentalSharedTransitionApi::class)
fun Modifier.navigationSharedElement(
sharedContentState: SharedTransitionScope.SharedContentState,
) = sharedElement(
sharedContentState = sharedContentState,
animatedVisibilityScope = delegate,
zIndexInOverlay = NavigationSharedElementZIndex,
)
}
}

private data object BottomNavSharedElementKey
private data object NavRailSharedElementKey
private data object NavigationBarSharedElementKey
private data object NavigationRailSharedElementKey

private const val NavigationSharedElementZIndex = 2f

private val NavigationRailBoundsTransform = BoundsTransform { _, _ -> snap() }
Loading