Implement AdaptiveScaffold

Change-Id: I02d1058593bbfa25eb7d9f2489b1960792b0e71e
feature/adaptive_scaffold
Miłosz Moczkowski 1 year ago
parent 4d65946f95
commit 594e1798d9

@ -0,0 +1,216 @@
/*
* 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.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.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
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),
) {
content(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,
),
)
}
}
content(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 ->
content(padding)
}
}
}
}

@ -16,29 +16,22 @@
package com.google.samples.apps.nowinandroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
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.Scaffold
import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarDuration.Short
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,7 +46,6 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
@ -66,21 +58,13 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc
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.NiaNavigationBar
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRail
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRailItem
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.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(
ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class,
)
@ -132,29 +116,44 @@ fun NiaApp(
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle()
Scaffold(
modifier = Modifier.semantics {
testTagsAsResourceId = true
AdaptiveScaffold(
navigationItems = TopLevelDestination.values().toList(),
navigationItemTitle = { item, _ -> Text(text = stringResource(id = item.titleTextId)) },
navigationItemIcon = { item, isSelected ->
val isUnread = unreadDestinations.contains(item)
Icon(
imageVector = if (isSelected) item.selectedIcon else item.unselectedIcon,
contentDescription = stringResource(id = item.iconTextId),
modifier = if (isUnread) Modifier.notificationDot() else Modifier,
)
},
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
isItemSelected = { item ->
appState.currentDestination.isTopLevelDestinationInHierarchy(
item,
)
},
onNavigationItemClick = appState::navigateToTopLevelDestination,
colors = AdaptiveScaffoldNavigationComponentDefaults.colors(
drawerContainerColor = Color.Transparent,
railContainerColor = Color.Transparent,
contentContainerColor = Color.Transparent,
),
contentWindowInsets = WindowInsets(0, 0, 0, 0),
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
if (appState.shouldShowBottomBar) {
NiaBottomBar(
destinations = appState.topLevelDestinations,
destinationsWithUnreadResources = unreadDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier.testTag("NiaBottomBar"),
)
}
modifier = Modifier.semantics {
testTagsAsResourceId = true
},
) { padding ->
Row(
Modifier
.fillMaxSize()
NiaNavHost(
appState = appState,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = Short,
) == ActionPerformed
},
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
@ -162,124 +161,8 @@ fun NiaApp(
WindowInsetsSides.Horizontal,
),
),
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
destinations = appState.topLevelDestinations,
destinationsWithUnreadResources = unreadDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier
.testTag("NiaNavRail")
.safeDrawingPadding(),
)
}
Column(Modifier.fillMaxSize()) {
// Show the top app bar on top level destinations.
val destination = appState.currentTopLevelDestination
if (destination != null) {
NiaTopAppBar(
titleRes = destination.titleTextId,
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() },
)
}
NiaNavHost(appState = appState, onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = Short,
) == ActionPerformed
})
}
// TODO: We may want to add padding or spacer when the snackbar is shown so that
// content doesn't display behind it.
}
}
}
}
}
@Composable
private fun NiaNavRail(
destinations: List<TopLevelDestination>,
destinationsWithUnreadResources: Set<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?,
modifier: Modifier = Modifier,
) {
NiaNavigationRail(modifier = modifier) {
destinations.forEach { destination ->
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
val hasUnread = destinationsWithUnreadResources.contains(destination)
NiaNavigationRailItem(
selected = selected,
onClick = { onNavigateToDestination(destination) },
icon = {
Icon(
imageVector = destination.unselectedIcon,
contentDescription = null,
)
},
selectedIcon = {
Icon(
imageVector = destination.selectedIcon,
contentDescription = null,
)
},
label = { Text(stringResource(destination.iconTextId)) },
modifier = if (hasUnread) Modifier.notificationDot() else Modifier,
)
}
}
}
@Composable
private fun NiaBottomBar(
destinations: List<TopLevelDestination>,
destinationsWithUnreadResources: Set<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?,
modifier: Modifier = Modifier,
) {
NiaNavigationBar(
modifier = modifier,
) {
destinations.forEach { destination ->
val hasUnread = destinationsWithUnreadResources.contains(destination)
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
NiaNavigationBarItem(
selected = selected,
onClick = { onNavigateToDestination(destination) },
icon = {
Icon(
imageVector = destination.unselectedIcon,
contentDescription = null,
)
},
selectedIcon = {
Icon(
imageVector = destination.selectedIcon,
contentDescription = null,
)
},
label = { Text(stringResource(destination.iconTextId)) },
modifier = if (hasUnread) Modifier.notificationDot() else Modifier,
)
}
}
}

@ -25,7 +25,7 @@ androidxTestRules = "1.5.0"
androidxTestRunner = "1.5.1"
androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0"
androidxWindowManager = "1.0.0"
androidxWindowManager = "1.1.0"
androidxWork = "2.9.0-alpha01"
coil = "2.2.2"
firebaseBom = "31.2.0"

Loading…
Cancel
Save