feat(ui): full adaptive layout prototype
This commit is contained in:
parent
fe46a32eba
commit
7535d8342e
9 changed files with 415 additions and 21 deletions
|
|
@ -3,6 +3,7 @@ package moe.lava.neon.ui
|
|||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MotionScheme
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
|
|
@ -21,12 +22,16 @@ import kotlinx.serialization.modules.polymorphic
|
|||
import moe.lava.neon.ui.di.AppUiGraph
|
||||
import moe.lava.neon.ui.screens.Login
|
||||
import moe.lava.neon.ui.screens.Sample
|
||||
import moe.lava.neon.ui.screens.chat.Chat
|
||||
import moe.lava.neon.ui.screens.members.MembersList
|
||||
import moe.lava.neon.ui.screens.navigator.Navigator
|
||||
import moe.lava.neon.ui.screens.navigator.NavigatorModel
|
||||
import moe.lava.neon.ui.screens.navigator.NavigatorPreviewProvider
|
||||
import moe.lava.neon.ui.util.ThreePaneSceneStrategy
|
||||
import moe.lava.neon.ui.util.rememberThreePaneSceneStrategy
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private object Route {
|
||||
object Route {
|
||||
@Serializable
|
||||
data object Login : NavKey
|
||||
|
||||
|
|
@ -35,6 +40,12 @@ private object Route {
|
|||
|
||||
@Serializable
|
||||
data class Navigator(val left: Boolean) : NavKey
|
||||
|
||||
@Serializable
|
||||
data object Chat : NavKey
|
||||
|
||||
@Serializable
|
||||
data object MembersList : NavKey
|
||||
}
|
||||
|
||||
private val config = SavedStateConfiguration {
|
||||
|
|
@ -43,11 +54,13 @@ private val config = SavedStateConfiguration {
|
|||
subclass(Route.Login::class, Route.Login.serializer())
|
||||
subclass(Route.Sample::class, Route.Sample.serializer())
|
||||
subclass(Route.Navigator::class, Route.Navigator.serializer())
|
||||
subclass(Route.Chat::class, Route.Chat.serializer())
|
||||
subclass(Route.MembersList::class, Route.MembersList.serializer())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun App() {
|
||||
Thread.setDefaultUncaughtExceptionHandler { t: Thread, e: Throwable ->
|
||||
|
|
@ -64,7 +77,8 @@ fun App() {
|
|||
motionScheme = MotionScheme.expressive(),
|
||||
) {
|
||||
val init = if (graph.auth.token != null) Route.Sample else Route.Login
|
||||
val backStack = rememberNavBackStack(config, init)
|
||||
val backStack = rememberNavBackStack(config, Route.Sample)
|
||||
val threePaneStrategy = rememberThreePaneSceneStrategy<NavKey>()
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryDecorators = listOf(
|
||||
|
|
@ -72,6 +86,7 @@ fun App() {
|
|||
rememberViewModelStoreNavEntryDecorator(),
|
||||
),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
sceneStrategy = threePaneStrategy,
|
||||
entryProvider = entryProvider {
|
||||
entry<Route.Login> {
|
||||
Login(
|
||||
|
|
@ -85,6 +100,8 @@ fun App() {
|
|||
Sample(
|
||||
navTest = {
|
||||
backStack.add(Route.Navigator(it))
|
||||
backStack.add(Route.Chat)
|
||||
backStack.add(Route.MembersList)
|
||||
},
|
||||
onRequestLogout = {
|
||||
backStack.clear()
|
||||
|
|
@ -92,7 +109,10 @@ fun App() {
|
|||
}
|
||||
)
|
||||
}
|
||||
entry<Route.Navigator> { key ->
|
||||
|
||||
entry<Route.Navigator>(
|
||||
metadata = ThreePaneSceneStrategy.listPane()
|
||||
) { key ->
|
||||
if (key.left) {
|
||||
Navigator(
|
||||
NavigatorPreviewProvider.base2.copy(
|
||||
|
|
@ -107,6 +127,20 @@ fun App() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
entry<Route.Chat>(
|
||||
metadata = ThreePaneSceneStrategy.detailPane()
|
||||
) {
|
||||
Chat(
|
||||
onOpenMembers = { backStack.add(Route.MembersList) }
|
||||
)
|
||||
}
|
||||
|
||||
entry<Route.MembersList>(
|
||||
metadata = ThreePaneSceneStrategy.extraPane()
|
||||
) {
|
||||
MembersList()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
78
ui/src/commonMain/kotlin/moe/lava/neon/ui/Previews.kt
Normal file
78
ui/src/commonMain/kotlin/moe/lava/neon/ui/Previews.kt
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package moe.lava.neon.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.scene.SceneStrategyScope
|
||||
import moe.lava.neon.ui.screens.chat.Chat
|
||||
import moe.lava.neon.ui.screens.members.MembersList
|
||||
import moe.lava.neon.ui.screens.navigator.Navigator
|
||||
import moe.lava.neon.ui.screens.navigator.NavigatorModel
|
||||
import moe.lava.neon.ui.screens.navigator.NavigatorPreviewProvider
|
||||
import moe.lava.neon.ui.util.ThemedPreview
|
||||
import moe.lava.neon.ui.util.ThreePaneSceneStrategy
|
||||
import moe.lava.neon.ui.util.rememberThreePaneSceneStrategy
|
||||
|
||||
@PreviewScreenSizes
|
||||
@Preview(
|
||||
name = "Phone - Navigator",
|
||||
device = "spec:width=411dp,height=892dp,orientation=portrait,dpi=420",
|
||||
showSystemUi = true,
|
||||
)
|
||||
@Composable
|
||||
fun AppPrewiew() {
|
||||
val chat: NavEntry<NavKey> = NavEntry(
|
||||
Route.Chat,
|
||||
metadata = ThreePaneSceneStrategy.detailPane(),
|
||||
) {
|
||||
Chat(onOpenMembers = {})
|
||||
}
|
||||
val nav: NavEntry<NavKey> = NavEntry(
|
||||
Route.Navigator(true),
|
||||
metadata = ThreePaneSceneStrategy.listPane(),
|
||||
) {
|
||||
Navigator(
|
||||
NavigatorPreviewProvider.base2.copy(
|
||||
guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar
|
||||
)
|
||||
)
|
||||
}
|
||||
val nav2: NavEntry<NavKey> = NavEntry(
|
||||
Route.Navigator(true),
|
||||
metadata = ThreePaneSceneStrategy.listPane(),
|
||||
) {
|
||||
Navigator(
|
||||
NavigatorPreviewProvider.base2.copy(
|
||||
guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet
|
||||
)
|
||||
)
|
||||
}
|
||||
val mem: NavEntry<NavKey> = NavEntry(
|
||||
Route.MembersList,
|
||||
metadata = ThreePaneSceneStrategy.extraPane(),
|
||||
) {
|
||||
MembersList()
|
||||
}
|
||||
val strat = rememberThreePaneSceneStrategy<NavKey>()
|
||||
ThemedPreview {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
SceneStrategyScope<NavKey>().run {
|
||||
strat.run {
|
||||
calculateScene(listOf(chat, nav, mem))?.content()
|
||||
?: if (LocalWindowInfo.current.containerDpSize.height == 892.dp) {
|
||||
nav2.Content()
|
||||
} else {
|
||||
chat.Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package moe.lava.neon.ui.screens.chat
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun Chat(
|
||||
onOpenMembers: () -> Unit,
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Button(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
onClick = { onOpenMembers() }
|
||||
) {
|
||||
Text("Hi! Click to open members")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package moe.lava.neon.ui.screens.members
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedListItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.neon.ui.util.ThemedPreview
|
||||
|
||||
data class MembersListModel(
|
||||
val members: List<Member>,
|
||||
) {
|
||||
data class Member(
|
||||
val avatar: String,
|
||||
val name: String,
|
||||
val status: String?,
|
||||
)
|
||||
}
|
||||
|
||||
val sampleModel = MembersListModel(
|
||||
listOf(
|
||||
MembersListModel.Member("", "Mem1", "Something?"),
|
||||
MembersListModel.Member("", "Mem2", null),
|
||||
MembersListModel.Member("", "Mem4", "Bleh"),
|
||||
MembersListModel.Member("", "Mem8", null),
|
||||
MembersListModel.Member("", "Mem7", null),
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun MembersList(
|
||||
model: MembersListModel = sampleModel,
|
||||
) {
|
||||
Scaffold(
|
||||
|
||||
) { contentPadding ->
|
||||
Box(
|
||||
Modifier.fillMaxSize()
|
||||
.padding(contentPadding.plus(PaddingValues(16.dp)))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)
|
||||
) {
|
||||
model.members.forEachIndexed { idx, member ->
|
||||
MemberEntry(member, idx, model.members.size)
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
model.members.forEachIndexed { idx, member ->
|
||||
MemberEntry(member, idx, model.members.size)
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
model.members.forEachIndexed { idx, member ->
|
||||
MemberEntry(member, idx, model.members.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun MemberEntry(
|
||||
model: MembersListModel.Member,
|
||||
index: Int,
|
||||
size: Int,
|
||||
) {
|
||||
val colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
colors = colors,
|
||||
leadingContent = { },
|
||||
supportingContent = {
|
||||
model.status?.let { Text(it) }
|
||||
},
|
||||
shapes = ListItemDefaults.segmentedShapes(index, size)
|
||||
) {
|
||||
Text(
|
||||
text = model.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MembersListPreview() {
|
||||
ThemedPreview {
|
||||
MembersList()
|
||||
}
|
||||
}
|
||||
|
|
@ -42,8 +42,13 @@ fun Channels(
|
|||
model: NavigatorModel,
|
||||
bottomSpace: Dp,
|
||||
padding: PaddingValues,
|
||||
onNavigate: (NavigatorModel.Channel) -> Unit,
|
||||
) {
|
||||
val colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||
val colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -87,7 +92,8 @@ fun Channels(
|
|||
Column(verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)) {
|
||||
cat.channels.forEachIndexed { idx, ch ->
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
selected = model.selected.second == ch,
|
||||
onClick = { onNavigate(ch) },
|
||||
colors = colors,
|
||||
leadingContent = {
|
||||
val res = when (ch.type) {
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ fun GuildButton(
|
|||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val target = when {
|
||||
isHovered -> 0.8f
|
||||
isPressed -> 0.85f
|
||||
selected -> 1f
|
||||
isHovered -> 0.8f
|
||||
else -> 0f
|
||||
}
|
||||
val fadingIn = isPressed && !selected || !isPressed && selected
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState
|
|||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -47,6 +47,7 @@ data class NavigatorModel(
|
|||
val guildName: String,
|
||||
val channels: List<Category>,
|
||||
val guilds: List<Guild>,
|
||||
val selected: Pair<Guild, Channel?>,
|
||||
) {
|
||||
data class Category(
|
||||
val name: String,
|
||||
|
|
@ -70,21 +71,38 @@ data class NavigatorModel(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Move channel's bottomSpace and padding into a Modifier, so this can be greatly simplified
|
||||
// by sharing the Channels and GuildButton implementations in both layouts
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun Navigator(
|
||||
model: NavigatorModel
|
||||
) {
|
||||
// TODO: this obviously should be done properly
|
||||
var selected by remember { mutableStateOf(model.selected) }
|
||||
val onNavigateChannel: (NavigatorModel.Channel) -> Unit = { ch -> selected = selected.copy(second = ch) }
|
||||
val onNavigateGuild: (NavigatorModel.Guild) -> Unit = { guild -> selected = selected.copy(first = guild) }
|
||||
|
||||
when (model.guildNavPosition) {
|
||||
NavigatorModel.GuildNavPosition.BottomSheet -> BottomSheetNavigator(model)
|
||||
NavigatorModel.GuildNavPosition.LeftSidebar -> SidebarNavigator(model)
|
||||
NavigatorModel.GuildNavPosition.BottomSheet -> BottomSheetNavigator(
|
||||
model.copy(selected = selected),
|
||||
onNavigateGuild = onNavigateGuild,
|
||||
onNavigateChannel = onNavigateChannel,
|
||||
)
|
||||
NavigatorModel.GuildNavPosition.LeftSidebar -> SidebarNavigator(
|
||||
model.copy(selected = selected),
|
||||
onNavigateGuild = onNavigateGuild,
|
||||
onNavigateChannel = onNavigateChannel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun BottomSheetNavigator(
|
||||
model: NavigatorModel
|
||||
model: NavigatorModel,
|
||||
onNavigateGuild: (NavigatorModel.Guild) -> Unit,
|
||||
onNavigateChannel: (NavigatorModel.Channel) -> Unit,
|
||||
) {
|
||||
val morph = remember {
|
||||
Morph(MaterialShapes.Diamond, MaterialShapes.Cookie9Sided)
|
||||
|
|
@ -97,7 +115,6 @@ private fun BottomSheetNavigator(
|
|||
val offsetDp = with (LocalDensity.current) { offset.toDp() } - TopAppBarDefaults.TopAppBarExpandedHeight
|
||||
BottomSheetScaffold(
|
||||
sheetContent = {
|
||||
var selected by remember { mutableIntStateOf(-1) }
|
||||
LazyRow(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -110,8 +127,8 @@ private fun BottomSheetNavigator(
|
|||
GuildButton(
|
||||
model = guild,
|
||||
morph = morph,
|
||||
selected = selected == idx,
|
||||
onSelected = { selected = idx }
|
||||
selected = model.selected.first == guild,
|
||||
onSelected = { onNavigateGuild(guild) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,20 +142,26 @@ private fun BottomSheetNavigator(
|
|||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Channels(model, offsetDp, innerPadding)
|
||||
Channels(
|
||||
model = model,
|
||||
bottomSpace = offsetDp,
|
||||
padding = innerPadding,
|
||||
onNavigate = { onNavigateChannel(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SidebarNavigator(
|
||||
model: NavigatorModel
|
||||
model: NavigatorModel,
|
||||
onNavigateGuild: (NavigatorModel.Guild) -> Unit,
|
||||
onNavigateChannel: (NavigatorModel.Channel) -> Unit,
|
||||
) {
|
||||
val morph = remember {
|
||||
Morph(MaterialShapes.Diamond, MaterialShapes.Cookie9Sided)
|
||||
}
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
var selected by remember { mutableIntStateOf(-1) }
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
|
|
@ -153,8 +176,8 @@ private fun SidebarNavigator(
|
|||
GuildButton(
|
||||
model = guild,
|
||||
morph = morph,
|
||||
selected = selected == idx,
|
||||
onSelected = { selected = idx }
|
||||
selected = model.selected.first == guild,
|
||||
onSelected = { onNavigateGuild(guild) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +188,7 @@ private fun SidebarNavigator(
|
|||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Channels(model, 0.dp, innerPadding)
|
||||
Channels(model, 0.dp, innerPadding, onNavigate = { onNavigateChannel(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ internal class NavigatorPreviewProvider : PreviewParameterProvider<NavigatorMode
|
|||
NavigatorModel.Guild(name = "Hutao", url = ""),
|
||||
NavigatorModel.Guild(name = "huuutao", url = ""),
|
||||
),
|
||||
selected = NavigatorModel.Guild(name = "Hu Tao", url = "") to null,
|
||||
channels = listOf(
|
||||
NavigatorModel.Category(
|
||||
name = "Text Channels",
|
||||
|
|
@ -161,7 +162,8 @@ internal class NavigatorPreviewProvider : PreviewParameterProvider<NavigatorMode
|
|||
NavigatorModel.Guild(name = "huuutao", url = Res.getUri("drawable/placeholder_9.jpg")),
|
||||
NavigatorModel.Guild(name = "huuutao", url = Res.getUri("drawable/placeholder_10.jpg")),
|
||||
NavigatorModel.Guild(name = "huuutao", url = Res.getUri("drawable/placeholder_11.jpg")),
|
||||
)
|
||||
),
|
||||
selected = NavigatorModel.Guild(name = "Hu Tao", url = Res.getUri("drawable/placeholder_1.webp")) to null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
120
ui/src/commonMain/kotlin/moe/lava/neon/ui/util/ThreePaneScene.kt
Normal file
120
ui/src/commonMain/kotlin/moe/lava/neon/ui/util/ThreePaneScene.kt
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package moe.lava.neon.ui.util
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.scene.Scene
|
||||
import androidx.navigation3.scene.SceneStrategy
|
||||
import androidx.navigation3.scene.SceneStrategyScope
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
|
||||
private val listWidth = 350.dp
|
||||
private val extraWidth = 300.dp
|
||||
//private val minChatWidth = 250.dp
|
||||
|
||||
class ThreePaneScene<T : Any>(
|
||||
override val key: Any,
|
||||
override val previousEntries: List<NavEntry<T>>,
|
||||
val listEntry: NavEntry<T>?,
|
||||
val detailEntry: NavEntry<T>,
|
||||
val extraEntry: NavEntry<T>?,
|
||||
) : Scene<T> {
|
||||
override val entries: List<NavEntry<T>> = listOfNotNull(listEntry, detailEntry, extraEntry)
|
||||
override val content: @Composable (() -> Unit) = {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
if (listEntry != null) {
|
||||
Column(modifier = Modifier.width(listWidth)) {
|
||||
listEntry.Content()
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
AnimatedContent(
|
||||
targetState = detailEntry,
|
||||
contentKey = { entry -> entry.contentKey },
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it }
|
||||
) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { -it })
|
||||
}
|
||||
){ entry ->
|
||||
entry.Content()
|
||||
}
|
||||
}
|
||||
if (extraEntry != null) {
|
||||
Column(modifier = Modifier.width(extraWidth)) {
|
||||
AnimatedContent(
|
||||
targetState = extraEntry,
|
||||
contentKey = { entry -> entry.contentKey },
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it }
|
||||
) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { -it })
|
||||
}
|
||||
){ entry ->
|
||||
entry.Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val LIST_KEY = "ThreePaneScene-List"
|
||||
private const val DETAIL_KEY = "ThreePaneScene-Detail"
|
||||
private const val EXTRA_KEY = "ThreePaneScene-Extra"
|
||||
|
||||
class ThreePaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> {
|
||||
override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? {
|
||||
val useTwoPanes = when {
|
||||
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_LARGE_LOWER_BOUND)
|
||||
-> false
|
||||
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
|
||||
-> true
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: return null
|
||||
val detailEntry = entries.findLast { it.metadata.containsKey(DETAIL_KEY) } ?: return null
|
||||
val extraEntry = entries.findLast { it.metadata.containsKey(EXTRA_KEY) }
|
||||
|
||||
val sceneKey = listEntry.contentKey
|
||||
|
||||
return ThreePaneScene(
|
||||
key = sceneKey,
|
||||
previousEntries = entries.dropLast(1),
|
||||
listEntry = listEntry.takeIf { !(useTwoPanes && extraEntry != null) },
|
||||
// listEntry = listEntry,
|
||||
detailEntry = detailEntry,
|
||||
extraEntry = extraEntry,
|
||||
// extraEntry = extraEntry.takeIf { !useTwoPanes },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun listPane() = mapOf(LIST_KEY to true)
|
||||
fun detailPane() = mapOf(DETAIL_KEY to true)
|
||||
fun extraPane() = mapOf(EXTRA_KEY to true)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Any> rememberThreePaneSceneStrategy(): ThreePaneSceneStrategy<T> {
|
||||
val windowSizeClass = currentWindowAdaptiveInfo(true).windowSizeClass
|
||||
|
||||
return remember(windowSizeClass) {
|
||||
ThreePaneSceneStrategy(windowSizeClass)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue