diff --git a/app-nia-catalog/build.gradle.kts b/app-nia-catalog/build.gradle.kts index 28edda383..26eb06969 100644 --- a/app-nia-catalog/build.gradle.kts +++ b/app-nia-catalog/build.gradle.kts @@ -37,6 +37,7 @@ android { dependencies { implementation(project(":core-ui")) + implementation(project(":core-designsystem")) implementation(libs.androidx.activity.compose) implementation(libs.accompanist.flowlayout) diff --git a/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt b/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt index 30e73e17b..d4d7b499b 100644 --- a/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt +++ b/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt @@ -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()) } ) } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 16406e511..816911b04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { implementation(project(":feature-topic")) implementation(project(":core-ui")) + implementation(project(":core-designsystem")) implementation(project(":core-navigation")) implementation(project(":sync")) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt index f2404e88c..6023982e8 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt @@ -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 ) ) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 135634a31..2de4693cb 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -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)) } ) diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 9545bfb12..58ccca210 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -42,6 +42,7 @@ class AndroidFeatureConventionPlugin : Plugin { 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")) diff --git a/core-designsystem/.gitignore b/core-designsystem/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core-designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core-designsystem/build.gradle.kts b/core-designsystem/build.gradle.kts new file mode 100644 index 000000000..bada9e329 --- /dev/null +++ b/core-designsystem/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/core-designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt b/core-designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt new file mode 100644 index 000000000..d0fb5ff83 --- /dev/null +++ b/core-designsystem/src/androidTest/java/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt @@ -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) + } +} diff --git a/core-designsystem/src/main/AndroidManifest.xml b/core-designsystem/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0f6a479a3 --- /dev/null +++ b/core-designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt similarity index 93% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt index cc797409f..a3af38f23 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Background.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt @@ -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) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Button.kt similarity index 97% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Button.kt index 0e6fc5dde..696c7e8fc 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Button.kt @@ -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 }, diff --git a/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt new file mode 100644 index 000000000..3d2cf3f2c --- /dev/null +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt @@ -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 +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DropdownMenu.kt similarity index 97% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DropdownMenu.kt index 0e21fdc19..270fd07a6 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/DropdownMenu.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/LoadingWheel.kt similarity index 95% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/LoadingWheel.kt index fb0b31be6..4ce7e1473 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/LoadingWheel.kt @@ -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") } } } diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt similarity index 98% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt index 28f906932..8537f5fbf 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tabs.kt similarity index 97% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tabs.kt index 72bfb19ca..76a0a3248 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tabs.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tag.kt similarity index 97% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tag.kt index a47a85667..c2790c538 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Tag.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ToggleButton.kt similarity index 60% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ToggleButton.kt index df2b67f73..b50bc9ff9 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ToggleButton.kt @@ -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() + } } } ) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/TopAppBar.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt similarity index 94% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/TopAppBar.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt index d0a1e6309..09cb209db 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/TopAppBar.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt @@ -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, diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ViewToggle.kt similarity index 93% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ViewToggle.kt index e9779ad70..a8267040a 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/ViewToggle.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt similarity index 82% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt index c89407059..b6c06c8ee 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt @@ -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() +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Background.kt similarity index 69% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Background.kt index d39e21e67..f91e5a30d 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Background.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Background.kt @@ -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 ) /** diff --git a/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt new file mode 100644 index 000000000..50c6f972f --- /dev/null +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt @@ -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) diff --git a/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Gradient.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Gradient.kt new file mode 100644 index 000000000..7ecaa94b8 --- /dev/null +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Gradient.kt @@ -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() } diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt similarity index 69% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt index 8b7f7a151..efec108f8 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Theme.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Theme.kt @@ -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, diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Type.kt b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt similarity index 96% rename from core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Type.kt rename to core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt index d65a75add..9d8fb8df8 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Type.kt +++ b/core-designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt @@ -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, diff --git a/core-ui/src/main/res/drawable/ic_bookmark.xml b/core-designsystem/src/main/res/drawable/ic_bookmark.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_bookmark.xml rename to core-designsystem/src/main/res/drawable/ic_bookmark.xml diff --git a/core-ui/src/main/res/drawable/ic_bookmark_border.xml b/core-designsystem/src/main/res/drawable/ic_bookmark_border.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_bookmark_border.xml rename to core-designsystem/src/main/res/drawable/ic_bookmark_border.xml diff --git a/core-ui/src/main/res/drawable/ic_bookmarks.xml b/core-designsystem/src/main/res/drawable/ic_bookmarks.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_bookmarks.xml rename to core-designsystem/src/main/res/drawable/ic_bookmarks.xml diff --git a/core-ui/src/main/res/drawable/ic_bookmarks_border.xml b/core-designsystem/src/main/res/drawable/ic_bookmarks_border.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_bookmarks_border.xml rename to core-designsystem/src/main/res/drawable/ic_bookmarks_border.xml diff --git a/core-ui/src/main/res/drawable/ic_menu_book.xml b/core-designsystem/src/main/res/drawable/ic_menu_book.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_menu_book.xml rename to core-designsystem/src/main/res/drawable/ic_menu_book.xml diff --git a/core-ui/src/main/res/drawable/ic_menu_book_border.xml b/core-designsystem/src/main/res/drawable/ic_menu_book_border.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_menu_book_border.xml rename to core-designsystem/src/main/res/drawable/ic_menu_book_border.xml diff --git a/core-ui/src/main/res/drawable/ic_placeholder_default.xml b/core-designsystem/src/main/res/drawable/ic_placeholder_default.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_placeholder_default.xml rename to core-designsystem/src/main/res/drawable/ic_placeholder_default.xml diff --git a/core-ui/src/main/res/drawable/ic_upcoming.xml b/core-designsystem/src/main/res/drawable/ic_upcoming.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_upcoming.xml rename to core-designsystem/src/main/res/drawable/ic_upcoming.xml diff --git a/core-ui/src/main/res/drawable/ic_upcoming_border.xml b/core-designsystem/src/main/res/drawable/ic_upcoming_border.xml similarity index 100% rename from core-ui/src/main/res/drawable/ic_upcoming_border.xml rename to core-designsystem/src/main/res/drawable/ic_upcoming_border.xml diff --git a/core-ui/build.gradle.kts b/core-ui/build.gradle.kts index 7fade59be..5bc3bb10f 100644 --- a/core-ui/build.gradle.kts +++ b/core-ui/build.gradle.kts @@ -21,6 +21,7 @@ plugins { } dependencies { + implementation(project(":core-designsystem")) implementation(project(":core-model")) implementation(libs.androidx.core.ktx) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowButton.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowButton.kt deleted file mode 100644 index 46a162942..000000000 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowButton.kt +++ /dev/null @@ -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) -} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 718840d17..3aabc7105 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -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 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Chip.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Chip.kt deleted file mode 100644 index 95a28892a..000000000 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Chip.kt +++ /dev/null @@ -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 - } - ) -} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt deleted file mode 100644 index d8d07dce0..000000000 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/theme/Color.kt +++ /dev/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 -} diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index a55cc697a..548ea63ef 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -20,6 +20,4 @@ Back Open Resource Link - - Title diff --git a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index 74c72624d..254b92ce9 100644 --- a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -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)) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt index c75592511..f36b09f57 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt @@ -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( diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index cd996c3f3..5acf0daa0 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -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(), diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index ca1b03c71..61959a748 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -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 { diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index e536b632f..aa6c358ad 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -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), ) diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 3cb99d67e..4e7f5b657 100644 --- a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -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) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78724ff06..84e0416e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lint/build.gradle b/lint/build.gradle new file mode 100644 index 000000000..4574fdffd --- /dev/null +++ b/lint/build.gradle @@ -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 +} diff --git a/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt new file mode 100644 index 000000000..135c66407 --- /dev/null +++ b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt @@ -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> { + 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" + ) + } + } +} diff --git a/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt new file mode 100644 index 000000000..152c07c28 --- /dev/null +++ b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt @@ -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" + ) +} diff --git a/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 000000000..4b8002da2 --- /dev/null +++ b/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 489ed6f8d..d61d01798 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")