From 594e1798d9be6bd4150324050fb5aace7c7c5423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Sun, 18 Jun 2023 11:37:08 +0200 Subject: [PATCH] Implement AdaptiveScaffold Change-Id: I02d1058593bbfa25eb7d9f2489b1960792b0e71e --- .../apps/nowinandroid/ui/AdaptiveScaffold.kt | 216 ++++++++++++++++++ .../samples/apps/nowinandroid/ui/NiaApp.kt | 185 +++------------ gradle/libs.versions.toml | 2 +- 3 files changed, 251 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/AdaptiveScaffold.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/AdaptiveScaffold.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/AdaptiveScaffold.kt new file mode 100644 index 000000000..b651106dc --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/AdaptiveScaffold.kt @@ -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 AdaptiveScaffold( + modifier: Modifier = Modifier, + navigationItems: List, + 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) + } + } + } +} diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index aa85afebd..effe22b52 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -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, + ) + }, + isItemSelected = { item -> + appState.currentDestination.isTopLevelDestinationInHierarchy( + item, + ) }, - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, + 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,128 +161,12 @@ 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, - destinationsWithUnreadResources: Set, - 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, - destinationsWithUnreadResources: Set, - 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, - ) - } - } -} - private fun Modifier.notificationDot(): Modifier = composed { val tertiaryColor = MaterialTheme.colorScheme.tertiary diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc50f2402..d65615e91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"