Improve design system usage and add lint checks

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

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

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

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

@ -16,14 +16,12 @@
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.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.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests
@ -59,22 +57,22 @@ class NiaTopLevelNavigation(private val navController: NavHostController) {
data class TopLevelDestination(
val route: String,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector,
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
)
val TOP_LEVEL_DESTINATIONS = listOf(
TopLevelDestination(
route = ForYouDestination.route,
selectedIcon = Icons.Filled.Upcoming,
unselectedIcon = Icons.Outlined.Upcoming,
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = for_you
),
TopLevelDestination(
route = InterestsDestination.route,
selectedIcon = Icons.Filled.Grid3x3,
unselectedIcon = Icons.Outlined.Grid3x3,
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interests
)
)

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

@ -42,6 +42,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
dependencies {
add("implementation", project(":core-model"))
add("implementation", project(":core-ui"))
add("implementation", project(":core-designsystem"))
add("implementation", project(":core-data"))
add("implementation", project(":core-common"))
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.
*/
package com.google.samples.apps.nowinandroid.core.ui.component
package com.google.samples.apps.nowinandroid.core.designsystem.component
import android.content.res.Configuration
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.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.ui.theme.LocalBackgroundTheme
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
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 kotlin.math.tan
/**
@ -75,8 +76,8 @@ fun NiaBackground(
@Composable
fun NiaGradientBackground(
modifier: Modifier = Modifier,
topColor: Color = LocalBackgroundTheme.current.primaryGradientColor,
bottomColor: Color = LocalBackgroundTheme.current.secondaryGradientColor,
topColor: Color = LocalGradientColors.current.primary,
bottomColor: Color = LocalGradientColors.current.secondary,
content: @Composable () -> Unit
) {
val currentTopColor by rememberUpdatedState(topColor)

@ -14,7 +14,7 @@
* 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.layout.Box
@ -63,9 +63,7 @@ fun NiaFilledButton(
Button(
onClick = onClick,
modifier = if (small) {
Modifier
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
} else {
modifier
},
@ -154,9 +152,7 @@ fun NiaOutlinedButton(
OutlinedButton(
onClick = onClick,
modifier = if (small) {
Modifier
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
} else {
modifier
},
@ -247,9 +243,7 @@ fun NiaTextButton(
TextButton(
onClick = onClick,
modifier = if (small) {
Modifier
.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
.then(modifier)
modifier.heightIn(min = NiaButtonDefaults.SmallButtonHeight)
} else {
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.
*/
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.material3.DropdownMenu
@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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

@ -14,7 +14,7 @@
* 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 androidx.compose.animation.animateColor
@ -46,11 +46,11 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
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
@Composable
fun LoadingWheel(
fun NiaLoadingWheel(
contentDesc: String,
modifier: Modifier = Modifier
) {
@ -134,10 +134,10 @@ fun LoadingWheel(
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
@Composable
fun LoadingWheelPreview() {
fun NiaLoadingWheelPreview() {
NiaTheme {
Surface {
LoadingWheel(contentDesc = "LoadingWheel")
NiaLoadingWheel(contentDesc = "LoadingWheel")
}
}
}

@ -14,7 +14,7 @@
* 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.RowScope

@ -14,7 +14,7 @@
* 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.padding

@ -14,7 +14,7 @@
* 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.material3.MaterialTheme

@ -14,16 +14,21 @@
* 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.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
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
* will not be clickable and will appear disabled to accessibility services.
* @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 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
fun NiaToggleButton(
@ -50,33 +59,38 @@ fun NiaToggleButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
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 checkedRadius = with(LocalDensity.current) {
checkedBackgroundRadius.toPx()
}
val radius = with(LocalDensity.current) { (size / 2).toPx() }
IconButton(
onClick = { onCheckedChange(!checked) },
modifier = Modifier
modifier = modifier
.size(size)
.toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {})
.drawBehind {
if (checked) drawCircle(
color = checkedColor,
radius = checkedRadius
drawCircle(
color = if (checked) checkedBackgroundColor else backgroundColor,
radius = radius
)
}
.then(modifier),
},
enabled = enabled,
content = {
Box(
modifier = Modifier.sizeIn(
maxWidth = NiaToggleButtonDefaults.ToggleButtonIconSize,
maxHeight = NiaToggleButtonDefaults.ToggleButtonIconSize
maxWidth = iconSize,
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.
*/
package com.google.samples.apps.nowinandroid.core.ui.component
package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.annotation.StringRes
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.google.samples.apps.nowinandroid.core.ui.R
@Composable
fun NiaTopAppBar(
@ -75,7 +74,7 @@ fun NiaTopAppBar(
@Composable
fun NiaTopAppBarPreview() {
NiaTopAppBar(
titleRes = R.string.top_app_bar_preview_title,
titleRes = android.R.string.untitled,
navigationIcon = Icons.Default.Search,
navigationIconContentDescription = "Navigation icon",
actionIcon = Icons.Default.MoreVert,

@ -14,12 +14,12 @@
* 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.runtime.Composable
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

@ -14,8 +14,9 @@
* 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.filled.MoreVert
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.ExpandLess
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.Search
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.VolumeUp
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.
@ -54,9 +57,11 @@ object NiaIcons {
val Close = Icons.Rounded.Close
val ExpandLess = Icons.Rounded.ExpandLess
val Fullscreen = Icons.Rounded.Fullscreen
val Grid3x3 = Icons.Rounded.Grid3x3
val MenuBook = R.drawable.ic_menu_book
val MenuBookBorder = R.drawable.ic_menu_book_border
val MoreVert = Icons.Default.MoreVert
val Person = Icons.Rounded.Person
val PlayArrow = Icons.Rounded.PlayArrow
val Search = Icons.Rounded.Search
val ShortText = Icons.Rounded.ShortText
@ -67,3 +72,11 @@ object NiaIcons {
val VolumeOff = Icons.Rounded.VolumeOff
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.
*/
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.staticCompositionLocalOf
@ -22,17 +22,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
/**
* A class to model background values for Now in Android,
* including color, tonal elevation and gradient colors.
* A class to model background color and tonal elevation values for Now in Android.
*/
@Immutable
data class BackgroundTheme(
val color: Color = Color.Unspecified,
val tonalElevation: Dp = Dp.Unspecified,
val primaryGradientColor: Color = Color.Unspecified,
val secondaryGradientColor: Color = Color.Unspecified,
val tertiaryGradientColor: Color = Color.Unspecified,
val neutralGradientColor: Color = Color.Unspecified
val tonalElevation: Dp = Dp.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.
*/
package com.google.samples.apps.nowinandroid.core.ui.theme
package com.google.samples.apps.nowinandroid.core.designsystem.theme
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@ -32,7 +33,8 @@ import androidx.compose.ui.unit.dp
/**
* Light default theme color scheme
*/
private val LightDefaultColorScheme = lightColorScheme(
@VisibleForTesting
val LightDefaultColorScheme = lightColorScheme(
primary = Purple40,
onPrimary = Color.White,
primaryContainer = Purple90,
@ -61,7 +63,8 @@ private val LightDefaultColorScheme = lightColorScheme(
/**
* Dark default theme color scheme
*/
private val DarkDefaultColorScheme = darkColorScheme(
@VisibleForTesting
val DarkDefaultColorScheme = darkColorScheme(
primary = Purple80,
onPrimary = Purple20,
primaryContainer = Purple30,
@ -90,7 +93,8 @@ private val DarkDefaultColorScheme = darkColorScheme(
/**
* Light Android theme color scheme
*/
private val LightAndroidColorScheme = lightColorScheme(
@VisibleForTesting
val LightAndroidColorScheme = lightColorScheme(
primary = Green40,
onPrimary = Color.White,
primaryContainer = Green90,
@ -119,7 +123,8 @@ private val LightAndroidColorScheme = lightColorScheme(
/**
* Dark Android theme color scheme
*/
private val DarkAndroidColorScheme = darkColorScheme(
@VisibleForTesting
val DarkAndroidColorScheme = darkColorScheme(
primary = Green80,
onPrimary = Green20,
primaryContainer = Green30,
@ -145,6 +150,26 @@ private val DarkAndroidColorScheme = darkColorScheme(
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.
*
@ -164,45 +189,45 @@ fun NiaTheme(
content: @Composable() () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
dynamicColor -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
}
androidTheme && darkTheme -> DarkAndroidColorScheme
androidTheme -> LightAndroidColorScheme
darkTheme -> DarkDefaultColorScheme
else -> LightDefaultColorScheme
androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
else -> if (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 {
androidTheme && darkTheme -> BackgroundTheme(
color = Color.Black
)
androidTheme -> BackgroundTheme(
color = DarkGreenGray95
)
darkTheme -> BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp,
primaryGradientColor = colorScheme.primary.lighten(0.95f),
secondaryGradientColor = colorScheme.secondary.lighten(0.95f),
tertiaryGradientColor = colorScheme.tertiary.lighten(0.95f),
neutralGradientColor = colorScheme.surface.lighten(0.95f)
)
else -> BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp,
primaryGradientColor = Purple95,
secondaryGradientColor = Orange95,
tertiaryGradientColor = Blue95,
neutralGradientColor = DarkPurpleGray95
)
dynamicColor -> defaultBackgroundTheme
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
else -> defaultBackgroundTheme
}
CompositionLocalProvider(LocalBackgroundTheme provides backgroundTheme) {
CompositionLocalProvider(
LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme
) {
MaterialTheme(
colorScheme = colorScheme,
typography = NiaTypography,

@ -14,7 +14,7 @@
* 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.ui.text.TextStyle
@ -27,7 +27,7 @@ import androidx.compose.ui.unit.sp
*
* TODO: Add custom font
*/
val NiaTypography = Typography(
internal val NiaTypography = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.W400,
fontSize = 57.sp,

@ -21,6 +21,7 @@ plugins {
}
dependencies {
implementation(project(":core-designsystem"))
implementation(project(":core-model"))
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.shape.CircleShape
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.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -61,11 +56,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat
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.NewsResource
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.ui.theme.NiaTheme
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant
@ -184,7 +181,7 @@ fun NewsResourceAuthors(
modifier = authorImageModifier
.background(MaterialTheme.colorScheme.surface)
.padding(4.dp),
imageVector = Icons.Filled.Person,
imageVector = NiaIcons.Person,
contentDescription = null // decorative image
)
}
@ -208,23 +205,23 @@ fun BookmarkButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val clickActionLabel = stringResource(
if (isBookmarked) R.string.unbookmark else R.string.bookmark
)
IconToggleButton(
NiaToggleButton(
checked = isBookmarked,
onCheckedChange = { onClick() },
modifier = modifier.semantics {
// Use custom label for accessibility services to communicate button's action to user.
// Pass null for action to only override the label and not the actual action.
this.onClick(label = clickActionLabel, action = null)
modifier = modifier,
icon = {
Icon(
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

@ -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="card_tap_action">Open Resource Link</string>
<string name="top_app_bar_preview_title">Title</string>
</resources>

@ -50,15 +50,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.FollowableAuthor
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.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.theme.NiaTheme
@Composable
fun AuthorRoute(
@ -102,7 +102,7 @@ internal fun AuthorScreen(
when (authorState) {
AuthorUiState.Loading -> {
item {
LoadingWheel(
NiaLoadingWheel(
modifier = modifier,
contentDesc = stringResource(id = R.string.author_loading),
)
@ -186,7 +186,7 @@ private fun LazyListScope.authorCards(news: NewsUiState) {
)
}
is NewsUiState.Loading -> item {
LoadingWheel(contentDesc = "Loading news") // TODO
NiaLoadingWheel(contentDesc = "Loading news") // TODO
}
else -> item {
Text("Error") // TODO
@ -217,8 +217,8 @@ private fun AuthorToolbar(
val selected = uiState.isFollowed
NiaFilterChip(
modifier = Modifier.padding(horizontal = 16.dp),
checked = selected,
onCheckedChange = onFollowClick,
selected = selected,
onSelectedChange = onFollowClick,
) {
if (selected) {
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.selection.toggleable
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.material3.Icon
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.unit.dp
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.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.ui.FollowButton
@Composable
fun AuthorsCarousel(
@ -121,7 +121,7 @@ fun AuthorItem(
modifier = authorImageModifier
.background(MaterialTheme.colorScheme.surface)
.padding(4.dp),
imageVector = Icons.Filled.Person,
imageVector = NiaIcons.Person,
contentDescription = null // decorative image
)
} else {
@ -132,12 +132,24 @@ fun AuthorItem(
contentDescription = null
)
}
FollowButton(
following = following,
backgroundColor = MaterialTheme.colorScheme.surface,
size = 20.dp,
iconSize = 14.dp,
modifier = Modifier.align(Alignment.BottomEnd)
NiaToggleButton(
checked = following,
onCheckedChange = onAuthorClick,
modifier = Modifier.align(Alignment.BottomEnd),
icon = {
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))
@ -155,7 +167,7 @@ fun AuthorItem(
@Preview
@Composable
fun AuthorCarouselPreview() {
MaterialTheme {
NiaTheme {
Surface {
AuthorsCarousel(
authors = listOf(
@ -202,7 +214,7 @@ fun AuthorCarouselPreview() {
@Preview
@Composable
fun AuthorItemPreview() {
MaterialTheme {
NiaTheme {
Surface {
AuthorItem(
author = Author(

@ -47,9 +47,6 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CornerSize
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.Icon
import androidx.compose.material3.MaterialTheme
@ -79,21 +76,20 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
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.FollowableTopic
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.previewNewsResources
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.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
@Composable
@ -133,11 +129,11 @@ fun ForYouScreen(
topBar = {
NiaTopAppBar(
titleRes = R.string.top_app_bar_title,
navigationIcon = Icons.Filled.Search,
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc
),
actionIcon = Icons.Outlined.AccountCircle,
actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc
),
@ -219,7 +215,7 @@ private fun LazyListScope.InterestsSelection(
ForYouInterestsSelectionUiState.Loading -> {
if (showLoadingUIIfLoading) {
item {
LoadingWheel(
NiaLoadingWheel(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(),
@ -237,7 +233,7 @@ private fun LazyListScope.InterestsSelection(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
style = NiaTypography.titleMedium
style = MaterialTheme.typography.titleMedium
)
}
item {
@ -247,7 +243,7 @@ private fun LazyListScope.InterestsSelection(
.fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp),
textAlign = TextAlign.Center,
style = NiaTypography.bodyMedium
style = MaterialTheme.typography.bodyMedium
)
}
item {
@ -354,7 +350,7 @@ private fun SingleTopicButton(
)
Text(
text = name,
style = NiaTypography.titleSmall,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.padding(horizontal = 12.dp)
.weight(1f),
@ -365,14 +361,14 @@ private fun SingleTopicButton(
onCheckedChange = { checked -> onClick(topicId, checked) },
icon = {
Icon(
imageVector = NiaIcons.Add, contentDescription = name,
tint = MaterialTheme.colorScheme.onSurface
imageVector = NiaIcons.Add,
contentDescription = name
)
},
checkedIcon = {
Icon(
imageVector = NiaIcons.Check, contentDescription = name,
tint = MaterialTheme.colorScheme.onSurface
imageVector = NiaIcons.Check,
contentDescription = name
)
}
)
@ -414,7 +410,7 @@ private fun LazyListScope.Feed(
ForYouFeedUiState.Loading -> {
if (showLoadingUIIfLoading) {
item {
LoadingWheel(
NiaLoadingWheel(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(),

@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.MaterialTheme
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 coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.ui.FollowButton
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
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.feature.interests.R.string
@Composable
@ -69,15 +68,25 @@ fun InterestsItem(
Spacer(modifier = Modifier.width(16.dp))
InterestContent(name, description)
}
FollowButton(
following = following,
onFollowChange = onFollowButtonClick,
notFollowingContentDescription = stringResource(
id = string.interests_card_follow_button_content_desc
),
followingContentDescription = stringResource(
id = string.interests_card_unfollow_button_content_desc
)
NiaToggleButton(
checked = following,
onCheckedChange = onFollowButtonClick,
icon = {
Icon(
imageVector = NiaIcons.Add,
contentDescription = stringResource(
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
.background(MaterialTheme.colorScheme.surface)
.padding(4.dp),
imageVector = Icons.Filled.Person,
imageVector = NiaIcons.Person,
contentDescription = null, // decorative image
)
} else {

@ -23,28 +23,28 @@ import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
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.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.FollowableTopic
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.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
fun InterestsRoute(
@ -93,18 +93,21 @@ fun InterestsScreen(
NiaTopAppBar(
titleRes = R.string.interests,
navigationIcon = Icons.Filled.Search,
navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc
),
actionIcon = Icons.Filled.MoreVert,
actionIcon = NiaIcons.MoreVert,
actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
)
)
when (uiState) {
InterestsUiState.Loading ->
LoadingWheel(
NiaLoadingWheel(
modifier = modifier,
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.hilt.navigation.compose.hiltViewModel
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.previewNewsResources
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.theme.NiaTheme
import com.google.samples.apps.nowinandroid.feature.topic.R.string
import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@ -101,7 +101,7 @@ internal fun TopicScreen(
}
when (topicState) {
Loading -> item {
LoadingWheel(
NiaLoadingWheel(
modifier = modifier,
contentDesc = stringResource(id = string.topic_loading),
)
@ -157,7 +157,8 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.align(Alignment.CenterHorizontally)
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(216.dp)
.padding(bottom = 12.dp)
)
@ -184,7 +185,7 @@ private fun LazyListScope.TopicCards(news: NewsUiState) {
)
}
is NewsUiState.Loading -> item {
LoadingWheel(contentDesc = "Loading news") // TODO
NiaLoadingWheel(contentDesc = "Loading news") // TODO
}
else -> item {
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
private fun TopicToolbar(
uiState: FollowableTopic,
@ -214,8 +228,8 @@ private fun TopicToolbar(
}
val selected = uiState.isFollowed
NiaFilterChip(
checked = selected,
onCheckedChange = onFollowClick,
selected = selected,
onSelectedChange = onFollowClick,
modifier = Modifier.padding(end = 24.dp)
) {
if (selected) {

@ -33,6 +33,7 @@ kotlinxDatetime = "0.3.3"
kotlinxSerializationJson = "1.3.3"
ksp = "1.6.21-1.0.5"
ktlint = "0.43.0"
lint = "30.2.1"
material3 = "1.6.1"
okhttp = "4.9.3"
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" }
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-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-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-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" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
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-datastore")
include(":core-datastore-test")
include(":core-designsystem")
include(":core-model")
include(":core-navigation")
include(":core-network")
@ -58,4 +59,5 @@ include(":feature-author")
include(":feature-foryou")
include(":feature-interests")
include(":feature-topic")
include(":lint")
include(":sync")

Loading…
Cancel
Save