Add details pane support to AdaptiveScaffold

Change-Id: I68ff2584ec0c5be2209c21d1bec9b71acad6f42d
feature/adaptive_scaffold
Miłosz Moczkowski 2 years ago
parent 2a6fb1ea60
commit 0cd61134e1

@ -49,8 +49,9 @@ fun NiaNavHost(
startDestination = startDestination,
modifier = modifier,
) {
// TODO: handle topic clicks from each top level destination
forYouScreen(onTopicClick = {})
forYouScreen(
onTopicClick = navController::navigateToTopic,
)
bookmarksScreen(
onTopicClick = navController::navigateToTopic,
onShowSnackbar = onShowSnackbar,
@ -61,15 +62,11 @@ fun NiaNavHost(
onTopicClick = navController::navigateToTopic,
)
interestsGraph(
onTopicClick = { topicId ->
navController.navigateToTopic(topicId)
},
nestedGraphs = {
onTopicClick = navController::navigateToTopic,
)
topicScreen(
onBackClick = navController::popBackStack,
onTopicClick = {},
)
},
onTopicClick = navController::navigateToTopic,
)
}
}

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

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration.Indefinite
@ -32,6 +33,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -53,30 +55,42 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.ui.AdaptiveScaffold
import com.google.samples.apps.nowinandroid.core.ui.AdaptiveScaffoldNavigationComponentDefaults
import com.google.samples.apps.nowinandroid.core.ui.AdaptiveScaffoldNavigator
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
@OptIn(
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class,
)
@Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
adaptiveScaffoldNavigator: AdaptiveScaffoldNavigator = remember {
AdaptiveScaffoldNavigator()
},
appState: NiaAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass,
userNewsResourceRepository = userNewsResourceRepository,
navController = rememberNavController(adaptiveScaffoldNavigator),
),
) {
val shouldShowGradientBackground =
@ -117,6 +131,7 @@ fun NiaApp(
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle()
AdaptiveScaffold(
navigator = adaptiveScaffoldNavigator,
navigationItems = TopLevelDestination.values().toList(),
navigationItemTitle = { item, _ -> Text(text = stringResource(id = item.titleTextId)) },
navigationItemIcon = { item, isSelected ->
@ -138,6 +153,28 @@ fun NiaApp(
railContainerColor = Color.Transparent,
contentContainerColor = Color.Transparent,
),
topBar = {
val titleResId by appState.topBarTitle
val title = titleResId?.let { stringResource(it) }
if (title != null) {
NiaTopAppBar(
title = title,
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_navigation_icon_description,
),
actionIcon = NiaIcons.Settings,
actionIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_action_icon_description,
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
),
onActionClick = { showSettingsDialog = true },
onNavigationClick = { appState.navigateToSearch() },
)
}
},
contentWindowInsets = WindowInsets(0, 0, 0, 0),
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = Modifier.semantics {

@ -20,6 +20,8 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
@ -97,6 +99,21 @@ class NiaAppState(
else -> null
}
val topBarTitle: State<Int?>
@Composable get() = produceState<Int?>(initialValue = null) {
navController.currentBackStackEntryFlow.collect { backStackEntry ->
val topLevelDestination = when (backStackEntry.destination.route) {
forYouNavigationRoute -> FOR_YOU
bookmarksRoute -> BOOKMARKS
interestsRoute -> INTERESTS
else -> null
}
if (topLevelDestination != null) {
value = topLevelDestination.titleTextId
}
}
}
val shouldShowBottomBar: Boolean
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact

@ -38,7 +38,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NiaTopAppBar(
@StringRes titleRes: Int,
title: String,
navigationIcon: ImageVector,
navigationIconContentDescription: String?,
actionIcon: ImageVector,
@ -49,7 +49,7 @@ fun NiaTopAppBar(
onActionClick: () -> Unit = {},
) {
CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = titleRes)) },
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = onNavigationClick) {
Icon(
@ -107,7 +107,7 @@ fun NiaTopAppBar(
@Composable
private fun NiaTopAppBarPreview() {
NiaTopAppBar(
titleRes = android.R.string.untitled,
title = stringResource(id = android.R.string.untitled),
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = "Navigation icon",
actionIcon = NiaIcons.MoreVert,

@ -46,6 +46,8 @@ dependencies {
implementation(project(":core:model"))
implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.window.manager)
implementation(libs.coil.kt)
implementation(libs.coil.kt.compose)
implementation(libs.kotlinx.datetime)

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

@ -32,7 +32,6 @@ fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) {
fun NavGraphBuilder.interestsGraph(
onTopicClick: (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit,
) {
navigation(
route = interestsGraphRoutePattern,
@ -41,6 +40,5 @@ fun NavGraphBuilder.interestsGraph(
composable(route = interestsRoute) {
InterestsRoute(onTopicClick)
}
nestedGraphs()
}
}

@ -22,9 +22,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.ui.detailsPane
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
@VisibleForTesting
@ -46,7 +46,7 @@ fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit,
onTopicClick: (String) -> Unit,
) {
composable(
detailsPane(
route = "topic_route/{$topicIdArg}",
arguments = listOf(
navArgument(topicIdArg) { type = NavType.StringType },

Loading…
Cancel
Save