Setup dynamic theming for imagery in the app

jv/dynamic-icon-tints
Jolanda Verhoef 2 years ago
parent ece3591ed2
commit 310de39120

@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -34,9 +33,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaDropdownMenuButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
@ -50,7 +52,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggl
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaViewToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaIconTint
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.niaIconTint
/**
* Now in Android component catalog.
@ -58,7 +62,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@Composable
fun NiaCatalog() {
NiaTheme {
Surface {
NiaBackground {
val contentPadding = WindowInsets
.systemBars
.add(WindowInsets(left = 16.dp, top = 16.dp, right = 16.dp, bottom = 16.dp))
@ -74,6 +78,37 @@ fun NiaCatalog() {
style = MaterialTheme.typography.headlineSmall,
)
}
item { Text("Themed icons", Modifier.padding(top = 16.dp)) }
item {
FlowRow(mainAxisSpacing = 16.dp) {
Icon(
tint = NiaIconTint.PRIMARY.color(),
imageVector = ImageVector.vectorResource(NiaIcons.Bookmarks),
contentDescription = null,
)
Icon(
tint = NiaIconTint.SECONDARY.color(),
imageVector = ImageVector.vectorResource(NiaIcons.Bookmarks),
contentDescription = null,
)
Icon(
tint = NiaIconTint.TERTIARY.color(),
imageVector = ImageVector.vectorResource(NiaIcons.Bookmarks),
contentDescription = null,
)
Icon(
modifier = Modifier.niaIconTint(NiaIconTint.PRIMARY_SECONDARY.brush()),
imageVector = ImageVector.vectorResource(NiaIcons.Bookmarks),
contentDescription = null,
)
Icon(
modifier = Modifier.niaIconTint(NiaIconTint.PRIMARY_TERTIARY.brush()),
imageVector = ImageVector.vectorResource(NiaIcons.Bookmarks),
contentDescription = null,
)
}
}
item { Text("Buttons", Modifier.padding(top = 16.dp)) }
item {
FlowRow(mainAxisSpacing = 16.dp) {

@ -84,3 +84,6 @@ internal val Teal30 = Color(0xFF214D56)
internal val Teal40 = Color(0xFF3A656F)
internal val Teal80 = Color(0xFFA2CED9)
internal val Teal90 = Color(0xFFBEEAF6)
internal val AndroidOrange = Color(0xFFFF8B5E)
internal val AndroidBlue = Color(0xFF1769E0)

@ -0,0 +1,107 @@
/*
* 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.designsystem.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
/**
* A class to model icon tint color values for Now in Android.
*/
@Immutable
data class IconTintColors(
val primary: Color = Color.Unspecified,
val secondary: Color = Color.Unspecified,
val tertiary: Color = Color.Unspecified
)
/**
* A composition local for [GradientColors].
*/
val LocalIconTintColors = staticCompositionLocalOf { IconTintColors() }
fun Modifier.niaIconTint(brush: Brush) = then(
Modifier
.graphicsLayer(alpha = 0.99f)
.drawWithCache {
onDrawWithContent {
drawContent()
drawRect(brush, blendMode = BlendMode.SrcAtop)
}
}
)
enum class NiaIconTint {
PRIMARY {
@Composable
override fun color(): Color {
return LocalIconTintColors.current.primary
}
},
SECONDARY {
@Composable
override fun color(): Color {
return LocalIconTintColors.current.secondary
}
},
TERTIARY {
@Composable
override fun color(): Color {
return LocalIconTintColors.current.tertiary
}
},
PRIMARY_SECONDARY {
@Composable
override fun brush(): Brush {
val iconTintColors = LocalIconTintColors.current
return Brush.linearGradient(
colors = listOf(iconTintColors.primary, iconTintColors.secondary),
start = Offset(x = Offset.Infinite.x, y = Offset.Zero.y),
end = Offset(x = Offset.Zero.x, y = Offset.Infinite.y),
)
}
},
PRIMARY_TERTIARY {
@Composable
override fun brush(): Brush {
val iconTintColors = LocalIconTintColors.current
return Brush.linearGradient(
colors = listOf(iconTintColors.primary, iconTintColors.tertiary),
start = Offset(x = Offset.Infinite.x, y = Offset.Zero.y),
end = Offset(x = Offset.Zero.x, y = Offset.Infinite.y),
)
}
};
@Composable
open fun brush(): Brush {
throw NotImplementedError("This NiaIconTint does not provide a brush. Look at color instead.")
}
@Composable
open fun color(): Color {
throw NotImplementedError("This NiaIconTint does not provide a color. Look at brush instead.")
}
}

@ -16,7 +16,9 @@
package com.google.samples.apps.nowinandroid.core.designsystem.theme
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@ -160,6 +162,54 @@ val LightDefaultGradientColors = GradientColors(
neutral = DarkPurpleGray95
)
/**
* Light dynamic gradient colors
*/
@RequiresApi(Build.VERSION_CODES.S)
fun lightDynamicIconTintColors(context: Context) = IconTintColors(
primary = Color(context.resources.getColor(android.R.color.system_accent1_200, context.theme)),
secondary = Color(
context.resources.getColor(
android.R.color.system_accent2_200,
context.theme
)
),
tertiary = Color(context.resources.getColor(android.R.color.system_accent3_200, context.theme))
)
/**
* Dark dynamic gradient colors
*/
@RequiresApi(Build.VERSION_CODES.S)
fun darkDynamicIconTintColors(context: Context) = IconTintColors(
primary = Color(context.resources.getColor(android.R.color.system_accent1_600, context.theme)),
secondary = Color(
context.resources.getColor(
android.R.color.system_accent2_600,
context.theme
)
),
tertiary = Color(context.resources.getColor(android.R.color.system_accent3_600, context.theme))
)
val LightDefaultIconTintColors = IconTintColors(
primary = Purple80,
secondary = Orange80,
tertiary = Blue80
)
val DarkDefaultIconTintColors = IconTintColors(
primary = Purple40,
secondary = Orange40,
tertiary = Blue40
)
val AndroidIconTintColors = IconTintColors(
primary = Green80,
secondary = Orange80,
tertiary = Blue80
)
/**
* Light Android background theme
*/
@ -184,7 +234,7 @@ val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
@Composable
fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
dynamicColor: Boolean = true,
androidTheme: Boolean = false,
content: @Composable() () -> Unit
) {
@ -201,6 +251,15 @@ fun NiaTheme(
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
val iconTintColors = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) darkDynamicIconTintColors(context)
else lightDynamicIconTintColors(context)
}
androidTheme -> AndroidIconTintColors
else -> if (darkTheme) DarkDefaultIconTintColors else LightDefaultIconTintColors
}
val defaultGradientColors = GradientColors()
val gradientColors = when {
dynamicColor -> {
@ -225,6 +284,7 @@ fun NiaTheme(
}
CompositionLocalProvider(
LocalIconTintColors provides iconTintColors,
LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme
) {

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@ -27,27 +28,37 @@ 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.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.NiaIconTint
import com.google.samples.apps.nowinandroid.core.designsystem.theme.niaIconTint
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@Composable
@ -70,51 +81,103 @@ fun BookmarksScreen(
removeFromBookmarks: (String) -> Unit,
modifier: Modifier = Modifier
) {
NiaGradientBackground {
Scaffold(
topBar = {
NiaTopAppBar(
titleRes = R.string.top_app_bar_title_saved,
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_action_search
),
actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource(
id = R.string.top_app_bar_action_menu
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
)
)
},
Scaffold(
topBar = { BookmarksTopAppBar() },
containerColor = Color.Transparent,
modifier = modifier
) { innerPadding ->
val modifierWithPadding = Modifier
.padding(innerPadding)
.consumedWindowInsets(innerPadding)
if (feedState is Success && feedState.feed.isEmpty()) {
EmptyState(modifierWithPadding)
} else {
LoadingAndContent(
feedState,
removeFromBookmarks,
modifierWithPadding
)
}
}
}
@Composable
fun BookmarksTopAppBar() {
NiaTopAppBar(
titleRes = R.string.top_app_bar_title_saved,
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_action_search
),
actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource(
id = R.string.top_app_bar_action_menu
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
) { innerPadding ->
LazyVerticalGrid(
columns = Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = modifier
.fillMaxSize()
.testTag("bookmarks:feed")
.padding(innerPadding)
.consumedWindowInsets(innerPadding)
) {
newsFeed(
feedState = feedState,
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
showLoadingUIIfLoading = true,
loadingContentDescription = R.string.saved_loading
)
),
modifier = Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
)
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
}
@Composable
private fun EmptyState(
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxSize()
) {
val brush = NiaIconTint.PRIMARY_TERTIARY.brush()
Icon(
modifier = Modifier.niaIconTint(brush),
imageVector = ImageVector.vectorResource(R.drawable.bookmarks),
contentDescription = null,
)
Text(
text = stringResource(R.string.empty_state_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.widthIn(max = 192.dp),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.empty_state_body),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.widthIn(max = 192.dp),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun LoadingAndContent(
feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = modifier
.fillMaxSize()
.testTag("bookmarks:feed")
) {
newsFeed(
feedState = feedState,
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
showLoadingUIIfLoading = true,
loadingContentDescription = R.string.saved_loading
)
item(span = { GridItemSpan(maxLineSpan) }) {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
}

@ -0,0 +1,28 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="64"
android:viewportHeight="64">
<group>
<path
android:fillColor="#FFFFFF"
android:pathData="M4,14C4,11.239 6.239,9 9,9H45C47.761,9 50,11.239 50,14V55.855C50,60.008 45.23,62.351 41.943,59.812L28.834,49.684C27.754,48.849 26.246,48.849 25.166,49.684L12.057,59.812C8.77,62.351 4,60.008 4,55.855V14ZM9,11C7.343,11 6,12.343 6,14V55.855C6,58.347 8.862,59.752 10.834,58.229L23.943,48.101C25.744,46.71 28.256,46.71 30.057,48.101L43.166,58.229C45.138,59.752 48,58.347 48,55.855V14C48,12.343 46.657,11 45,11H9ZM15,2C15,1.448 15.448,1 16,1H55C57.761,1 60,3.239 60,6V52C60,52.552 59.552,53 59,53C58.448,53 58,52.552 58,52V6C58,4.343 56.657,3 55,3H16C15.448,3 15,2.552 15,2Z" />
</group>
</vector>

@ -20,4 +20,6 @@
<string name="top_app_bar_title_saved">Saved</string>
<string name="top_app_bar_action_search">Search</string>
<string name="top_app_bar_action_menu">Menu</string>
<string name="empty_state_title">No saved updates</string>
<string name="empty_state_body">Updates you save will be stored here to read later</string>
</resources>
Loading…
Cancel
Save