Improve design system usage and add lint checks

Change-Id: I05f9400a9410e31bdf1534e458d560fe793b809f
pull/148/head^2
Nick Rout 3 years ago
parent 0a3b3af2ed
commit d05b573f52

@ -37,6 +37,7 @@ android {
dependencies { dependencies {
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-designsystem"))
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.flowlayout)

@ -37,20 +37,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.google.samples.apps.nowinandroid.core.ui.component.NiaDropdownMenuButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaDropdownMenuButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilledButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.ui.component.NiaNavigationBar import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
import com.google.samples.apps.nowinandroid.core.ui.component.NiaNavigationBarItem import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
import com.google.samples.apps.nowinandroid.core.ui.component.NiaOutlinedButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOutlinedButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTab
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRow
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTextButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.ui.component.NiaViewToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaViewToggleButton
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
/** /**
* Now in Android component catalog. * Now in Android component catalog.
@ -416,22 +416,22 @@ fun NiaCatalog() {
FlowRow(mainAxisSpacing = 16.dp) { FlowRow(mainAxisSpacing = 16.dp) {
var firstChecked by remember { mutableStateOf(false) } var firstChecked by remember { mutableStateOf(false) }
NiaFilterChip( NiaFilterChip(
checked = firstChecked, selected = firstChecked,
onCheckedChange = { checked -> firstChecked = checked }, onSelectedChange = { checked -> firstChecked = checked },
text = { Text(text = "Enabled".uppercase()) } label = { Text(text = "Enabled".uppercase()) }
) )
var secondChecked by remember { mutableStateOf(true) } var secondChecked by remember { mutableStateOf(true) }
NiaFilterChip( NiaFilterChip(
checked = secondChecked, selected = secondChecked,
onCheckedChange = { checked -> secondChecked = checked }, onSelectedChange = { checked -> secondChecked = checked },
text = { Text(text = "Enabled".uppercase()) } label = { Text(text = "Enabled".uppercase()) }
) )
var thirdChecked by remember { mutableStateOf(true) } var thirdChecked by remember { mutableStateOf(true) }
NiaFilterChip( NiaFilterChip(
checked = thirdChecked, selected = thirdChecked,
onCheckedChange = { checked -> thirdChecked = checked }, onSelectedChange = { checked -> thirdChecked = checked },
enabled = false, enabled = false,
text = { Text(text = "Disabled".uppercase()) } label = { Text(text = "Disabled".uppercase()) }
) )
} }
} }

@ -100,6 +100,7 @@ dependencies {
implementation(project(":feature-topic")) implementation(project(":feature-topic"))
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-designsystem"))
implementation(project(":core-navigation")) implementation(project(":core-navigation"))
implementation(project(":sync")) implementation(project(":sync"))

@ -16,14 +16,12 @@
package com.google.samples.apps.nowinandroid.navigation package com.google.samples.apps.nowinandroid.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Grid3x3
import androidx.compose.material.icons.filled.Upcoming
import androidx.compose.material.icons.outlined.Grid3x3
import androidx.compose.material.icons.outlined.Upcoming
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests
@ -59,22 +57,22 @@ class NiaTopLevelNavigation(private val navController: NavHostController) {
data class TopLevelDestination( data class TopLevelDestination(
val route: String, val route: String,
val selectedIcon: ImageVector, val selectedIcon: Icon,
val unselectedIcon: ImageVector, val unselectedIcon: Icon,
val iconTextId: Int val iconTextId: Int
) )
val TOP_LEVEL_DESTINATIONS = listOf( val TOP_LEVEL_DESTINATIONS = listOf(
TopLevelDestination( TopLevelDestination(
route = ForYouDestination.route, route = ForYouDestination.route,
selectedIcon = Icons.Filled.Upcoming, selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = Icons.Outlined.Upcoming, unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = for_you iconTextId = for_you
), ),
TopLevelDestination( TopLevelDestination(
route = InterestsDestination.route, route = InterestsDestination.route,
selectedIcon = Icons.Filled.Grid3x3, selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = Icons.Outlined.Grid3x3, unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interests iconTextId = interests
) )
) )

@ -30,10 +30,6 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -44,14 +40,20 @@ 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.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme 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.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS
@ -119,18 +121,29 @@ private fun NiaNavRail(
currentDestination: NavDestination?, currentDestination: NavDestination?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
NavigationRail(modifier = modifier) { NiaNavigationRail(modifier = modifier) {
TOP_LEVEL_DESTINATIONS.forEach { destination -> TOP_LEVEL_DESTINATIONS.forEach { destination ->
val selected = val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NavigationRailItem( NiaNavigationRailItem(
selected = selected, selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) }, onClick = { onNavigateToTopLevelDestination(destination) },
icon = { icon = {
Icon( val icon = if (selected) {
if (selected) destination.selectedIcon else destination.unselectedIcon, destination.selectedIcon
contentDescription = null } else {
) destination.unselectedIcon
}
when (icon) {
is ImageVectorIcon -> Icon(
imageVector = icon.imageVector,
contentDescription = null
)
is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id),
contentDescription = null
)
}
}, },
label = { Text(stringResource(destination.iconTextId)) } label = { Text(stringResource(destination.iconTextId)) }
) )
@ -146,30 +159,36 @@ private fun NiaBottomBar(
// Wrap the navigation bar in a surface so the color behind the system // Wrap the navigation bar in a surface so the color behind the system
// navigation is equal to the container color of the navigation bar. // navigation is equal to the container color of the navigation bar.
Surface(color = MaterialTheme.colorScheme.surface) { Surface(color = MaterialTheme.colorScheme.surface) {
NavigationBar( NiaNavigationBar(
modifier = Modifier.windowInsetsPadding( modifier = Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only( WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
) )
), )
tonalElevation = 0.dp
) { ) {
TOP_LEVEL_DESTINATIONS.forEach { destination -> TOP_LEVEL_DESTINATIONS.forEach { destination ->
val selected = val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem( NiaNavigationBarItem(
selected = selected, selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) }, onClick = { onNavigateToTopLevelDestination(destination) },
icon = { icon = {
Icon( val icon = if (selected) {
if (selected) { destination.selectedIcon
destination.selectedIcon } else {
} else { destination.unselectedIcon
destination.unselectedIcon }
}, when (icon) {
contentDescription = null is ImageVectorIcon -> Icon(
) imageVector = icon.imageVector,
contentDescription = null
)
is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id),
contentDescription = null
)
}
}, },
label = { Text(stringResource(destination.iconTextId)) } label = { Text(stringResource(destination.iconTextId)) }
) )

@ -42,6 +42,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
dependencies { dependencies {
add("implementation", project(":core-model")) add("implementation", project(":core-model"))
add("implementation", project(":core-ui")) add("implementation", project(":core-ui"))
add("implementation", project(":core-designsystem"))
add("implementation", project(":core-data")) add("implementation", project(":core-data"))
add("implementation", project(":core-common")) add("implementation", project(":core-common"))
add("implementation", project(":core-navigation")) add("implementation", project(":core-navigation"))

@ -0,0 +1 @@
/build

@ -0,0 +1,44 @@
/*
* 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.
*/
plugins {
id("nowinandroid.android.library")
id("nowinandroid.android.library.compose")
id("nowinandroid.android.library.jacoco")
id("nowinandroid.spotless")
}
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
lint {
checkDependencies = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
api(libs.androidx.compose.foundation)
api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended)
api(libs.androidx.compose.material3)
debugApi(libs.androidx.compose.ui.tooling)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.ui.util)
api(libs.androidx.compose.runtime)
lintPublish(project(":lint"))
androidTestImplementation(project(":core-testing"))
}

@ -0,0 +1,276 @@
/*
* 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
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.theme.BackgroundTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkAndroidBackgroundTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkAndroidColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkDefaultColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightAndroidBackgroundTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightAndroidColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightDefaultColorScheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightDefaultGradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalBackgroundTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
/**
* Tests [NiaTheme] using different combinations of the theme mode parameters:
* darkTheme, dynamicColor, and androidTheme.
*
* It verifies that the various composition locals [MaterialTheme], [LocalGradientColors] and
* [LocalBackgroundTheme] have the expected values for a given theme mode, as specified by the
* design system.
*/
class ThemeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun darkThemeFalse_dynamicColorFalse_androidThemeFalse() {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = false,
androidTheme = false
) {
val colorScheme = LightDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = LightDefaultGradientColors
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeTrue_dynamicColorFalse_androidThemeFalse() {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = false,
androidTheme = false
) {
val colorScheme = DarkDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeFalse_dynamicColorTrue_androidThemeFalse() {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = true,
androidTheme = false
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(LocalContext.current)
} else {
LightDefaultColorScheme
}
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
GradientColors()
} else {
LightDefaultGradientColors
}
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeTrue_dynamicColorTrue_androidThemeFalse() {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = true,
androidTheme = false
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicDarkColorScheme(LocalContext.current)
} else {
DarkDefaultColorScheme
}
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeFalse_dynamicColorFalse_androidThemeTrue() {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = false,
androidTheme = true
) {
val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeTrue_dynamicColorFalse_androidThemeTrue() {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = false,
androidTheme = true
) {
val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeFalse_dynamicColorTrue_androidThemeTrue() {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = true,
androidTheme = true
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(LocalContext.current)
} else {
LightDefaultColorScheme
}
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
GradientColors()
} else {
LightDefaultGradientColors
}
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
@Test
fun darkThemeTrue_dynamicColorTrue_androidThemeTrue() {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = true,
androidTheme = true
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicDarkColorScheme(LocalContext.current)
} else {
DarkDefaultColorScheme
}
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
}
/**
* Workaround for the fact that the NiA design system specify all color scheme values.
*/
private fun assertColorSchemesEqual(
expectedColorScheme: ColorScheme,
actualColorScheme: ColorScheme
) {
assertEquals(expectedColorScheme.primary, actualColorScheme.primary)
assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary)
assertEquals(expectedColorScheme.primaryContainer, actualColorScheme.primaryContainer)
assertEquals(expectedColorScheme.onPrimaryContainer, actualColorScheme.onPrimaryContainer)
assertEquals(expectedColorScheme.secondary, actualColorScheme.secondary)
assertEquals(expectedColorScheme.onSecondary, actualColorScheme.onSecondary)
assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer)
assertEquals(
expectedColorScheme.onSecondaryContainer,
actualColorScheme.onSecondaryContainer
)
assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary)
assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary)
assertEquals(expectedColorScheme.tertiaryContainer, actualColorScheme.tertiaryContainer)
assertEquals(expectedColorScheme.onTertiaryContainer, actualColorScheme.onTertiaryContainer)
assertEquals(expectedColorScheme.error, actualColorScheme.error)
assertEquals(expectedColorScheme.onError, actualColorScheme.onError)
assertEquals(expectedColorScheme.errorContainer, actualColorScheme.errorContainer)
assertEquals(expectedColorScheme.onErrorContainer, actualColorScheme.onErrorContainer)
assertEquals(expectedColorScheme.background, actualColorScheme.background)
assertEquals(expectedColorScheme.onBackground, actualColorScheme.onBackground)
assertEquals(expectedColorScheme.surface, actualColorScheme.surface)
assertEquals(expectedColorScheme.onSurface, actualColorScheme.onSurface)
assertEquals(expectedColorScheme.surfaceVariant, actualColorScheme.surfaceVariant)
assertEquals(expectedColorScheme.onSurfaceVariant, actualColorScheme.onSurfaceVariant)
assertEquals(expectedColorScheme.outline, actualColorScheme.outline)
}
}

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.samples.apps.nowinandroid.core.designsystem">
</manifest>

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -34,8 +34,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.designsystem.theme.LocalBackgroundTheme
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import kotlin.math.tan import kotlin.math.tan
/** /**
@ -75,8 +76,8 @@ fun NiaBackground(
@Composable @Composable
fun NiaGradientBackground( fun NiaGradientBackground(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topColor: Color = LocalBackgroundTheme.current.primaryGradientColor, topColor: Color = LocalGradientColors.current.primary,
bottomColor: Color = LocalBackgroundTheme.current.secondaryGradientColor, bottomColor: Color = LocalGradientColors.current.secondary,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val currentTopColor by rememberUpdatedState(topColor) val currentTopColor by rememberUpdatedState(topColor)

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -63,9 +63,7 @@ fun NiaFilledButton(
Button( Button(
onClick = onClick, onClick = onClick,
modifier = if (small) { modifier = if (small) {
Modifier modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
} else { } else {
modifier modifier
}, },
@ -154,9 +152,7 @@ fun NiaOutlinedButton(
OutlinedButton( OutlinedButton(
onClick = onClick, onClick = onClick,
modifier = if (small) { modifier = if (small) {
Modifier modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
} else { } else {
modifier modifier
}, },
@ -247,9 +243,7 @@ fun NiaTextButton(
TextButton( TextButton(
onClick = onClick, onClick = onClick,
modifier = if (small) { modifier = if (small) {
Modifier modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
} else { } else {
modifier modifier
}, },

@ -0,0 +1,111 @@
/*
* 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.component
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Shapes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
/**
* Now in Android filter chip with included leading checked icon as well as text content slot.
*
* @param selected Whether the chip is currently checked.
* @param onSelectedChange Called when the user clicks the chip and toggles checked.
* @param modifier Modifier to be applied to the chip.
* @param enabled Controls the enabled state of the chip. When `false`, this chip will not be
* clickable and will appear disabled to accessibility services.
* @param label The text label content.
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun NiaFilterChip(
selected: Boolean,
onSelectedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable () -> Unit
) {
FilterChip(
selected = selected,
onClick = { onSelectedChange(!selected) },
label = {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
label()
}
},
modifier = modifier,
enabled = enabled,
selectedIcon = {
Icon(
imageVector = NiaIcons.Check,
contentDescription = null
)
},
shape = Shapes.Full,
border = FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.onBackground,
selectedBorderColor = MaterialTheme.colorScheme.onBackground,
disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha
),
disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha
),
borderWidth = NiaChipDefaults.ChipBorderWidth,
selectedBorderWidth = NiaChipDefaults.ChipBorderWidth
),
colors = FilterChipDefaults.filterChipColors(
containerColor = Color.Transparent,
labelColor = MaterialTheme.colorScheme.onBackground,
iconColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = if (selected) {
MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContainerAlpha
)
} else {
Color.Transparent
},
disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha
),
disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha
),
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onBackground,
selectedLeadingIconColor = MaterialTheme.colorScheme.onBackground
)
)
}
/**
* Now in Android chip default values.
*/
object NiaChipDefaults {
const val DisabledChipContainerAlpha = 0.12f
const val DisabledChipContentAlpha = 0.38f
val ChipBorderWidth = 1.dp
}

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
/** /**
* Now in Android dropdown menu button with included trailing icon as well as text label and item * Now in Android dropdown menu button with included trailing icon as well as text label and item

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui package com.google.samples.apps.nowinandroid.core.designsystem.component
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.animateColor import androidx.compose.animation.animateColor
@ -46,11 +46,11 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview 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.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun LoadingWheel( fun NiaLoadingWheel(
contentDesc: String, contentDesc: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -134,10 +134,10 @@ fun LoadingWheel(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
) )
@Composable @Composable
fun LoadingWheelPreview() { fun NiaLoadingWheelPreview() {
NiaTheme { NiaTheme {
Surface { Surface {
LoadingWheel(contentDesc = "LoadingWheel") NiaLoadingWheel(contentDesc = "LoadingWheel")
} }
} }
} }

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme

@ -14,16 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -39,9 +44,13 @@ import androidx.compose.ui.unit.dp
* @param enabled Controls the enabled state of the toggle button. When `false`, this toggle button * @param enabled Controls the enabled state of the toggle button. When `false`, this toggle button
* will not be clickable and will appear disabled to accessibility services. * will not be clickable and will appear disabled to accessibility services.
* @param icon The icon content to show when unchecked. * @param icon The icon content to show when unchecked.
* @param checkedBackgroundRadius The background radius that will be used to draw a background color
* behind the checkedIcon when this toggle button is checked.
* @param checkedIcon The icon content to show when checked. * @param checkedIcon The icon content to show when checked.
* @param size The size of the toggle button.
* @param iconSize The size of the icon.
* @param backgroundColor The background color when unchecked.
* @param checkedBackgroundColor The background color when checked.
* @param iconColor The icon color when unchecked.
* @param iconColor The icon color when checked.
*/ */
@Composable @Composable
fun NiaToggleButton( fun NiaToggleButton(
@ -50,33 +59,38 @@ fun NiaToggleButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
checkedBackgroundRadius: Dp = NiaToggleButtonDefaults.ToggleButtonSize / 2, checkedIcon: @Composable () -> Unit = icon,
checkedIcon: @Composable () -> Unit = icon size: Dp = NiaToggleButtonDefaults.ToggleButtonSize,
iconSize: Dp = NiaToggleButtonDefaults.ToggleButtonIconSize,
backgroundColor: Color = Color.Transparent,
checkedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer,
iconColor: Color = contentColorFor(backgroundColor),
checkedIconColor: Color = contentColorFor(checkedBackgroundColor)
) { ) {
val checkedColor = MaterialTheme.colorScheme.primaryContainer val radius = with(LocalDensity.current) { (size / 2).toPx() }
val checkedRadius = with(LocalDensity.current) {
checkedBackgroundRadius.toPx()
}
IconButton( IconButton(
onClick = { onCheckedChange(!checked) }, onClick = { onCheckedChange(!checked) },
modifier = Modifier modifier = modifier
.size(size)
.toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {}) .toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {})
.drawBehind { .drawBehind {
if (checked) drawCircle( drawCircle(
color = checkedColor, color = if (checked) checkedBackgroundColor else backgroundColor,
radius = checkedRadius radius = radius
) )
} },
.then(modifier),
enabled = enabled, enabled = enabled,
content = { content = {
Box( Box(
modifier = Modifier.sizeIn( modifier = Modifier.sizeIn(
maxWidth = NiaToggleButtonDefaults.ToggleButtonIconSize, maxWidth = iconSize,
maxHeight = NiaToggleButtonDefaults.ToggleButtonIconSize maxHeight = iconSize
) )
) { ) {
if (checked) checkedIcon() else icon() val contentColor = if (checked) checkedIconColor else iconColor
CompositionLocalProvider(LocalContentColor provides contentColor) {
if (checked) checkedIcon() else icon()
}
} }
} }
) )

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier
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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.google.samples.apps.nowinandroid.core.ui.R
@Composable @Composable
fun NiaTopAppBar( fun NiaTopAppBar(
@ -75,7 +74,7 @@ fun NiaTopAppBar(
@Composable @Composable
fun NiaTopAppBarPreview() { fun NiaTopAppBarPreview() {
NiaTopAppBar( NiaTopAppBar(
titleRes = R.string.top_app_bar_preview_title, titleRes = android.R.string.untitled,
navigationIcon = Icons.Default.Search, navigationIcon = Icons.Default.Search,
navigationIconContentDescription = "Navigation icon", navigationIconContentDescription = "Navigation icon",
actionIcon = Icons.Default.MoreVert, actionIcon = Icons.Default.MoreVert,

@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.component package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
/** /**
* Now in Android view toggle button with included trailing icon as well as compact and expanded * Now in Android view toggle button with included trailing icon as well as compact and expanded

@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.icon package com.google.samples.apps.nowinandroid.core.designsystem.icon
import androidx.annotation.DrawableRes
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.AccountCircle
@ -27,6 +28,8 @@ import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.ExpandLess import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.Fullscreen import androidx.compose.material.icons.rounded.Fullscreen
import androidx.compose.material.icons.rounded.Grid3x3
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.ShortText import androidx.compose.material.icons.rounded.ShortText
@ -35,7 +38,7 @@ import androidx.compose.material.icons.rounded.ViewDay
import androidx.compose.material.icons.rounded.VolumeOff import androidx.compose.material.icons.rounded.VolumeOff
import androidx.compose.material.icons.rounded.VolumeUp import androidx.compose.material.icons.rounded.VolumeUp
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.core.ui.R import com.google.samples.apps.nowinandroid.core.designsystem.R
/** /**
* Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs.
@ -54,9 +57,11 @@ object NiaIcons {
val Close = Icons.Rounded.Close val Close = Icons.Rounded.Close
val ExpandLess = Icons.Rounded.ExpandLess val ExpandLess = Icons.Rounded.ExpandLess
val Fullscreen = Icons.Rounded.Fullscreen val Fullscreen = Icons.Rounded.Fullscreen
val Grid3x3 = Icons.Rounded.Grid3x3
val MenuBook = R.drawable.ic_menu_book val MenuBook = R.drawable.ic_menu_book
val MenuBookBorder = R.drawable.ic_menu_book_border val MenuBookBorder = R.drawable.ic_menu_book_border
val MoreVert = Icons.Default.MoreVert val MoreVert = Icons.Default.MoreVert
val Person = Icons.Rounded.Person
val PlayArrow = Icons.Rounded.PlayArrow val PlayArrow = Icons.Rounded.PlayArrow
val Search = Icons.Rounded.Search val Search = Icons.Rounded.Search
val ShortText = Icons.Rounded.ShortText val ShortText = Icons.Rounded.ShortText
@ -67,3 +72,11 @@ object NiaIcons {
val VolumeOff = Icons.Rounded.VolumeOff val VolumeOff = Icons.Rounded.VolumeOff
val VolumeUp = Icons.Rounded.VolumeUp val VolumeUp = Icons.Rounded.VolumeUp
} }
/**
* A sealed class to make dealing with [ImageVector] and [DrawableRes] icons easier.
*/
sealed class Icon {
data class ImageVectorIcon(val imageVector: ImageVector) : Icon()
data class DrawableResourceIcon(@DrawableRes val id: Int) : Icon()
}

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.theme package com.google.samples.apps.nowinandroid.core.designsystem.theme
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
@ -22,17 +22,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
/** /**
* A class to model background values for Now in Android, * A class to model background color and tonal elevation values for Now in Android.
* including color, tonal elevation and gradient colors.
*/ */
@Immutable @Immutable
data class BackgroundTheme( data class BackgroundTheme(
val color: Color = Color.Unspecified, val color: Color = Color.Unspecified,
val tonalElevation: Dp = Dp.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
) )
/** /**

@ -0,0 +1,86 @@
/*
* 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.ui.graphics.Color
/**
* Now in Android colors.
*/
internal val Blue10 = Color(0xFF001F29)
internal val Blue20 = Color(0xFF003544)
internal val Blue30 = Color(0xFF004D61)
internal val Blue40 = Color(0xFF006781)
internal val Blue80 = Color(0xFF5DD4FB)
internal val Blue90 = Color(0xFFB5EAFF)
internal val Blue95 = Color(0xFFDCF5FF)
internal val DarkGreen10 = Color(0xFF0D1F12)
internal val DarkGreen20 = Color(0xFF223526)
internal val DarkGreen30 = Color(0xFF394B3C)
internal val DarkGreen40 = Color(0xFF4F6352)
internal val DarkGreen80 = Color(0xFFB7CCB8)
internal val DarkGreen90 = Color(0xFFD3E8D3)
internal val DarkGreenGray10 = Color(0xFF1A1C1A)
internal val DarkGreenGray90 = Color(0xFFE2E3DE)
internal val DarkGreenGray95 = Color(0xFFF0F1EC)
internal val DarkGreenGray99 = Color(0xFFFBFDF7)
internal val DarkPurpleGray10 = Color(0xFF201A1B)
internal val DarkPurpleGray90 = Color(0xFFECDFE0)
internal val DarkPurpleGray95 = Color(0xFFFAEEEF)
internal val DarkPurpleGray99 = Color(0xFFFCFCFC)
internal val Green10 = Color(0xFF00210B)
internal val Green20 = Color(0xFF003919)
internal val Green30 = Color(0xFF005227)
internal val Green40 = Color(0xFF006D36)
internal val Green80 = Color(0xFF0EE37C)
internal val Green90 = Color(0xFF5AFF9D)
internal val GreenGray30 = Color(0xFF414941)
internal val GreenGray50 = Color(0xFF727971)
internal val GreenGray60 = Color(0xFF8B938A)
internal val GreenGray80 = Color(0xFFC1C9BF)
internal val GreenGray90 = Color(0xFFDDE5DB)
internal val Orange10 = Color(0xFF390C00)
internal val Orange20 = Color(0xFF5D1900)
internal val Orange30 = Color(0xFF812800)
internal val Orange40 = Color(0xFFA23F16)
internal val Orange80 = Color(0xFFFFB599)
internal val Orange90 = Color(0xFFFFDBCE)
internal val Orange95 = Color(0xFFFFEDE6)
internal val Purple10 = Color(0xFF36003D)
internal val Purple20 = Color(0xFF560A5E)
internal val Purple30 = Color(0xFF702776)
internal val Purple40 = Color(0xFF8C4190)
internal val Purple80 = Color(0xFFFFA8FF)
internal val Purple90 = Color(0xFFFFD5FC)
internal val Purple95 = Color(0xFFFFEBFB)
internal val PurpleGray30 = Color(0xFF4E444C)
internal val PurpleGray50 = Color(0xFF7F747C)
internal val PurpleGray60 = Color(0xFF998D96)
internal val PurpleGray80 = Color(0xFFD0C2CC)
internal val PurpleGray90 = Color(0xFFEDDEE8)
internal val Red10 = Color(0xFF410001)
internal val Red20 = Color(0xFF680003)
internal val Red30 = Color(0xFF930006)
internal val Red40 = Color(0xFFBA1B1B)
internal val Red80 = Color(0xFFFFB4A9)
internal val Red90 = Color(0xFFFFDAD4)
internal val Teal10 = Color(0xFF001F26)
internal val Teal20 = Color(0xFF02363F)
internal val Teal30 = Color(0xFF214D56)
internal val Teal40 = Color(0xFF3A656F)
internal val Teal80 = Color(0xFFA2CED9)
internal val Teal90 = Color(0xFFBEEAF6)

@ -0,0 +1,37 @@
/*
* 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.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
/**
* A class to model gradient color values for Now in Android.
*/
@Immutable
data class GradientColors(
val primary: Color = Color.Unspecified,
val secondary: Color = Color.Unspecified,
val tertiary: Color = Color.Unspecified,
val neutral: Color = Color.Unspecified
)
/**
* A composition local for [GradientColors].
*/
val LocalGradientColors = staticCompositionLocalOf { GradientColors() }

@ -14,9 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.theme package com.google.samples.apps.nowinandroid.core.designsystem.theme
import android.os.Build import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
@ -32,7 +33,8 @@ import androidx.compose.ui.unit.dp
/** /**
* Light default theme color scheme * Light default theme color scheme
*/ */
private val LightDefaultColorScheme = lightColorScheme( @VisibleForTesting
val LightDefaultColorScheme = lightColorScheme(
primary = Purple40, primary = Purple40,
onPrimary = Color.White, onPrimary = Color.White,
primaryContainer = Purple90, primaryContainer = Purple90,
@ -61,7 +63,8 @@ private val LightDefaultColorScheme = lightColorScheme(
/** /**
* Dark default theme color scheme * Dark default theme color scheme
*/ */
private val DarkDefaultColorScheme = darkColorScheme( @VisibleForTesting
val DarkDefaultColorScheme = darkColorScheme(
primary = Purple80, primary = Purple80,
onPrimary = Purple20, onPrimary = Purple20,
primaryContainer = Purple30, primaryContainer = Purple30,
@ -90,7 +93,8 @@ private val DarkDefaultColorScheme = darkColorScheme(
/** /**
* Light Android theme color scheme * Light Android theme color scheme
*/ */
private val LightAndroidColorScheme = lightColorScheme( @VisibleForTesting
val LightAndroidColorScheme = lightColorScheme(
primary = Green40, primary = Green40,
onPrimary = Color.White, onPrimary = Color.White,
primaryContainer = Green90, primaryContainer = Green90,
@ -119,7 +123,8 @@ private val LightAndroidColorScheme = lightColorScheme(
/** /**
* Dark Android theme color scheme * Dark Android theme color scheme
*/ */
private val DarkAndroidColorScheme = darkColorScheme( @VisibleForTesting
val DarkAndroidColorScheme = darkColorScheme(
primary = Green80, primary = Green80,
onPrimary = Green20, onPrimary = Green20,
primaryContainer = Green30, primaryContainer = Green30,
@ -145,6 +150,26 @@ private val DarkAndroidColorScheme = darkColorScheme(
outline = GreenGray60 outline = GreenGray60
) )
/**
* Light default gradient colors
*/
val LightDefaultGradientColors = GradientColors(
primary = Purple95,
secondary = Orange95,
tertiary = Blue95,
neutral = DarkPurpleGray95
)
/**
* Light Android background theme
*/
val LightAndroidBackgroundTheme = BackgroundTheme(color = DarkGreenGray95)
/**
* Dark Android background theme
*/
val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
/** /**
* Now in Android theme. * Now in Android theme.
* *
@ -164,45 +189,45 @@ fun NiaTheme(
content: @Composable() () -> Unit content: @Composable() () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor -> {
val context = LocalContext.current if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
} }
androidTheme && darkTheme -> DarkAndroidColorScheme androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
androidTheme -> LightAndroidColorScheme else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
darkTheme -> DarkDefaultColorScheme
else -> LightDefaultColorScheme
} }
val defaultGradientColors = GradientColors()
val gradientColors = when {
dynamicColor -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
defaultGradientColors
} else {
if (darkTheme) defaultGradientColors else LightDefaultGradientColors
}
}
androidTheme -> defaultGradientColors
else -> if (darkTheme) defaultGradientColors else LightDefaultGradientColors
}
val defaultBackgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
val backgroundTheme = when { val backgroundTheme = when {
androidTheme && darkTheme -> BackgroundTheme( dynamicColor -> defaultBackgroundTheme
color = Color.Black androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
) else -> defaultBackgroundTheme
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) {
CompositionLocalProvider(
LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme
) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = NiaTypography, typography = NiaTypography,

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.ui.theme package com.google.samples.apps.nowinandroid.core.designsystem.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -27,7 +27,7 @@ import androidx.compose.ui.unit.sp
* *
* TODO: Add custom font * TODO: Add custom font
*/ */
val NiaTypography = Typography( internal val NiaTypography = Typography(
displayLarge = TextStyle( displayLarge = TextStyle(
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 57.sp, fontSize = 57.sp,

@ -21,6 +21,7 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":core-designsystem"))
implementation(project(":core-model")) implementation(project(":core-model"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)

@ -1,106 +0,0 @@
/*
* 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
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun FollowButton(
following: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onFollowChange: ((Boolean) -> Unit)? = null,
backgroundColor: Color = Color.Transparent,
size: Dp = 32.dp,
iconSize: Dp = size / 2,
followingContentDescription: String? = null,
notFollowingContentDescription: String? = null,
) {
val background = if (following) {
MaterialTheme.colorScheme.secondaryContainer
} else {
backgroundColor
}
Box(
modifier = modifier.followButton(onFollowChange, following, enabled, background, size),
contentAlignment = Alignment.Center
) {
if (following) {
Icon(
imageVector = Filled.Done,
contentDescription = followingContentDescription,
modifier = Modifier.size(iconSize)
)
} else {
Icon(
imageVector = Filled.Add,
contentDescription = notFollowingContentDescription,
modifier = Modifier.size(iconSize)
)
}
}
}
private fun Modifier.followButton(
onFollowChange: ((Boolean) -> Unit)?,
following: Boolean,
enabled: Boolean,
background: Color,
size: Dp
): Modifier = composed {
val boxModifier = if (onFollowChange != null) {
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = 24.dp)
this
.toggleable(
value = following,
onValueChange = onFollowChange,
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
indication = ripple
)
} else {
this
}
boxModifier
.clip(CircleShape)
.background(background)
.size(size)
}

@ -28,15 +28,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -61,11 +56,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
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.theme.NiaTheme
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@ -184,7 +181,7 @@ fun NewsResourceAuthors(
modifier = authorImageModifier modifier = authorImageModifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(4.dp), .padding(4.dp),
imageVector = Icons.Filled.Person, imageVector = NiaIcons.Person,
contentDescription = null // decorative image contentDescription = null // decorative image
) )
} }
@ -208,23 +205,23 @@ fun BookmarkButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val clickActionLabel = stringResource( NiaToggleButton(
if (isBookmarked) R.string.unbookmark else R.string.bookmark
)
IconToggleButton(
checked = isBookmarked, checked = isBookmarked,
onCheckedChange = { onClick() }, onCheckedChange = { onClick() },
modifier = modifier.semantics { modifier = modifier,
// Use custom label for accessibility services to communicate button's action to user. icon = {
// Pass null for action to only override the label and not the actual action. Icon(
this.onClick(label = clickActionLabel, action = null) painter = painterResource(NiaIcons.BookmarkBorder),
contentDescription = stringResource(R.string.bookmark)
)
},
checkedIcon = {
Icon(
painter = painterResource(NiaIcons.Bookmark),
contentDescription = stringResource(R.string.unbookmark)
)
} }
) { )
Icon(
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Filled.BookmarkBorder,
contentDescription = null // handled by click label of parent
)
}
} }
@Composable @Composable

@ -1,90 +0,0 @@
/*
* 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 androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
/**
* Now in Android filter chip with included leading checked icon as well as text content slot.
*
* @param checked Whether the chip is currently checked.
* @param onCheckedChange Called when the user clicks the chip and toggles checked.
* @param modifier Modifier to be applied to the chip.
* @param enabled Controls the enabled state of the chip. When `false`, this chip will not be
* clickable and will appear disabled to accessibility services.
* @param text The text label content.
*/
@Composable
fun NiaFilterChip(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
text: @Composable () -> Unit
) {
// TODO: Replace with Chip when available in Compose Material 3: b/197399111
NiaOutlinedButton(
onClick = { onCheckedChange(!checked) },
modifier = Modifier
.toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {})
.then(modifier),
enabled = enabled,
small = true,
border = NiaButtonDefaults.outlinedButtonBorder(
enabled = enabled,
disabledColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = if (checked) {
NiaButtonDefaults.DisabledButtonContentAlpha
} else {
NiaButtonDefaults.DisabledButtonContainerAlpha
}
)
),
colors = NiaButtonDefaults.outlinedButtonColors(
containerColor = if (checked) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Transparent
},
disabledContainerColor = if (checked) {
MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaButtonDefaults.DisabledButtonContainerAlpha
)
} else {
Color.Transparent
}
),
text = text,
leadingIcon = if (checked) {
{
Icon(
imageVector = NiaIcons.Check,
contentDescription = null
)
}
} else {
null
}
)
}

@ -1,108 +0,0 @@
/*
* 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.ui.graphics.Color
import androidx.core.graphics.ColorUtils
import kotlin.math.roundToInt
/**
* Now in Android colors.
*/
val Blue10 = Color(0xFF001F29)
val Blue20 = Color(0xFF003544)
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)
val DarkGreen40 = Color(0xFF4F6352)
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)
val Green30 = Color(0xFF005227)
val Green40 = Color(0xFF006D36)
val Green80 = Color(0xFF0EE37C)
val Green90 = Color(0xFF5AFF9D)
val GreenGray30 = Color(0xFF414941)
val GreenGray50 = Color(0xFF727971)
val GreenGray60 = Color(0xFF8B938A)
val GreenGray80 = Color(0xFFC1C9BF)
val GreenGray90 = Color(0xFFDDE5DB)
val Orange10 = Color(0xFF390C00)
val Orange20 = Color(0xFF5D1900)
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)
val PurpleGray80 = Color(0xFFD0C2CC)
val PurpleGray90 = Color(0xFFEDDEE8)
val Red10 = Color(0xFF410001)
val Red20 = Color(0xFF680003)
val Red30 = Color(0xFF930006)
val Red40 = Color(0xFFBA1B1B)
val Red80 = Color(0xFFFFB4A9)
val Red90 = Color(0xFFFFDAD4)
val Teal10 = Color(0xFF001F26)
val Teal20 = Color(0xFF02363F)
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
}

@ -20,6 +20,4 @@
<string name="back">Back</string> <string name="back">Back</string>
<string name="card_tap_action">Open Resource Link</string> <string name="card_tap_action">Open Resource Link</string>
<string name="top_app_bar_preview_title">Title</string>
</resources> </resources>

@ -50,15 +50,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
@Composable @Composable
fun AuthorRoute( fun AuthorRoute(
@ -102,7 +102,7 @@ internal fun AuthorScreen(
when (authorState) { when (authorState) {
AuthorUiState.Loading -> { AuthorUiState.Loading -> {
item { item {
LoadingWheel( NiaLoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.author_loading), contentDesc = stringResource(id = R.string.author_loading),
) )
@ -186,7 +186,7 @@ private fun LazyListScope.authorCards(news: NewsUiState) {
) )
} }
is NewsUiState.Loading -> item { is NewsUiState.Loading -> item {
LoadingWheel(contentDesc = "Loading news") // TODO NiaLoadingWheel(contentDesc = "Loading news") // TODO
} }
else -> item { else -> item {
Text("Error") // TODO Text("Error") // TODO
@ -217,8 +217,8 @@ private fun AuthorToolbar(
val selected = uiState.isFollowed val selected = uiState.isFollowed
NiaFilterChip( NiaFilterChip(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
checked = selected, selected = selected,
onCheckedChange = onFollowClick, onSelectedChange = onFollowClick,
) { ) {
if (selected) { if (selected) {
Text(stringResource(id = R.string.author_following)) Text(stringResource(id = R.string.author_following))

@ -32,8 +32,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -54,9 +52,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.ui.FollowButton
@Composable @Composable
fun AuthorsCarousel( fun AuthorsCarousel(
@ -121,7 +121,7 @@ fun AuthorItem(
modifier = authorImageModifier modifier = authorImageModifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(4.dp), .padding(4.dp),
imageVector = Icons.Filled.Person, imageVector = NiaIcons.Person,
contentDescription = null // decorative image contentDescription = null // decorative image
) )
} else { } else {
@ -132,12 +132,24 @@ fun AuthorItem(
contentDescription = null contentDescription = null
) )
} }
FollowButton( NiaToggleButton(
following = following, checked = following,
backgroundColor = MaterialTheme.colorScheme.surface, onCheckedChange = onAuthorClick,
size = 20.dp, modifier = Modifier.align(Alignment.BottomEnd),
iconSize = 14.dp, icon = {
modifier = Modifier.align(Alignment.BottomEnd) Icon(
imageVector = NiaIcons.Add,
contentDescription = null
)
},
checkedIcon = {
Icon(
imageVector = NiaIcons.Check,
contentDescription = null
)
},
size = 24.dp,
backgroundColor = MaterialTheme.colorScheme.surface
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@ -155,7 +167,7 @@ fun AuthorItem(
@Preview @Preview
@Composable @Composable
fun AuthorCarouselPreview() { fun AuthorCarouselPreview() {
MaterialTheme { NiaTheme {
Surface { Surface {
AuthorsCarousel( AuthorsCarousel(
authors = listOf( authors = listOf(
@ -202,7 +214,7 @@ fun AuthorCarouselPreview() {
@Preview @Preview
@Composable @Composable
fun AuthorItemPreview() { fun AuthorItemPreview() {
MaterialTheme { NiaTheme {
Surface { Surface {
AuthorItem( AuthorItem(
author = Author( author = Author(

@ -47,9 +47,6 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -79,21 +76,20 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
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.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
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.NiaFilledButton
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
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography
import kotlin.math.floor import kotlin.math.floor
@Composable @Composable
@ -133,11 +129,11 @@ fun ForYouScreen(
topBar = { topBar = {
NiaTopAppBar( NiaTopAppBar(
titleRes = R.string.top_app_bar_title, titleRes = R.string.top_app_bar_title,
navigationIcon = Icons.Filled.Search, navigationIcon = NiaIcons.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 = NiaIcons.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
), ),
@ -219,7 +215,7 @@ private fun LazyListScope.InterestsSelection(
ForYouInterestsSelectionUiState.Loading -> { ForYouInterestsSelectionUiState.Loading -> {
if (showLoadingUIIfLoading) { if (showLoadingUIIfLoading) {
item { item {
LoadingWheel( NiaLoadingWheel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentSize(), .wrapContentSize(),
@ -237,7 +233,7 @@ private fun LazyListScope.InterestsSelection(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 24.dp), .padding(top = 24.dp),
style = NiaTypography.titleMedium style = MaterialTheme.typography.titleMedium
) )
} }
item { item {
@ -247,7 +243,7 @@ private fun LazyListScope.InterestsSelection(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp), .padding(top = 8.dp, start = 16.dp, end = 16.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = NiaTypography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }
item { item {
@ -354,7 +350,7 @@ private fun SingleTopicButton(
) )
Text( Text(
text = name, text = name,
style = NiaTypography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.weight(1f), .weight(1f),
@ -365,14 +361,14 @@ private fun SingleTopicButton(
onCheckedChange = { checked -> onClick(topicId, checked) }, onCheckedChange = { checked -> onClick(topicId, checked) },
icon = { icon = {
Icon( Icon(
imageVector = NiaIcons.Add, contentDescription = name, imageVector = NiaIcons.Add,
tint = MaterialTheme.colorScheme.onSurface contentDescription = name
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
imageVector = NiaIcons.Check, contentDescription = name, imageVector = NiaIcons.Check,
tint = MaterialTheme.colorScheme.onSurface contentDescription = name
) )
} }
) )
@ -414,7 +410,7 @@ private fun LazyListScope.Feed(
ForYouFeedUiState.Loading -> { ForYouFeedUiState.Loading -> {
if (showLoadingUIIfLoading) { if (showLoadingUIIfLoading) {
item { item {
LoadingWheel( NiaLoadingWheel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentSize(), .wrapContentSize(),

@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -38,8 +36,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.ui.FollowButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.feature.interests.R.string import com.google.samples.apps.nowinandroid.feature.interests.R.string
@Composable @Composable
@ -69,15 +68,25 @@ fun InterestsItem(
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
InterestContent(name, description) InterestContent(name, description)
} }
FollowButton( NiaToggleButton(
following = following, checked = following,
onFollowChange = onFollowButtonClick, onCheckedChange = onFollowButtonClick,
notFollowingContentDescription = stringResource( icon = {
id = string.interests_card_follow_button_content_desc Icon(
), imageVector = NiaIcons.Add,
followingContentDescription = stringResource( contentDescription = stringResource(
id = string.interests_card_unfollow_button_content_desc id = string.interests_card_follow_button_content_desc
) )
)
},
checkedIcon = {
Icon(
imageVector = NiaIcons.Check,
contentDescription = stringResource(
id = string.interests_card_unfollow_button_content_desc
)
)
}
) )
} }
} }
@ -108,7 +117,7 @@ private fun InterestsIcon(topicImageUrl: String, modifier: Modifier = Modifier)
modifier = modifier modifier = modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(4.dp), .padding(4.dp),
imageVector = Icons.Filled.Person, imageVector = NiaIcons.Person,
contentDescription = null, // decorative image contentDescription = null, // decorative image
) )
} else { } else {

@ -23,28 +23,28 @@ import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTab
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRow
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.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
@Composable @Composable
fun InterestsRoute( fun InterestsRoute(
@ -93,18 +93,21 @@ fun InterestsScreen(
NiaTopAppBar( NiaTopAppBar(
titleRes = R.string.interests, titleRes = R.string.interests,
navigationIcon = Icons.Filled.Search, navigationIcon = NiaIcons.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.Filled.MoreVert, actionIcon = NiaIcons.MoreVert,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.top_app_bar_navigation_button_content_desc
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
) )
) )
when (uiState) { when (uiState) {
InterestsUiState.Loading -> InterestsUiState.Loading ->
LoadingWheel( NiaLoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.interests_loading), contentDesc = stringResource(id = R.string.interests_loading),
) )

@ -48,14 +48,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.R.string
import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@ -101,7 +101,7 @@ internal fun TopicScreen(
} }
when (topicState) { when (topicState) {
Loading -> item { Loading -> item {
LoadingWheel( NiaLoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = string.topic_loading), contentDesc = stringResource(id = string.topic_loading),
) )
@ -157,7 +157,8 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(216.dp) .size(216.dp)
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
) )
@ -184,7 +185,7 @@ private fun LazyListScope.TopicCards(news: NewsUiState) {
) )
} }
is NewsUiState.Loading -> item { is NewsUiState.Loading -> item {
LoadingWheel(contentDesc = "Loading news") // TODO NiaLoadingWheel(contentDesc = "Loading news") // TODO
} }
else -> item { else -> item {
Text("Error") // TODO Text("Error") // TODO
@ -192,6 +193,19 @@ private fun LazyListScope.TopicCards(news: NewsUiState) {
} }
} }
@Preview
@Composable
private fun TopicBodyPreview() {
NiaTheme {
LazyColumn {
TopicBody(
"Jetpack Compose", "Lorem ipsum maximum",
NewsUiState.Success(emptyList()), ""
)
}
}
}
@Composable @Composable
private fun TopicToolbar( private fun TopicToolbar(
uiState: FollowableTopic, uiState: FollowableTopic,
@ -214,8 +228,8 @@ private fun TopicToolbar(
} }
val selected = uiState.isFollowed val selected = uiState.isFollowed
NiaFilterChip( NiaFilterChip(
checked = selected, selected = selected,
onCheckedChange = onFollowClick, onSelectedChange = onFollowClick,
modifier = Modifier.padding(end = 24.dp) modifier = Modifier.padding(end = 24.dp)
) { ) {
if (selected) { if (selected) {

@ -33,6 +33,7 @@ kotlinxDatetime = "0.3.3"
kotlinxSerializationJson = "1.3.3" kotlinxSerializationJson = "1.3.3"
ksp = "1.6.21-1.0.5" ksp = "1.6.21-1.0.5"
ktlint = "0.43.0" ktlint = "0.43.0"
lint = "30.2.1"
material3 = "1.6.1" material3 = "1.6.1"
okhttp = "4.9.3" okhttp = "4.9.3"
protobuf = "3.21.1" protobuf = "3.21.1"
@ -94,10 +95,12 @@ hilt-gradlePlugin = { group = "com.google.dagger", name = "hilt-android-gradle-p
junit4 = { group = "junit", name = "junit", version.ref = "junit4" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serializationPlugin = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } kotlin-serializationPlugin = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
material3 = { group = "com.google.android.material", name = "material", version.ref = "material3" } material3 = { group = "com.google.android.material", name = "material", version.ref = "material3" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }

1
lint/.gitignore vendored

@ -0,0 +1 @@
/build

@ -0,0 +1,30 @@
/*
* 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.
*/
plugins {
id 'java-library'
id 'kotlin'
id 'com.android.lint'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
compileOnly libs.kotlin.stdlib
compileOnly libs.lint.api
}

@ -0,0 +1,120 @@
/*
* 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.lint.designsystem
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UQualifiedReferenceExpression
/**
* A detector that checks for incorrect usages of Compose Material APIs over equivalents in
* the Now in Android design system module.
*/
@Suppress("UnstableApiUsage")
class DesignSystemDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(
UCallExpression::class.java,
UQualifiedReferenceExpression::class.java
)
}
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
val name = node.methodName ?: return
val preferredName = METHOD_NAMES[name] ?: return
reportIssue(context, node, name, preferredName)
}
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
val name = node.receiver.asRenderString()
val preferredName = RECEIVER_NAMES[name] ?: return
reportIssue(context, node, name, preferredName)
}
}
}
companion object {
@JvmField
val ISSUE: Issue = Issue.create(
id = "DesignSystem",
briefDescription = "Design system",
explanation = "This check highlights calls in code that use Compose Material " +
"composables instead of equivalents from the Now in Android design system " +
"module.",
category = Category.CUSTOM_LINT_CHECKS,
priority = 7,
severity = Severity.ERROR,
implementation = Implementation(
DesignSystemDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
// Unfortunately :lint is a Java module and thus can't depend on the :core-designsystem
// Android module, so we can't use composable function references (eg. ::Button.name)
// instead of hardcoded names.
val METHOD_NAMES = mapOf(
"MaterialTheme" to "NiaTheme",
"Button" to "NiaFilledButton",
"OutlinedButton" to "NiaOutlinedButton",
"TextButton" to "NiaTextButton",
"FilterChip" to "NiaFilterChip",
"ElevatedFilterChip" to "NiaFilterChip",
"DropdownMenu" to "NiaDropdownMenu",
"NavigationBar" to "NiaNavigationBar",
"NavigationBarItem" to "NiaNavigationBarItem",
"NavigationRail" to "NiaNavigationRail",
"NavigationRailItem" to "NiaNavigationRailItem",
"TabRow" to "NiaTabRow",
"Tab" to "NiaTab",
"IconToggleButton" to "NiaToggleButton",
"FilledIconToggleButton" to "NiaToggleButton",
"FilledTonalIconToggleButton" to "NiaToggleButton",
"OutlinedIconToggleButton" to "NiaToggleButton",
"CenterAlignedTopAppBar" to "NiaTopAppBar",
"SmallTopAppBar" to "NiaTopAppBar",
"MediumTopAppBar" to "NiaTopAppBar",
"LargeTopAppBar" to "NiaTopAppBar"
)
val RECEIVER_NAMES = mapOf(
"Icons" to "NiaIcons"
)
fun reportIssue(
context: JavaContext,
node: UElement,
name: String,
preferredName: String
) {
context.report(
ISSUE, node, context.getLocation(node),
"Using $name instead of $preferredName"
)
}
}
}

@ -0,0 +1,40 @@
/*
* 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.lint.designsystem
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
/**
* An issue registry that checks for incorrect usages of Compose Material APIs over equivalents in
* the Now in Android design system module.
*/
@Suppress("UnstableApiUsage")
class DesignSystemIssueRegistry : IssueRegistry() {
override val issues = listOf(DesignSystemDetector.ISSUE)
override val api: Int = CURRENT_API
override val minApi: Int = 12
override val vendor: Vendor = Vendor(
vendorName = "Now in Android",
feedbackUrl = "https://github.com/android/nowinandroid/issues",
contact = "https://github.com/android/nowinandroid"
)
}

@ -0,0 +1,17 @@
#
# 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
#
# http://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.
#
com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemIssueRegistry

@ -49,6 +49,7 @@ include(":core-data-test")
include(":core-database") include(":core-database")
include(":core-datastore") include(":core-datastore")
include(":core-datastore-test") include(":core-datastore-test")
include(":core-designsystem")
include(":core-model") include(":core-model")
include(":core-navigation") include(":core-navigation")
include(":core-network") include(":core-network")
@ -58,4 +59,5 @@ include(":feature-author")
include(":feature-foryou") include(":feature-foryou")
include(":feature-interests") include(":feature-interests")
include(":feature-topic") include(":feature-topic")
include(":lint")
include(":sync") include(":sync")

Loading…
Cancel
Save