Change-Id: I68ff2584ec0c5be2209c21d1bec9b71acad6f42dfeature/adaptive_scaffold
parent
2a6fb1ea60
commit
0cd61134e1
@ -1,220 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.samples.apps.nowinandroid.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.NavigationBar
|
|
||||||
import androidx.compose.material3.NavigationBarDefaults
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
|
||||||
import androidx.compose.material3.NavigationDrawerItem
|
|
||||||
import androidx.compose.material3.NavigationDrawerItemDefaults
|
|
||||||
import androidx.compose.material3.NavigationRail
|
|
||||||
import androidx.compose.material3.NavigationRailDefaults
|
|
||||||
import androidx.compose.material3.NavigationRailItem
|
|
||||||
import androidx.compose.material3.NavigationRailItemDefaults
|
|
||||||
import androidx.compose.material3.PermanentDrawerSheet
|
|
||||||
import androidx.compose.material3.PermanentNavigationDrawer
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.ScaffoldDefaults
|
|
||||||
import androidx.compose.material3.contentColorFor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.movableContentOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.window.layout.WindowMetricsCalculator
|
|
||||||
|
|
||||||
class AdaptiveScaffoldNavigationComponentColors internal constructor(
|
|
||||||
val railContainerColor: Color,
|
|
||||||
val drawerContainerColor: Color,
|
|
||||||
val bottomBarContainerColor: Color,
|
|
||||||
val contentContainerColor: Color,
|
|
||||||
val contentColor: Color,
|
|
||||||
val selectedIconColor: Color,
|
|
||||||
val selectedTextColor: Color,
|
|
||||||
val unselectedIconColor: Color,
|
|
||||||
val unselectedTextColor: Color,
|
|
||||||
val selectedContainerColor: Color,
|
|
||||||
val unselectedContainerColor: Color,
|
|
||||||
)
|
|
||||||
|
|
||||||
object AdaptiveScaffoldNavigationComponentDefaults {
|
|
||||||
@Composable
|
|
||||||
fun colors(
|
|
||||||
railContainerColor: Color = NavigationRailDefaults.ContainerColor,
|
|
||||||
drawerContainerColor: Color = MaterialTheme.colorScheme.surface,
|
|
||||||
bottomBarContainerColor: Color = NavigationBarDefaults.containerColor,
|
|
||||||
contentContainerColor: Color = MaterialTheme.colorScheme.background,
|
|
||||||
contentColor: Color = contentColorFor(contentContainerColor),
|
|
||||||
selectedIconColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
selectedTextColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
unselectedIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
unselectedTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
selectedContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
unselectedContainerColor: Color = MaterialTheme.colorScheme.surface,
|
|
||||||
) = AdaptiveScaffoldNavigationComponentColors(
|
|
||||||
railContainerColor = railContainerColor,
|
|
||||||
drawerContainerColor = drawerContainerColor,
|
|
||||||
bottomBarContainerColor = bottomBarContainerColor,
|
|
||||||
contentContainerColor = contentContainerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
selectedIconColor = selectedIconColor,
|
|
||||||
selectedTextColor = selectedTextColor,
|
|
||||||
unselectedIconColor = unselectedIconColor,
|
|
||||||
unselectedTextColor = unselectedTextColor,
|
|
||||||
selectedContainerColor = selectedContainerColor,
|
|
||||||
unselectedContainerColor = unselectedContainerColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun <T> AdaptiveScaffold(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
navigationItems: List<T>,
|
|
||||||
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
|
||||||
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
|
||||||
isItemSelected: @Composable (item: T) -> Boolean,
|
|
||||||
onNavigationItemClick: (item: T) -> Unit,
|
|
||||||
topBar: @Composable () -> Unit = {},
|
|
||||||
snackbarHost: @Composable () -> Unit = {},
|
|
||||||
colors: AdaptiveScaffoldNavigationComponentColors = AdaptiveScaffoldNavigationComponentDefaults.colors(),
|
|
||||||
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
|
||||||
content: @Composable (padding: PaddingValues) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
|
|
||||||
val widthDp = metrics.bounds.width() / context.resources.displayMetrics.density
|
|
||||||
val movableContent = remember(content) {
|
|
||||||
movableContentOf(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
widthDp >= 1240f -> {
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = topBar,
|
|
||||||
snackbarHost = snackbarHost,
|
|
||||||
containerColor = colors.contentContainerColor,
|
|
||||||
contentColor = colors.contentColor,
|
|
||||||
contentWindowInsets = contentWindowInsets,
|
|
||||||
) { padding ->
|
|
||||||
PermanentNavigationDrawer(
|
|
||||||
drawerContent = {
|
|
||||||
PermanentDrawerSheet(
|
|
||||||
drawerContainerColor = colors.drawerContainerColor,
|
|
||||||
) {
|
|
||||||
navigationItems.forEach { item ->
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { navigationItemTitle(item, isItemSelected(item)) },
|
|
||||||
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
|
||||||
selected = isItemSelected(item),
|
|
||||||
onClick = { onNavigationItemClick(item) },
|
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
|
||||||
selectedContainerColor = colors.selectedContainerColor,
|
|
||||||
unselectedContainerColor = colors.unselectedContainerColor,
|
|
||||||
selectedIconColor = colors.selectedIconColor,
|
|
||||||
unselectedIconColor = colors.unselectedIconColor,
|
|
||||||
selectedTextColor = colors.selectedTextColor,
|
|
||||||
unselectedTextColor = colors.unselectedTextColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(padding),
|
|
||||||
) {
|
|
||||||
movableContent(padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
widthDp >= 600f -> {
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = topBar,
|
|
||||||
snackbarHost = snackbarHost,
|
|
||||||
containerColor = colors.contentContainerColor,
|
|
||||||
contentColor = colors.contentColor,
|
|
||||||
contentWindowInsets = contentWindowInsets,
|
|
||||||
) { padding ->
|
|
||||||
Row {
|
|
||||||
NavigationRail(
|
|
||||||
containerColor = colors.railContainerColor,
|
|
||||||
) {
|
|
||||||
navigationItems.forEach { item ->
|
|
||||||
NavigationRailItem(
|
|
||||||
label = { navigationItemTitle(item, isItemSelected(item)) },
|
|
||||||
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
|
||||||
selected = isItemSelected(item),
|
|
||||||
onClick = { onNavigationItemClick(item) },
|
|
||||||
colors = NavigationRailItemDefaults.colors(
|
|
||||||
indicatorColor = colors.selectedContainerColor,
|
|
||||||
selectedIconColor = colors.selectedIconColor,
|
|
||||||
unselectedIconColor = colors.unselectedIconColor,
|
|
||||||
selectedTextColor = colors.selectedTextColor,
|
|
||||||
unselectedTextColor = colors.unselectedTextColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
movableContent(padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
snackbarHost = snackbarHost,
|
|
||||||
bottomBar = {
|
|
||||||
NavigationBar(
|
|
||||||
containerColor = colors.bottomBarContainerColor,
|
|
||||||
) {
|
|
||||||
navigationItems.forEach { item ->
|
|
||||||
NavigationBarItem(
|
|
||||||
label = { navigationItemTitle(item, isItemSelected(item)) },
|
|
||||||
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
|
||||||
selected = isItemSelected(item),
|
|
||||||
onClick = { onNavigationItemClick(item) },
|
|
||||||
colors = NavigationBarItemDefaults.colors(
|
|
||||||
indicatorColor = colors.selectedContainerColor,
|
|
||||||
selectedIconColor = colors.selectedIconColor,
|
|
||||||
unselectedIconColor = colors.unselectedIconColor,
|
|
||||||
selectedTextColor = colors.selectedTextColor,
|
|
||||||
unselectedTextColor = colors.unselectedTextColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = colors.contentContainerColor,
|
|
||||||
contentColor = colors.contentColor,
|
|
||||||
contentWindowInsets = contentWindowInsets,
|
|
||||||
) { padding ->
|
|
||||||
movableContent(padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,414 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
|
import androidx.compose.material3.NavigationDrawerItem
|
||||||
|
import androidx.compose.material3.NavigationDrawerItemDefaults
|
||||||
|
import androidx.compose.material3.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailDefaults
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.NavigationRailItemDefaults
|
||||||
|
import androidx.compose.material3.PermanentDrawerSheet
|
||||||
|
import androidx.compose.material3.PermanentNavigationDrawer
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.movableContentOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
|
|
||||||
|
class AdaptiveScaffoldNavigationComponentColors internal constructor(
|
||||||
|
val detailsPaneContainerColor: Color,
|
||||||
|
val railContainerColor: Color,
|
||||||
|
val drawerContainerColor: Color,
|
||||||
|
val bottomBarContainerColor: Color,
|
||||||
|
val contentContainerColor: Color,
|
||||||
|
val contentColor: Color,
|
||||||
|
val selectedIconColor: Color,
|
||||||
|
val selectedTextColor: Color,
|
||||||
|
val unselectedIconColor: Color,
|
||||||
|
val unselectedTextColor: Color,
|
||||||
|
val selectedContainerColor: Color,
|
||||||
|
val unselectedContainerColor: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
object AdaptiveScaffoldNavigationComponentDefaults {
|
||||||
|
@Composable
|
||||||
|
fun colors(
|
||||||
|
detailsPaneContainerColor: Color = NavigationBarDefaults.containerColor,
|
||||||
|
railContainerColor: Color = NavigationRailDefaults.ContainerColor,
|
||||||
|
drawerContainerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
bottomBarContainerColor: Color = NavigationBarDefaults.containerColor,
|
||||||
|
contentContainerColor: Color = MaterialTheme.colorScheme.background,
|
||||||
|
contentColor: Color = contentColorFor(contentContainerColor),
|
||||||
|
selectedIconColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
selectedTextColor: Color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
unselectedIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
unselectedTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
selectedContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
unselectedContainerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
) = AdaptiveScaffoldNavigationComponentColors(
|
||||||
|
detailsPaneContainerColor = detailsPaneContainerColor,
|
||||||
|
railContainerColor = railContainerColor,
|
||||||
|
drawerContainerColor = drawerContainerColor,
|
||||||
|
bottomBarContainerColor = bottomBarContainerColor,
|
||||||
|
contentContainerColor = contentContainerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
selectedIconColor = selectedIconColor,
|
||||||
|
selectedTextColor = selectedTextColor,
|
||||||
|
unselectedIconColor = unselectedIconColor,
|
||||||
|
unselectedTextColor = unselectedTextColor,
|
||||||
|
selectedContainerColor = selectedContainerColor,
|
||||||
|
unselectedContainerColor = unselectedContainerColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> AdaptiveScaffold(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navigationItems: List<T>,
|
||||||
|
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
isItemSelected: @Composable (item: T) -> Boolean,
|
||||||
|
onNavigationItemClick: (item: T) -> Unit,
|
||||||
|
topBar: @Composable () -> Unit = {},
|
||||||
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
|
colors: AdaptiveScaffoldNavigationComponentColors = AdaptiveScaffoldNavigationComponentDefaults.colors(),
|
||||||
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
|
isDetailsPaneVisible: Boolean = false,
|
||||||
|
detailsPane: @Composable () -> Unit = {},
|
||||||
|
content: @Composable (padding: PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
|
||||||
|
val widthDp = metrics.bounds.width() / context.resources.displayMetrics.density
|
||||||
|
val movableContent = remember(content) {
|
||||||
|
movableContentOf(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
widthDp >= 1240f -> DrawerScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationItems = navigationItems,
|
||||||
|
navigationItemTitle = navigationItemTitle,
|
||||||
|
navigationItemIcon = navigationItemIcon,
|
||||||
|
isItemSelected = isItemSelected,
|
||||||
|
onNavigationItemClick = onNavigationItemClick,
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
colors = colors,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
isDetailsPaneVisible = isDetailsPaneVisible,
|
||||||
|
detailsPane = detailsPane,
|
||||||
|
content = movableContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
widthDp >= 600f -> RailScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationItems = navigationItems,
|
||||||
|
navigationItemTitle = navigationItemTitle,
|
||||||
|
navigationItemIcon = navigationItemIcon,
|
||||||
|
isItemSelected = isItemSelected,
|
||||||
|
onNavigationItemClick = onNavigationItemClick,
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
colors = colors,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
isDetailsPaneVisible = isDetailsPaneVisible,
|
||||||
|
detailsPane = detailsPane,
|
||||||
|
content = movableContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> BottomBarScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationItems = navigationItems,
|
||||||
|
navigationItemTitle = navigationItemTitle,
|
||||||
|
navigationItemIcon = navigationItemIcon,
|
||||||
|
isItemSelected = isItemSelected,
|
||||||
|
onNavigationItemClick = onNavigationItemClick,
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
colors = colors,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
isDetailsPaneVisible = isDetailsPaneVisible,
|
||||||
|
detailsPane = detailsPane,
|
||||||
|
content = movableContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun <T> BottomBarScaffold(
|
||||||
|
modifier: Modifier,
|
||||||
|
navigationItems: List<T>,
|
||||||
|
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
isItemSelected: @Composable (item: T) -> Boolean,
|
||||||
|
onNavigationItemClick: (item: T) -> Unit,
|
||||||
|
topBar: @Composable () -> Unit,
|
||||||
|
snackbarHost: @Composable () -> Unit,
|
||||||
|
colors: AdaptiveScaffoldNavigationComponentColors,
|
||||||
|
contentWindowInsets: WindowInsets,
|
||||||
|
isDetailsPaneVisible: Boolean,
|
||||||
|
detailsPane: @Composable () -> Unit,
|
||||||
|
content: @Composable (padding: PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
bottomBar = {
|
||||||
|
NavigationBar(
|
||||||
|
containerColor = colors.bottomBarContainerColor,
|
||||||
|
) {
|
||||||
|
navigationItems.forEach { item ->
|
||||||
|
NavigationBarItem(
|
||||||
|
label = { navigationItemTitle(item, isItemSelected(item)) },
|
||||||
|
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
||||||
|
selected = isItemSelected(item),
|
||||||
|
onClick = { onNavigationItemClick(item) },
|
||||||
|
colors = NavigationBarItemDefaults.colors(
|
||||||
|
indicatorColor = colors.selectedContainerColor,
|
||||||
|
selectedIconColor = colors.selectedIconColor,
|
||||||
|
unselectedIconColor = colors.unselectedIconColor,
|
||||||
|
selectedTextColor = colors.selectedTextColor,
|
||||||
|
unselectedTextColor = colors.unselectedTextColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = colors.contentContainerColor,
|
||||||
|
contentColor = colors.contentColor,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
) { padding ->
|
||||||
|
content(padding)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isDetailsPaneVisible,
|
||||||
|
enter = slideInHorizontally(initialOffsetX = { it }),
|
||||||
|
exit = slideOutHorizontally(targetOffsetX = { it }),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Surface(color = colors.detailsPaneContainerColor) {
|
||||||
|
detailsPane()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun <T> RailScaffold(
|
||||||
|
modifier: Modifier,
|
||||||
|
navigationItems: List<T>,
|
||||||
|
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
isItemSelected: @Composable (item: T) -> Boolean,
|
||||||
|
onNavigationItemClick: (item: T) -> Unit,
|
||||||
|
topBar: @Composable () -> Unit,
|
||||||
|
snackbarHost: @Composable () -> Unit,
|
||||||
|
colors: AdaptiveScaffoldNavigationComponentColors,
|
||||||
|
contentWindowInsets: WindowInsets,
|
||||||
|
isDetailsPaneVisible: Boolean,
|
||||||
|
detailsPane: @Composable () -> Unit,
|
||||||
|
content: @Composable (padding: PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val weight: Float by animateFloatAsState(
|
||||||
|
targetValue = if (isDetailsPaneVisible) 0.5f else 1f,
|
||||||
|
animationSpec = tween(durationMillis = 500),
|
||||||
|
label = "Details Pane",
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(modifier = modifier) {
|
||||||
|
NavigationRail(
|
||||||
|
containerColor = colors.railContainerColor,
|
||||||
|
) {
|
||||||
|
navigationItems.forEach { item ->
|
||||||
|
NavigationRailItem(
|
||||||
|
label = { navigationItemTitle(item, isItemSelected(item)) },
|
||||||
|
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
||||||
|
selected = isItemSelected(item),
|
||||||
|
onClick = { onNavigationItemClick(item) },
|
||||||
|
colors = NavigationRailItemDefaults.colors(
|
||||||
|
indicatorColor = colors.selectedContainerColor,
|
||||||
|
selectedIconColor = colors.selectedIconColor,
|
||||||
|
unselectedIconColor = colors.unselectedIconColor,
|
||||||
|
selectedTextColor = colors.selectedTextColor,
|
||||||
|
unselectedTextColor = colors.unselectedTextColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(weight),
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
containerColor = colors.contentContainerColor,
|
||||||
|
contentColor = colors.contentColor,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
) { padding ->
|
||||||
|
content(padding)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
color = colors.detailsPaneContainerColor,
|
||||||
|
) {
|
||||||
|
detailsPane()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun <T> DrawerScaffold(
|
||||||
|
modifier: Modifier,
|
||||||
|
navigationItems: List<T>,
|
||||||
|
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
isItemSelected: @Composable (item: T) -> Boolean,
|
||||||
|
onNavigationItemClick: (item: T) -> Unit,
|
||||||
|
topBar: @Composable () -> Unit,
|
||||||
|
snackbarHost: @Composable () -> Unit,
|
||||||
|
colors: AdaptiveScaffoldNavigationComponentColors,
|
||||||
|
contentWindowInsets: WindowInsets,
|
||||||
|
isDetailsPaneVisible: Boolean,
|
||||||
|
detailsPane: @Composable () -> Unit,
|
||||||
|
content: @Composable (padding: PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val weight: Float by animateFloatAsState(
|
||||||
|
targetValue = if (isDetailsPaneVisible) 0.5f else 1f,
|
||||||
|
animationSpec = tween(durationMillis = 500),
|
||||||
|
label = "Details Pane",
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(modifier = modifier) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(weight),
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
containerColor = colors.contentContainerColor,
|
||||||
|
contentColor = colors.contentColor,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
) { padding ->
|
||||||
|
PermanentNavigationDrawer(
|
||||||
|
drawerContent = {
|
||||||
|
PermanentDrawerSheet(
|
||||||
|
drawerContainerColor = colors.drawerContainerColor,
|
||||||
|
) {
|
||||||
|
navigationItems.forEach { item ->
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = { navigationItemTitle(item, isItemSelected(item)) },
|
||||||
|
icon = { navigationItemIcon(item, isItemSelected(item)) },
|
||||||
|
selected = isItemSelected(item),
|
||||||
|
onClick = { onNavigationItemClick(item) },
|
||||||
|
colors = NavigationDrawerItemDefaults.colors(
|
||||||
|
selectedContainerColor = colors.selectedContainerColor,
|
||||||
|
unselectedContainerColor = colors.unselectedContainerColor,
|
||||||
|
selectedIconColor = colors.selectedIconColor,
|
||||||
|
unselectedIconColor = colors.unselectedIconColor,
|
||||||
|
selectedTextColor = colors.selectedTextColor,
|
||||||
|
unselectedTextColor = colors.unselectedTextColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
|
) {
|
||||||
|
content(padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
color = colors.detailsPaneContainerColor,
|
||||||
|
) {
|
||||||
|
detailsPane()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> AdaptiveScaffold(
|
||||||
|
navigator: AdaptiveScaffoldNavigator,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navigationItems: List<T>,
|
||||||
|
navigationItemTitle: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
navigationItemIcon: @Composable (item: T, isSelected: Boolean) -> Unit,
|
||||||
|
isItemSelected: @Composable (item: T) -> Boolean,
|
||||||
|
onNavigationItemClick: (item: T) -> Unit,
|
||||||
|
topBar: @Composable () -> Unit = {},
|
||||||
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
|
colors: AdaptiveScaffoldNavigationComponentColors = AdaptiveScaffoldNavigationComponentDefaults.colors(),
|
||||||
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
|
content: @Composable (padding: PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val isDetailsPaneVisible by navigator.isVisible.collectAsState(initial = false)
|
||||||
|
AdaptiveScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationItems = navigationItems,
|
||||||
|
navigationItemTitle = navigationItemTitle,
|
||||||
|
navigationItemIcon = navigationItemIcon,
|
||||||
|
isItemSelected = isItemSelected,
|
||||||
|
onNavigationItemClick = onNavigationItemClick,
|
||||||
|
topBar = topBar,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
colors = colors,
|
||||||
|
contentWindowInsets = contentWindowInsets,
|
||||||
|
isDetailsPaneVisible = isDetailsPaneVisible,
|
||||||
|
detailsPane = navigator.detailsPaneContent,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.FloatingWindow
|
||||||
|
import androidx.navigation.NamedNavArgument
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavDeepLink
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.Navigator
|
||||||
|
import androidx.navigation.NavigatorState
|
||||||
|
import androidx.navigation.compose.LocalOwnersProvider
|
||||||
|
import androidx.navigation.get
|
||||||
|
import com.google.samples.apps.nowinandroid.core.ui.AdaptiveScaffoldNavigator.Destination
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Navigator.Name("AdaptiveScaffoldNavigator")
|
||||||
|
class AdaptiveScaffoldNavigator : Navigator<Destination>() {
|
||||||
|
|
||||||
|
private var attached = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val backStack: Flow<List<NavBackStackEntry>> = attached.flatMapLatest {
|
||||||
|
if (it) {
|
||||||
|
state.backStack
|
||||||
|
} else {
|
||||||
|
MutableStateFlow(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isVisible = backStack.map { it.isNotEmpty() }
|
||||||
|
|
||||||
|
val detailsPaneContent: @Composable () -> Unit = {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
val currentBackStackEntry: NavBackStackEntry? by produceState<NavBackStackEntry?>(
|
||||||
|
initialValue = null,
|
||||||
|
key1 = backStack,
|
||||||
|
) {
|
||||||
|
backStack.map { backstack ->
|
||||||
|
backstack.lastOrNull()
|
||||||
|
}.collect { backStackEntry ->
|
||||||
|
value = backStackEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val backStackEntry = currentBackStackEntry
|
||||||
|
backStackEntry?.LocalOwnersProvider(saveableStateHolder) {
|
||||||
|
val destination = (backStackEntry.destination as? Destination)
|
||||||
|
destination?.content?.invoke(backStackEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(state: NavigatorState) {
|
||||||
|
super.onAttach(state)
|
||||||
|
attached.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigate(
|
||||||
|
entries: List<NavBackStackEntry>,
|
||||||
|
navOptions: NavOptions?,
|
||||||
|
navigatorExtras: Extras?,
|
||||||
|
) {
|
||||||
|
entries.forEach { entry ->
|
||||||
|
state.pushWithTransition(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
|
||||||
|
state.popWithTransition(popUpTo, savedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@NavDestination.ClassType(Composable::class)
|
||||||
|
class Destination(
|
||||||
|
navigator: AdaptiveScaffoldNavigator,
|
||||||
|
internal val content: @Composable (NavBackStackEntry) -> Unit,
|
||||||
|
) : NavDestination(navigator), FloatingWindow
|
||||||
|
|
||||||
|
override fun createDestination(): Destination = Destination(
|
||||||
|
this,
|
||||||
|
) { /* no-op */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.detailsPane(
|
||||||
|
route: String,
|
||||||
|
arguments: List<NamedNavArgument> = emptyList(),
|
||||||
|
deepLinks: List<NavDeepLink> = emptyList(),
|
||||||
|
content: @Composable (backstackEntry: NavBackStackEntry) -> Unit,
|
||||||
|
) {
|
||||||
|
addDestination(
|
||||||
|
Destination(
|
||||||
|
provider[AdaptiveScaffoldNavigator::class],
|
||||||
|
content,
|
||||||
|
).apply {
|
||||||
|
this.route = route
|
||||||
|
arguments.forEach { (argumentName, argument) ->
|
||||||
|
addArgument(argumentName, argument)
|
||||||
|
}
|
||||||
|
deepLinks.forEach { deepLink ->
|
||||||
|
addDeepLink(deepLink)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in new issue