Add gradient background to For You Screen

Change-Id: I6a85ee82a78b1897020f1737503611bd100764a1
pull/2/head
Jolanda Verhoef 2 years ago committed by Don Turner
parent cce9402b04
commit ce0142b98a

@ -55,6 +55,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -64,6 +65,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme 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 import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@ -78,41 +80,45 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination val currentDestination = navBackStackEntry?.destination
Scaffold( NiaBackground {
modifier = Modifier, Scaffold(
bottomBar = { modifier = Modifier,
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { containerColor = Color.Transparent,
NiABottomBar( contentColor = MaterialTheme.colorScheme.onBackground,
navigationActions = navigationActions, bottomBar = {
currentDestination = currentDestination if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
) NiABottomBar(
navigationActions = navigationActions,
currentDestination = currentDestination
)
}
} }
} ) { padding ->
) { padding -> Row(
Row( Modifier
Modifier .fillMaxSize()
.fillMaxSize() .windowInsetsPadding(
.windowInsetsPadding( WindowInsets.safeDrawing.only(
WindowInsets.safeDrawing.only( WindowInsetsSides.Horizontal
WindowInsetsSides.Horizontal )
) )
) ) {
) { if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) {
if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { NiANavRail(
NiANavRail( navigationActions = navigationActions,
navigationActions = navigationActions, currentDestination = currentDestination,
currentDestination = currentDestination, modifier = Modifier.safeDrawingPadding()
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)
)
} }
} }
} }

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

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

@ -17,6 +17,8 @@
package com.google.samples.apps.nowinandroid.core.ui.theme package com.google.samples.apps.nowinandroid.core.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.core.graphics.ColorUtils
import kotlin.math.roundToInt
/** /**
* Now in Android colors. * Now in Android colors.
@ -27,6 +29,7 @@ val Blue30 = Color(0xFF004D61)
val Blue40 = Color(0xFF006781) val Blue40 = Color(0xFF006781)
val Blue80 = Color(0xFF5DD4FB) val Blue80 = Color(0xFF5DD4FB)
val Blue90 = Color(0xFFB5EAFF) val Blue90 = Color(0xFFB5EAFF)
val Blue95 = Color(0xFFDCF5FF)
val DarkGreen10 = Color(0xFF0D1F12) val DarkGreen10 = Color(0xFF0D1F12)
val DarkGreen20 = Color(0xFF223526) val DarkGreen20 = Color(0xFF223526)
val DarkGreen30 = Color(0xFF394B3C) val DarkGreen30 = Color(0xFF394B3C)
@ -35,9 +38,11 @@ val DarkGreen80 = Color(0xFFB7CCB8)
val DarkGreen90 = Color(0xFFD3E8D3) val DarkGreen90 = Color(0xFFD3E8D3)
val DarkGreenGray10 = Color(0xFF1A1C1A) val DarkGreenGray10 = Color(0xFF1A1C1A)
val DarkGreenGray90 = Color(0xFFE2E3DE) val DarkGreenGray90 = Color(0xFFE2E3DE)
val DarkGreenGray95 = Color(0xFFF0F1EC)
val DarkGreenGray99 = Color(0xFFFBFDF7) val DarkGreenGray99 = Color(0xFFFBFDF7)
val DarkPurpleGray10 = Color(0xFF201A1B) val DarkPurpleGray10 = Color(0xFF201A1B)
val DarkPurpleGray90 = Color(0xFFECDFE0) val DarkPurpleGray90 = Color(0xFFECDFE0)
val DarkPurpleGray95 = Color(0xFFFAEEEF)
val DarkPurpleGray99 = Color(0xFFFCFCFC) val DarkPurpleGray99 = Color(0xFFFCFCFC)
val Green10 = Color(0xFF00210B) val Green10 = Color(0xFF00210B)
val Green20 = Color(0xFF003919) val Green20 = Color(0xFF003919)
@ -56,12 +61,14 @@ val Orange30 = Color(0xFF812800)
val Orange40 = Color(0xFFA23F16) val Orange40 = Color(0xFFA23F16)
val Orange80 = Color(0xFFFFB599) val Orange80 = Color(0xFFFFB599)
val Orange90 = Color(0xFFFFDBCE) val Orange90 = Color(0xFFFFDBCE)
val Orange95 = Color(0xFFFFEDE6)
val Purple10 = Color(0xFF36003D) val Purple10 = Color(0xFF36003D)
val Purple20 = Color(0xFF560A5E) val Purple20 = Color(0xFF560A5E)
val Purple30 = Color(0xFF702776) val Purple30 = Color(0xFF702776)
val Purple40 = Color(0xFF8C4190) val Purple40 = Color(0xFF8C4190)
val Purple80 = Color(0xFFFFA8FF) val Purple80 = Color(0xFFFFA8FF)
val Purple90 = Color(0xFFFFD5FC) val Purple90 = Color(0xFFFFD5FC)
val Purple95 = Color(0xFFFFEBFB)
val PurpleGray30 = Color(0xFF4E444C) val PurpleGray30 = Color(0xFF4E444C)
val PurpleGray50 = Color(0xFF7F747C) val PurpleGray50 = Color(0xFF7F747C)
val PurpleGray60 = Color(0xFF998D96) val PurpleGray60 = Color(0xFF998D96)
@ -79,3 +86,23 @@ val Teal30 = Color(0xFF214D56)
val Teal40 = Color(0xFF3A656F) val Teal40 = Color(0xFF3A656F)
val Teal80 = Color(0xFFA2CED9) val Teal80 = Color(0xFFA2CED9)
val Teal90 = Color(0xFFBEEAF6) 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
}

@ -24,8 +24,10 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
/** /**
* Light default theme color scheme * Light default theme color scheme
@ -171,9 +173,40 @@ fun NiaTheme(
darkTheme -> DarkDefaultColorScheme darkTheme -> DarkDefaultColorScheme
else -> LightDefaultColorScheme else -> LightDefaultColorScheme
} }
MaterialTheme(
colorScheme = colorScheme, val backgroundTheme = when {
typography = NiaTypography, androidTheme && darkTheme -> BackgroundTheme(
content = content 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
)
}
} }

@ -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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel 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.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.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
@ -123,68 +124,70 @@ fun ForYouScreen(
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// TODO: Replace with `LazyVerticalGrid` when blocking bugs are fixed: NiaGradientBackground {
// https://issuetracker.google.com/issues/230514914 // TODO: Replace with `LazyVerticalGrid` when blocking bugs are fixed:
// https://issuetracker.google.com/issues/231320714 // https://issuetracker.google.com/issues/230514914
BoxWithConstraints( // https://issuetracker.google.com/issues/231320714
modifier = modifier BoxWithConstraints(
) { modifier = modifier
val numberOfColumns = when (windowSizeClass.widthSizeClass) { ) {
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1 val numberOfColumns = when (windowSizeClass.widthSizeClass) {
else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1
} else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1)
}
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
item { item {
Spacer( Spacer(
// TODO: Replace with windowInsetsTopHeight after // TODO: Replace with windowInsetsTopHeight after
// https://issuetracker.google.com/issues/230383055 // https://issuetracker.google.com/issues/230383055
Modifier.windowInsetsPadding( Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Top) WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
)
) )
) }
}
item { item {
NiaTopAppBar( NiaTopAppBar(
titleRes = R.string.top_app_bar_title, titleRes = R.string.top_app_bar_title,
navigationIcon = Icons.Filled.Search, navigationIcon = Icons.Filled.Search,
navigationIconContentDescription = stringResource( navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.top_app_bar_navigation_button_content_desc
), ),
actionIcon = Icons.Outlined.AccountCircle, actionIcon = Icons.Outlined.AccountCircle,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.top_app_bar_navigation_button_content_desc
), ),
) )
} }
InterestsSelection( InterestsSelection(
interestsSelectionState = interestsSelectionState, interestsSelectionState = interestsSelectionState,
showLoadingUIIfLoading = true, showLoadingUIIfLoading = true,
onAuthorCheckedChanged = onAuthorCheckedChanged, onAuthorCheckedChanged = onAuthorCheckedChanged,
onTopicCheckedChanged = onTopicCheckedChanged, onTopicCheckedChanged = onTopicCheckedChanged,
saveFollowedTopics = saveFollowedTopics saveFollowedTopics = saveFollowedTopics
) )
Feed( Feed(
feedState = feedState, feedState = feedState,
// Avoid showing a second loading wheel if we already are for the interests // Avoid showing a second loading wheel if we already are for the interests
// selection // selection
showLoadingUIIfLoading = showLoadingUIIfLoading =
interestsSelectionState !is ForYouInterestsSelectionState.Loading, interestsSelectionState !is ForYouInterestsSelectionState.Loading,
numberOfColumns = numberOfColumns, numberOfColumns = numberOfColumns,
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged
) )
item { item {
Spacer( Spacer(
// TODO: Replace with windowInsetsBottomHeight after // TODO: Replace with windowInsetsBottomHeight after
// https://issuetracker.google.com/issues/230383055 // https://issuetracker.google.com/issues/230383055
Modifier.windowInsetsPadding( Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom) WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
)
) )
) }
} }
} }
} }

Loading…
Cancel
Save