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 c05427873..0fb160a1b 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 @@ -55,6 +55,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -64,6 +65,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme +import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @@ -78,41 +80,45 @@ fun NiaApp(windowSizeClass: WindowSizeClass) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - Scaffold( - modifier = Modifier, - bottomBar = { - if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { - NiABottomBar( - navigationActions = navigationActions, - currentDestination = currentDestination - ) + NiaBackground { + Scaffold( + modifier = Modifier, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + bottomBar = { + if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { + NiABottomBar( + navigationActions = navigationActions, + currentDestination = currentDestination + ) + } } - } - ) { padding -> - Row( - Modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal + ) { padding -> + Row( + Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal + ) ) - ) - ) { - if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { - NiANavRail( - navigationActions = navigationActions, - currentDestination = currentDestination, - modifier = Modifier.safeDrawingPadding() + ) { + if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { + NiANavRail( + navigationActions = navigationActions, + currentDestination = currentDestination, + modifier = Modifier.safeDrawingPadding() + ) + } + + NiaNavGraph( + windowSizeClass = windowSizeClass, + navController = navController, + modifier = Modifier + .padding(padding) + .consumedWindowInsets(padding) ) } - - NiaNavGraph( - windowSizeClass = windowSizeClass, - navController = navController, - modifier = Modifier - .padding(padding) - .consumedWindowInsets(padding) - ) } } } diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt new file mode 100644 index 000000000..0d02baac7 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2022 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.component + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.google.samples.apps.nowinandroid.core.ui.theme.LocalBackgroundTheme +import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme + +/** + * The main background for the app. + * Uses [LocalBackgroundTheme] to set the color and tonal elevation of a [Surface]. + * + * @param modifier Modifier to be applied to the background. + * @param content The background content. + */ +@Composable +fun NiaBackground( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val color = LocalBackgroundTheme.current.color + val tonalElevation = LocalBackgroundTheme.current.tonalElevation + Surface( + color = if (color == Color.Unspecified) Color.Transparent else color, + tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation, + modifier = modifier.fillMaxSize(), + content = content + ) +} + +/** + * A gradient background for select screens, to be overlaid on top of [NiaBackground]. + * Uses [LocalBackgroundTheme] to set the gradient colors of a [Box]. + * + * @param modifier Modifier to be applied to the background. + * @param topColor The top gradient color to be rendered. + * @param bottomColor The bottom gradient color to be rendered. + * @param content The background content. + */ +@Composable +fun NiaGradientBackground( + modifier: Modifier = Modifier, + topColor: Color = LocalBackgroundTheme.current.primaryGradientColor, + bottomColor: Color = LocalBackgroundTheme.current.secondaryGradientColor, + content: @Composable () -> Unit +) { + val gradientModifier = Modifier.background( + Brush.verticalGradient( + listOf( + if (topColor == Color.Unspecified) Color.Transparent else topColor, + Color.Transparent, + if (bottomColor == Color.Unspecified) Color.Transparent else bottomColor + ) + ) + ) + Box( + modifier = modifier + .fillMaxSize() + .then(gradientModifier) + ) { + content() + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun BackgroundDefault() { + NiaTheme { + NiaBackground(Modifier.size(100.dp), content = {}) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun BackgroundDynamic() { + NiaTheme(dynamicColor = true) { + NiaBackground(Modifier.size(100.dp), content = {}) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun BackgroundAndroid() { + NiaTheme(androidTheme = true) { + NiaBackground(Modifier.size(100.dp), content = {}) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun GradientBackgroundDefault() { + NiaTheme { + NiaGradientBackground(Modifier.size(100.dp), content = {}) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun GradientBackgroundDynamic() { + NiaTheme(dynamicColor = true) { + NiaGradientBackground(Modifier.size(100.dp), content = {}) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Composable +fun GradientBackgroundAndroid() { + NiaTheme(androidTheme = true) { + NiaGradientBackground(Modifier.size(100.dp), content = {}) + } +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt new file mode 100644 index 000000000..d39e21e67 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 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.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +/** + * A class to model background values for Now in Android, + * including color, tonal elevation and gradient colors. + */ +@Immutable +data class BackgroundTheme( + val color: Color = Color.Unspecified, + val tonalElevation: Dp = Dp.Unspecified, + val primaryGradientColor: Color = Color.Unspecified, + val secondaryGradientColor: Color = Color.Unspecified, + val tertiaryGradientColor: Color = Color.Unspecified, + val neutralGradientColor: Color = Color.Unspecified +) + +/** + * A composition local for [BackgroundTheme]. + */ +val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() } diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt index ffa628dbe..d8d07dce0 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt @@ -17,6 +17,8 @@ package com.google.samples.apps.nowinandroid.core.ui.theme import androidx.compose.ui.graphics.Color +import androidx.core.graphics.ColorUtils +import kotlin.math.roundToInt /** * Now in Android colors. @@ -27,6 +29,7 @@ val Blue30 = Color(0xFF004D61) val Blue40 = Color(0xFF006781) val Blue80 = Color(0xFF5DD4FB) val Blue90 = Color(0xFFB5EAFF) +val Blue95 = Color(0xFFDCF5FF) val DarkGreen10 = Color(0xFF0D1F12) val DarkGreen20 = Color(0xFF223526) val DarkGreen30 = Color(0xFF394B3C) @@ -35,9 +38,11 @@ val DarkGreen80 = Color(0xFFB7CCB8) val DarkGreen90 = Color(0xFFD3E8D3) val DarkGreenGray10 = Color(0xFF1A1C1A) val DarkGreenGray90 = Color(0xFFE2E3DE) +val DarkGreenGray95 = Color(0xFFF0F1EC) val DarkGreenGray99 = Color(0xFFFBFDF7) val DarkPurpleGray10 = Color(0xFF201A1B) val DarkPurpleGray90 = Color(0xFFECDFE0) +val DarkPurpleGray95 = Color(0xFFFAEEEF) val DarkPurpleGray99 = Color(0xFFFCFCFC) val Green10 = Color(0xFF00210B) val Green20 = Color(0xFF003919) @@ -56,12 +61,14 @@ val Orange30 = Color(0xFF812800) val Orange40 = Color(0xFFA23F16) val Orange80 = Color(0xFFFFB599) val Orange90 = Color(0xFFFFDBCE) +val Orange95 = Color(0xFFFFEDE6) val Purple10 = Color(0xFF36003D) val Purple20 = Color(0xFF560A5E) val Purple30 = Color(0xFF702776) val Purple40 = Color(0xFF8C4190) val Purple80 = Color(0xFFFFA8FF) val Purple90 = Color(0xFFFFD5FC) +val Purple95 = Color(0xFFFFEBFB) val PurpleGray30 = Color(0xFF4E444C) val PurpleGray50 = Color(0xFF7F747C) val PurpleGray60 = Color(0xFF998D96) @@ -79,3 +86,23 @@ val Teal30 = Color(0xFF214D56) val Teal40 = Color(0xFF3A656F) val Teal80 = Color(0xFFA2CED9) val Teal90 = Color(0xFFBEEAF6) + +/** + * Lighten the current [Color] instance to the given [luminance]. + * + * This is needed because we can't access the token values directly. For the dynamic color theme, + * this makes it impossible to get the 95% luminance token of the different theme colors. + * TODO: Link to bug + */ +internal fun Color.lighten(luminance: Float): Color { + val hsl = FloatArray(3) + ColorUtils.RGBToHSL( + (red * 256).roundToInt(), + (green * 256).roundToInt(), + (blue * 256).roundToInt(), + hsl + ) + hsl[2] = luminance + val color = Color(ColorUtils.HSLToColor(hsl)) + return color +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt index 1b9e8d47b..8b7f7a151 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt @@ -24,8 +24,10 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp /** * Light default theme color scheme @@ -171,9 +173,40 @@ fun NiaTheme( darkTheme -> DarkDefaultColorScheme else -> LightDefaultColorScheme } - MaterialTheme( - colorScheme = colorScheme, - typography = NiaTypography, - content = content - ) + + val backgroundTheme = when { + androidTheme && darkTheme -> BackgroundTheme( + color = Color.Black + ) + androidTheme -> BackgroundTheme( + color = DarkGreenGray95 + ) + darkTheme -> BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp + ) + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp, + primaryGradientColor = colorScheme.primary.lighten(0.95f), + secondaryGradientColor = colorScheme.secondary.lighten(0.95f), + tertiaryGradientColor = colorScheme.tertiary.lighten(0.95f), + neutralGradientColor = colorScheme.surface.lighten(0.95f) + ) + else -> BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp, + primaryGradientColor = Purple95, + secondaryGradientColor = Orange95, + tertiaryGradientColor = Blue95, + neutralGradientColor = DarkPurpleGray95 + ) + } + CompositionLocalProvider(LocalBackgroundTheme provides backgroundTheme) { + MaterialTheme( + colorScheme = colorScheme, + typography = NiaTypography, + content = content + ) + } } diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 9abf130d1..2c790d4b8 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -84,6 +84,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded +import com.google.samples.apps.nowinandroid.core.ui.component.NiaGradientBackground import com.google.samples.apps.nowinandroid.core.ui.component.NiaToggleButton import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons @@ -123,68 +124,70 @@ fun ForYouScreen( onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { - // TODO: Replace with `LazyVerticalGrid` when blocking bugs are fixed: - // https://issuetracker.google.com/issues/230514914 - // https://issuetracker.google.com/issues/231320714 - BoxWithConstraints( - modifier = modifier - ) { - val numberOfColumns = when (windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1 - else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) - } + NiaGradientBackground { + // TODO: Replace with `LazyVerticalGrid` when blocking bugs are fixed: + // https://issuetracker.google.com/issues/230514914 + // https://issuetracker.google.com/issues/231320714 + BoxWithConstraints( + modifier = modifier + ) { + val numberOfColumns = when (windowSizeClass.widthSizeClass) { + WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1 + else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) + } - LazyColumn(modifier = Modifier.fillMaxSize()) { - item { - Spacer( - // TODO: Replace with windowInsetsTopHeight after - // https://issuetracker.google.com/issues/230383055 - Modifier.windowInsetsPadding( - WindowInsets.safeDrawing.only(WindowInsetsSides.Top) + LazyColumn(modifier = Modifier.fillMaxSize()) { + item { + Spacer( + // TODO: Replace with windowInsetsTopHeight after + // https://issuetracker.google.com/issues/230383055 + Modifier.windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Top) + ) ) - ) - } + } - item { - NiaTopAppBar( - titleRes = R.string.top_app_bar_title, - navigationIcon = Icons.Filled.Search, - navigationIconContentDescription = stringResource( - id = R.string.top_app_bar_navigation_button_content_desc - ), - actionIcon = Icons.Outlined.AccountCircle, - actionIconContentDescription = stringResource( - id = R.string.top_app_bar_navigation_button_content_desc - ), - ) - } + item { + NiaTopAppBar( + titleRes = R.string.top_app_bar_title, + navigationIcon = Icons.Filled.Search, + navigationIconContentDescription = stringResource( + id = R.string.top_app_bar_navigation_button_content_desc + ), + actionIcon = Icons.Outlined.AccountCircle, + actionIconContentDescription = stringResource( + id = R.string.top_app_bar_navigation_button_content_desc + ), + ) + } - InterestsSelection( - interestsSelectionState = interestsSelectionState, - showLoadingUIIfLoading = true, - onAuthorCheckedChanged = onAuthorCheckedChanged, - onTopicCheckedChanged = onTopicCheckedChanged, - saveFollowedTopics = saveFollowedTopics - ) + InterestsSelection( + interestsSelectionState = interestsSelectionState, + showLoadingUIIfLoading = true, + onAuthorCheckedChanged = onAuthorCheckedChanged, + onTopicCheckedChanged = onTopicCheckedChanged, + saveFollowedTopics = saveFollowedTopics + ) - Feed( - feedState = feedState, - // Avoid showing a second loading wheel if we already are for the interests - // selection - showLoadingUIIfLoading = - interestsSelectionState !is ForYouInterestsSelectionState.Loading, - numberOfColumns = numberOfColumns, - onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged - ) + Feed( + feedState = feedState, + // Avoid showing a second loading wheel if we already are for the interests + // selection + showLoadingUIIfLoading = + interestsSelectionState !is ForYouInterestsSelectionState.Loading, + numberOfColumns = numberOfColumns, + onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged + ) - item { - Spacer( - // TODO: Replace with windowInsetsBottomHeight after - // https://issuetracker.google.com/issues/230383055 - Modifier.windowInsetsPadding( - WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom) + item { + Spacer( + // TODO: Replace with windowInsetsBottomHeight after + // https://issuetracker.google.com/issues/230383055 + Modifier.windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom) + ) ) - ) + } } } }