From a53066769d62073e57dc6508dc1df987474cb980 Mon Sep 17 00:00:00 2001 From: Nick Rout Date: Thu, 17 Mar 2022 14:39:57 +0200 Subject: [PATCH] Implement Now in Android Material 3 components and icons Bug: 216019424 Change-Id: I3f23b07dbaa5a834bb05f70e8b68f31ae66d6722 --- .../nowinandroid/core/ui/component/Button.kt | 449 ++++++++++++++ .../nowinandroid/core/ui/component/Catalog.kt | 571 ++++++++++++++++++ .../nowinandroid/core/ui/component/Chip.kt | 90 +++ .../core/ui/component/DropdownMenu.kt | 132 ++++ .../core/ui/component/Navigation.kt | 176 ++++++ .../nowinandroid/core/ui/component/Tabs.kt | 102 ++++ .../nowinandroid/core/ui/component/Tag.kt | 91 +++ .../core/ui/component/ToggleButton.kt | 87 +++ .../core/ui/component/ViewToggle.kt | 57 ++ .../nowinandroid/core/ui/icon/NiaIcons.kt | 69 +++ core-ui/src/main/res/drawable/ic_bookmark.xml | 24 + .../main/res/drawable/ic_bookmark_border.xml | 24 + .../src/main/res/drawable/ic_bookmarks.xml | 27 + .../main/res/drawable/ic_bookmarks_border.xml | 28 + .../src/main/res/drawable/ic_menu_book.xml | 34 ++ .../main/res/drawable/ic_menu_book_border.xml | 34 ++ core-ui/src/main/res/drawable/ic_upcoming.xml | 33 + .../main/res/drawable/ic_upcoming_border.xml | 34 ++ gradle/libs.versions.toml | 2 +- 19 files changed, 2063 insertions(+), 1 deletion(-) create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Catalog.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Chip.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt create mode 100644 core-ui/src/main/res/drawable/ic_bookmark.xml create mode 100644 core-ui/src/main/res/drawable/ic_bookmark_border.xml create mode 100644 core-ui/src/main/res/drawable/ic_bookmarks.xml create mode 100644 core-ui/src/main/res/drawable/ic_bookmarks_border.xml create mode 100644 core-ui/src/main/res/drawable/ic_menu_book.xml create mode 100644 core-ui/src/main/res/drawable/ic_menu_book_border.xml create mode 100644 core-ui/src/main/res/drawable/ic_upcoming.xml create mode 100644 core-ui/src/main/res/drawable/ic_upcoming_border.xml diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt new file mode 100644 index 000000000..0e6fc5dde --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Button.kt @@ -0,0 +1,449 @@ +/* + * 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.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Now in Android filled button with generic content slot. Wraps Material 3 [Button]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.filledButtonColors]. + * @param contentPadding The spacing values to apply internally between the container and the + * content. See [NiaButtonDefaults.buttonContentPadding]. + * @param content The button content. + */ +@Composable +fun NiaFilledButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + colors: ButtonColors = NiaButtonDefaults.filledButtonColors(), + contentPadding: PaddingValues = NiaButtonDefaults.buttonContentPadding(small = small), + content: @Composable RowScope.() -> Unit +) { + Button( + onClick = onClick, + modifier = if (small) { + Modifier + .heightIn(min = NiaButtonDefaults.SmallButtonHeight) + .then(modifier) + } else { + modifier + }, + enabled = enabled, + colors = colors, + contentPadding = contentPadding, + content = { + ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { + content() + } + } + ) +} + +/** + * Now in Android filled button with text and icon content slots. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.filledButtonColors]. + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. + * @param trailingIcon The button trailing icon content. Pass `null` here for no trailing icon. + */ +@Composable +fun NiaFilledButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + colors: ButtonColors = NiaButtonDefaults.filledButtonColors(), + text: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null +) { + NiaFilledButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + small = small, + colors = colors, + contentPadding = NiaButtonDefaults.buttonContentPadding( + small = small, + leadingIcon = leadingIcon != null, + trailingIcon = trailingIcon != null + ) + ) { + NiaButtonContent( + text = text, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + } +} + +/** + * Now in Android outlined button with generic content slot. Wraps Material 3 [OutlinedButton]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param border Border to draw around the button. Pass `null` here for no border. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.outlinedButtonColors]. + * @param contentPadding The spacing values to apply internally between the container and the + * content. See [NiaButtonDefaults.buttonContentPadding]. + * @param content The button content. + */ +@Composable +fun NiaOutlinedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + border: BorderStroke? = NiaButtonDefaults.outlinedButtonBorder(enabled = enabled), + colors: ButtonColors = NiaButtonDefaults.outlinedButtonColors(), + contentPadding: PaddingValues = NiaButtonDefaults.buttonContentPadding(small = small), + content: @Composable RowScope.() -> Unit +) { + OutlinedButton( + onClick = onClick, + modifier = if (small) { + Modifier + .heightIn(min = NiaButtonDefaults.SmallButtonHeight) + .then(modifier) + } else { + modifier + }, + enabled = enabled, + border = border, + colors = colors, + contentPadding = contentPadding, + content = { + ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { + content() + } + } + ) +} + +/** + * Now in Android outlined button with text and icon content slots. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param border Border to draw around the button. Pass `null` here for no border. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.outlinedButtonColors]. + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. + * @param trailingIcon The button trailing icon content. Pass `null` here for no trailing icon. + */ +@Composable +fun NiaOutlinedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + border: BorderStroke? = NiaButtonDefaults.outlinedButtonBorder(enabled = enabled), + colors: ButtonColors = NiaButtonDefaults.outlinedButtonColors(), + text: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null +) { + NiaOutlinedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + small = small, + border = border, + colors = colors, + contentPadding = NiaButtonDefaults.buttonContentPadding( + small = small, + leadingIcon = leadingIcon != null, + trailingIcon = trailingIcon != null + ) + ) { + NiaButtonContent( + text = text, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + } +} + +/** + * Now in Android text button with generic content slot. Wraps Material 3 [TextButton]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.textButtonColors]. + * @param contentPadding The spacing values to apply internally between the container and the + * content. See [NiaButtonDefaults.buttonContentPadding]. + * @param content The button content. + */ +@Composable +fun NiaTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + colors: ButtonColors = NiaButtonDefaults.textButtonColors(), + contentPadding: PaddingValues = NiaButtonDefaults.buttonContentPadding(small = small), + content: @Composable RowScope.() -> Unit +) { + TextButton( + onClick = onClick, + modifier = if (small) { + Modifier + .heightIn(min = NiaButtonDefaults.SmallButtonHeight) + .then(modifier) + } else { + modifier + }, + enabled = enabled, + colors = colors, + contentPadding = contentPadding, + content = { + ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { + content() + } + } + ) +} + +/** + * Now in Android text button with text and icon content slots. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param small Whether or not the size of the button should be small or regular. + * @param colors [ButtonColors] that will be used to resolve the container and content color for + * this button in different states. See [NiaButtonDefaults.textButtonColors]. + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. + * @param trailingIcon The button trailing icon content. Pass `null` here for no trailing icon. + */ +@Composable +fun NiaTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + small: Boolean = false, + colors: ButtonColors = NiaButtonDefaults.textButtonColors(), + text: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null +) { + NiaTextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + small = small, + colors = colors, + contentPadding = NiaButtonDefaults.buttonContentPadding( + small = small, + leadingIcon = leadingIcon != null, + trailingIcon = trailingIcon != null + ) + ) { + NiaButtonContent( + text = text, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + } +} + +/** + * Internal Now in Android button content layout for arranging the text label, leading icon and + * trailing icon. + * + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. + * @param trailingIcon The button trailing icon content. Pass `null` here for no trailing icon. + */ +@Composable +private fun RowScope.NiaButtonContent( + text: @Composable () -> Unit, + leadingIcon: @Composable (() -> Unit)?, + trailingIcon: @Composable (() -> Unit)? +) { + if (leadingIcon != null) { + Box(Modifier.sizeIn(maxHeight = NiaButtonDefaults.ButtonIconSize)) { + leadingIcon() + } + } + Box( + Modifier + .weight(1f, fill = false) + .padding( + start = if (leadingIcon != null) { + NiaButtonDefaults.ButtonContentSpacing + } else { + 0.dp + }, + end = if (trailingIcon != null) { + NiaButtonDefaults.ButtonContentSpacing + } else { + 0.dp + } + ) + ) { + text() + } + if (trailingIcon != null) { + Box(Modifier.sizeIn(maxHeight = NiaButtonDefaults.ButtonIconSize)) { + trailingIcon() + } + } +} + +/** + * Now in Android button default values. + */ +object NiaButtonDefaults { + val SmallButtonHeight = 32.dp + const val DisabledButtonContainerAlpha = 0.12f + const val DisabledButtonContentAlpha = 0.38f + val ButtonHorizontalPadding = 24.dp + val ButtonHorizontalIconPadding = 16.dp + val ButtonVerticalPadding = 8.dp + val SmallButtonHorizontalPadding = 16.dp + val SmallButtonHorizontalIconPadding = 12.dp + val SmallButtonVerticalPadding = 7.dp + val ButtonContentSpacing = 8.dp + val ButtonIconSize = 18.dp + fun buttonContentPadding( + small: Boolean, + leadingIcon: Boolean = false, + trailingIcon: Boolean = false + ): PaddingValues { + return PaddingValues( + start = when { + small && leadingIcon -> SmallButtonHorizontalIconPadding + small -> SmallButtonHorizontalPadding + leadingIcon -> ButtonHorizontalIconPadding + else -> ButtonHorizontalPadding + }, + top = if (small) SmallButtonVerticalPadding else ButtonVerticalPadding, + end = when { + small && trailingIcon -> SmallButtonHorizontalIconPadding + small -> SmallButtonHorizontalPadding + trailingIcon -> ButtonHorizontalIconPadding + else -> ButtonHorizontalPadding + }, + bottom = if (small) SmallButtonVerticalPadding else ButtonVerticalPadding + ) + } + @Composable + fun filledButtonColors( + containerColor: Color = MaterialTheme.colorScheme.onBackground, + contentColor: Color = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor: Color = MaterialTheme.colorScheme.onBackground.copy( + alpha = DisabledButtonContainerAlpha + ), + disabledContentColor: Color = MaterialTheme.colorScheme.onBackground.copy( + alpha = DisabledButtonContentAlpha + ) + ) = ButtonDefaults.buttonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor + ) + @Composable + fun outlinedButtonBorder( + enabled: Boolean, + width: Dp = 1.dp, + color: Color = MaterialTheme.colorScheme.onBackground, + disabledColor: Color = MaterialTheme.colorScheme.onBackground.copy( + alpha = DisabledButtonContainerAlpha + ) + ): BorderStroke = BorderStroke( + width = width, + color = if (enabled) color else disabledColor + ) + @Composable + fun outlinedButtonColors( + containerColor: Color = Color.Transparent, + contentColor: Color = MaterialTheme.colorScheme.onBackground, + disabledContainerColor: Color = Color.Transparent, + disabledContentColor: Color = MaterialTheme.colorScheme.onBackground.copy( + alpha = DisabledButtonContentAlpha + ) + ) = ButtonDefaults.outlinedButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor + ) + @Composable + fun textButtonColors( + containerColor: Color = Color.Transparent, + contentColor: Color = MaterialTheme.colorScheme.onBackground, + disabledContainerColor: Color = Color.Transparent, + disabledContentColor: Color = MaterialTheme.colorScheme.onBackground.copy( + alpha = DisabledButtonContentAlpha + ) + ) = ButtonDefaults.textButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor + ) +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Catalog.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Catalog.kt new file mode 100644 index 000000000..c5c459ab7 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Catalog.kt @@ -0,0 +1,571 @@ +/* + * 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.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons + +/** + * Now in Android component catalog. + */ +@Composable +fun NiaComponentCatalog() { + val contentPadding = WindowInsets + .systemBars + .add(WindowInsets(left = 16.dp, top = 16.dp, right = 16.dp, bottom = 16.dp)) + .asPaddingValues() + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Buttons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton(onClick = {}) { + Text(text = "Enabled") + } + NiaOutlinedButton(onClick = {}) { + Text(text = "Enabled") + } + NiaTextButton(onClick = {}) { + Text(text = "Enabled") + } + } + } + // Disabled buttons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false + ) { + Text(text = "Disabled") + } + NiaOutlinedButton( + onClick = {}, + enabled = false + ) { + Text(text = "Disabled") + } + NiaTextButton( + onClick = {}, + enabled = false + ) { + Text(text = "Disabled") + } + } + } + // Buttons with leading icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Disabled buttons with leading icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Buttons with trailing icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Disabled buttons with trailing icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + enabled = false, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Small buttons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + small = true + ) { + Text(text = "Enabled") + } + NiaOutlinedButton( + onClick = {}, + small = true + ) { + Text(text = "Enabled") + } + NiaTextButton( + onClick = {}, + small = true + ) { + Text(text = "Enabled") + } + } + } + // Disabled small buttons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false, + small = true + ) { + Text(text = "Disabled") + } + NiaOutlinedButton( + onClick = {}, + enabled = false, + small = true + ) { + Text(text = "Disabled") + } + NiaTextButton( + onClick = {}, + enabled = false, + small = true + ) { + Text(text = "Disabled") + } + } + } + // Small buttons with leading icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Disabled small buttons with leading icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + leadingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Small buttons with trailing icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + small = true, + text = { Text(text = "Enabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Disabled small buttons with trailing icons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + NiaFilledButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaOutlinedButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + NiaTextButton( + onClick = {}, + enabled = false, + small = true, + text = { Text(text = "Disabled") }, + trailingIcon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + } + ) + } + } + // Dropdown menu + item { + NiaDropdownMenuButton( + text = { Text("Newest first") }, + items = listOf("Item 1", "Item 2", "Item 3"), + onItemClick = {}, + itemText = { item -> Text(item) } + ) + } + // Chips + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + var firstChecked by remember { mutableStateOf(false) } + NiaFilterChip( + checked = firstChecked, + onCheckedChange = { checked -> firstChecked = checked }, + text = { Text(text = "Enabled".uppercase()) } + ) + var secondChecked by remember { mutableStateOf(true) } + NiaFilterChip( + checked = secondChecked, + onCheckedChange = { checked -> secondChecked = checked }, + text = { Text(text = "Enabled".uppercase()) } + ) + var thirdChecked by remember { mutableStateOf(true) } + NiaFilterChip( + checked = thirdChecked, + onCheckedChange = { checked -> thirdChecked = checked }, + enabled = false, + text = { Text(text = "Disabled".uppercase()) } + ) + } + } + // Toggle buttons + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + var firstChecked by remember { mutableStateOf(false) } + NiaToggleButton( + checked = firstChecked, + onCheckedChange = { checked -> firstChecked = checked }, + icon = { + Icon( + painter = painterResource(id = NiaIcons.BookmarkBorder), + contentDescription = null + ) + }, + checkedIcon = { + Icon( + painter = painterResource(id = NiaIcons.Bookmark), + contentDescription = null + ) + } + ) + var secondChecked by remember { mutableStateOf(true) } + NiaToggleButton( + checked = secondChecked, + onCheckedChange = { checked -> secondChecked = checked }, + icon = { + Icon( + painter = painterResource(id = NiaIcons.BookmarkBorder), + contentDescription = null + ) + }, + checkedIcon = { + Icon( + painter = painterResource(id = NiaIcons.Bookmark), + contentDescription = null + ) + } + ) + var thirdChecked by remember { mutableStateOf(false) } + NiaToggleButton( + checked = thirdChecked, + onCheckedChange = { checked -> thirdChecked = checked }, + icon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + }, + checkedIcon = { + Icon(imageVector = NiaIcons.Check, contentDescription = null) + } + ) + var fourthChecked by remember { mutableStateOf(true) } + NiaToggleButton( + checked = fourthChecked, + onCheckedChange = { checked -> fourthChecked = checked }, + icon = { + Icon(imageVector = NiaIcons.Add, contentDescription = null) + }, + checkedIcon = { + Icon(imageVector = NiaIcons.Check, contentDescription = null) + } + ) + } + } + // View toggle + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + var firstExpanded by remember { mutableStateOf(false) } + NiaViewToggleButton( + expanded = firstExpanded, + onExpandedChange = { expanded -> firstExpanded = expanded }, + compactText = { Text(text = "Compact view") }, + expandedText = { Text(text = "Expanded view") } + ) + var secondExpanded by remember { mutableStateOf(true) } + NiaViewToggleButton( + expanded = secondExpanded, + onExpandedChange = { expanded -> secondExpanded = expanded }, + compactText = { Text(text = "Compact view") }, + expandedText = { Text(text = "Expanded view") } + ) + } + } + // Tags + item { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + var firstFollowed by remember { mutableStateOf(false) } + NiaTopicTag( + followed = firstFollowed, + onFollowClick = { firstFollowed = true }, + onUnfollowClick = { firstFollowed = false }, + onBrowseClick = {}, + text = { Text(text = "Topic".uppercase()) }, + followText = { Text(text = "Follow") }, + unFollowText = { Text(text = "Unfollow") }, + browseText = { Text(text = "Browse topic") } + ) + var secondFollowed by remember { mutableStateOf(true) } + NiaTopicTag( + followed = secondFollowed, + onFollowClick = { secondFollowed = true }, + onUnfollowClick = { secondFollowed = false }, + onBrowseClick = {}, + text = { Text(text = "Topic".uppercase()) }, + followText = { Text(text = "Follow") }, + unFollowText = { Text(text = "Unfollow") }, + browseText = { Text(text = "Browse topic") } + ) + } + } + // Tabs + item { + var selectedTabIndex by remember { mutableStateOf(0) } + val titles = listOf("Topics", "People") + NiaTabRow(selectedTabIndex = selectedTabIndex) { + titles.forEachIndexed { index, title -> + NiaTab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { Text(text = title) } + ) + } + } + } + // Navigation + item { + var selectedItem by remember { mutableStateOf(0) } + val items = listOf("For you", "Episodes", "Saved", "Interests") + val icons = listOf( + NiaIcons.UpcomingBorder, + NiaIcons.MenuBookBorder, + NiaIcons.BookmarksBorder + ) + val selectedIcons = listOf( + NiaIcons.Upcoming, + NiaIcons.MenuBook, + NiaIcons.Bookmarks + ) + val tagIcon = NiaIcons.Tag + NiaNavigationBar { + items.forEachIndexed { index, item -> + NiaNavigationBarItem( + icon = { + if (index == 3) { + Icon(imageVector = tagIcon, contentDescription = null) + } else { + Icon( + painter = painterResource(id = icons[index]), + contentDescription = item + ) + } + }, + selectedIcon = { + if (index == 3) { + Icon(imageVector = tagIcon, contentDescription = null) + } else { + Icon( + painter = painterResource(id = selectedIcons[index]), + contentDescription = item + ) + } + }, + label = { Text(item) }, + selected = selectedItem == index, + onClick = { selectedItem = index } + ) + } + } + } + } +} 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 new file mode 100644 index 000000000..95a28892a --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Chip.kt @@ -0,0 +1,90 @@ +/* + * 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/component/DropdownMenu.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt new file mode 100644 index 000000000..0e21fdc19 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/DropdownMenu.kt @@ -0,0 +1,132 @@ +/* + * 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.layout.Box +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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 + +/** + * Now in Android dropdown menu button with included trailing icon as well as text label and item + * content slots. + * + * @param items The list of items to display in the menu. + * @param onItemClick Called when the user clicks on a menu item. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param dismissOnItemClick Whether the menu should be dismissed when an item is clicked. + * @param itemText The text label content for a given item. + * @param itemLeadingIcon The leading icon content for a given item. + * @param itemTrailingIcon The trailing icon content for a given item. + */ +@Composable +fun NiaDropdownMenuButton( + items: List, + onItemClick: (item: T) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + dismissOnItemClick: Boolean = true, + text: @Composable () -> Unit, + itemText: @Composable (item: T) -> Unit, + itemLeadingIcon: @Composable ((item: T) -> Unit)? = null, + itemTrailingIcon: @Composable ((item: T) -> Unit)? = null +) { + var expanded by remember { mutableStateOf(false) } + Box(modifier = modifier) { + NiaOutlinedButton( + onClick = { expanded = true }, + enabled = enabled, + text = text, + trailingIcon = { + Icon( + imageVector = if (expanded) NiaIcons.ArrowDropUp else NiaIcons.ArrowDropDown, + contentDescription = null + ) + } + ) + NiaDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + items = items, + onItemClick = onItemClick, + dismissOnItemClick = dismissOnItemClick, + itemText = itemText, + itemLeadingIcon = itemLeadingIcon, + itemTrailingIcon = itemTrailingIcon + ) + } +} + +/** + * Now in Android dropdown menu with item content slots. Wraps Material 3 [DropdownMenu] and + * [DropdownMenuItem]. + * + * @param expanded Whether the menu is currently open and visible to the user. + * @param onDismissRequest Called when the user requests to dismiss the menu, such as by + * tapping outside the menu's bounds. + * @param items The list of items to display in the menu. + * @param onItemClick Called when the user clicks on a menu item. + * @param dismissOnItemClick Whether the menu should be dismissed when an item is clicked. + * @param itemText The text label content for a given item. + * @param itemLeadingIcon The leading icon content for a given item. + * @param itemTrailingIcon The trailing icon content for a given item. + */ +@Composable +fun NiaDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + items: List, + onItemClick: (item: T) -> Unit, + dismissOnItemClick: Boolean = true, + itemText: @Composable (item: T) -> Unit, + itemLeadingIcon: @Composable ((item: T) -> Unit)? = null, + itemTrailingIcon: @Composable ((item: T) -> Unit)? = null +) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest + ) { + items.forEach { item -> + DropdownMenuItem( + text = { itemText(item) }, + onClick = { + onItemClick(item) + if (dismissOnItemClick) onDismissRequest() + }, + leadingIcon = if (itemLeadingIcon != null) { + { itemLeadingIcon(item) } + } else { + null + }, + trailingIcon = if (itemTrailingIcon != null) { + { itemTrailingIcon(item) } + } else { + null + } + ) + } + } +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt new file mode 100644 index 000000000..28f906932 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Navigation.kt @@ -0,0 +1,176 @@ +/* + * 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.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * Now in Android navigation bar item with icon and label content slots. Wraps Material 3 + * [NavigationBarItem]. + * + * @param selected Whether this item is selected. + * @param onClick The callback to be invoked when this item is selected. + * @param icon The item icon content. + * @param modifier Modifier to be applied to this item. + * @param selectedIcon The item icon content when selected. + * @param enabled controls the enabled state of this item. When `false`, this item will not be + * clickable and will appear disabled to accessibility services. + * @param label The item text label content. + * @param alwaysShowLabel Whether to always show the label for this item. If false, the label will + * only be shown when this item is selected. + */ +@Composable +fun RowScope.NiaNavigationBarItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + selectedIcon: @Composable () -> Unit = icon, + enabled: Boolean = true, + label: @Composable (() -> Unit)? = null, + alwaysShowLabel: Boolean = true +) { + NavigationBarItem( + selected = selected, + onClick = onClick, + icon = if (selected) selectedIcon else icon, + modifier = modifier, + enabled = enabled, + label = label, + alwaysShowLabel = alwaysShowLabel, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), + selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), + indicatorColor = NiaNavigationDefaults.navigationIndicatorColor() + ) + ) +} + +/** + * Now in Android navigation bar with content slot. Wraps Material 3 [NavigationBar]. + * + * @param modifier Modifier to be applied to the navigation bar. + * @param content Destinations inside the navigation bar. This should contain multiple + * [NavigationBarItem]s. + */ +@Composable +fun NiaNavigationBar( + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + NavigationBar( + modifier = modifier, + containerColor = NiaNavigationDefaults.NavigationContainerColor, + contentColor = NiaNavigationDefaults.navigationContentColor(), + tonalElevation = 0.dp, + content = content + ) +} + +/** + * Now in Android navigation rail item with icon and label content slots. Wraps Material 3 + * [NavigationRailItem]. + * + * @param selected Whether this item is selected. + * @param onClick The callback to be invoked when this item is selected. + * @param icon The item icon content. + * @param modifier Modifier to be applied to this item. + * @param selectedIcon The item icon content when selected. + * @param enabled controls the enabled state of this item. When `false`, this item will not be + * clickable and will appear disabled to accessibility services. + * @param label The item text label content. + * @param alwaysShowLabel Whether to always show the label for this item. If false, the label will + * only be shown when this item is selected. + */ +@Composable +fun NiaNavigationRailItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + selectedIcon: @Composable () -> Unit = icon, + enabled: Boolean = true, + label: @Composable (() -> Unit)? = null, + alwaysShowLabel: Boolean = true +) { + NavigationRailItem( + selected = selected, + onClick = onClick, + icon = if (selected) selectedIcon else icon, + modifier = modifier, + enabled = enabled, + label = label, + alwaysShowLabel = alwaysShowLabel, + colors = NavigationRailItemDefaults.colors( + selectedIconColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), + selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), + indicatorColor = NiaNavigationDefaults.navigationIndicatorColor() + ) + ) +} + +/** + * Now in Android navigation rail with header and content slots. Wraps Material 3 [NavigationRail]. + * + * @param modifier Modifier to be applied to the navigation rail. + * @param header Optional header that may hold a floating action button or a logo. + * @param content Destinations inside the navigation rail. This should contain multiple + * [NavigationRailItem]s. + */ +@Composable +fun NiaNavigationRail( + modifier: Modifier = Modifier, + header: @Composable (ColumnScope.() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + NavigationRail( + modifier = modifier, + containerColor = NiaNavigationDefaults.NavigationContainerColor, + contentColor = NiaNavigationDefaults.navigationContentColor(), + header = header, + content = content + ) +} + +/** + * Now in Android navigation default values. + */ +object NiaNavigationDefaults { + val NavigationContainerColor = Color.Transparent + @Composable + fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant + @Composable + fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer + @Composable + fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt new file mode 100644 index 000000000..72bfb19ca --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tabs.kt @@ -0,0 +1,102 @@ +/* + * 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.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +/** + * Now in Android tab. Wraps Material 3 [Tab] and shifts text label down. + * + * @param selected Whether this tab is selected or not. + * @param onClick The callback to be invoked when this tab is selected. + * @param modifier Modifier to be applied to the tab. + * @param enabled Controls the enabled state of the tab. When `false`, this tab will not be + * clickable and will appear disabled to accessibility services. + * @param text The text label content. + */ +@Composable +fun NiaTab( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: @Composable () -> Unit +) { + Tab( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + text = { + val style = MaterialTheme.typography.labelLarge.copy(textAlign = TextAlign.Center) + ProvideTextStyle( + value = style, + content = { + Box(modifier = Modifier.padding(top = NiaTabDefaults.TabTopPadding)) { + text() + } + } + ) + } + ) +} + +/** + * Now in Android tab row. Wraps Material 3 [TabRow]. + * + * @param selectedTabIndex The index of the currently selected tab. + * @param modifier Modifier to be applied to the tab row. + * @param tabs The tabs inside this tab row. Typically this will be multiple [NiaTab]s. Each element + * inside this lambda will be measured and placed evenly across the row, each taking up equal space. + */ +@Composable +fun NiaTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + tabs: @Composable () -> Unit +) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), + height = 2.dp, + color = MaterialTheme.colorScheme.onSurface + ) + }, + tabs = tabs + ) +} + +object NiaTabDefaults { + val TabTopPadding = 7.dp +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt new file mode 100644 index 000000000..a47a85667 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/Tag.kt @@ -0,0 +1,91 @@ +/* + * 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.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun NiaTopicTag( + followed: Boolean, + onFollowClick: () -> Unit, + onUnfollowClick: () -> Unit, + onBrowseClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: @Composable () -> Unit, + followText: @Composable () -> Unit, + unFollowText: @Composable () -> Unit, + browseText: @Composable () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + Box(modifier = modifier) { + val containerColor = if (followed) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + } + NiaTextButton( + onClick = { expanded = true }, + enabled = enabled, + small = true, + colors = NiaButtonDefaults.textButtonColors( + containerColor = containerColor, + contentColor = contentColorFor(backgroundColor = containerColor), + disabledContainerColor = if (followed) { + MaterialTheme.colorScheme.onBackground.copy( + alpha = NiaButtonDefaults.DisabledButtonContentAlpha + ) + } else { + Color.Transparent + } + ), + text = text + ) + NiaDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + items = if (followed) listOf(UNFOLLOW, BROWSE) else listOf(FOLLOW, BROWSE), + onItemClick = { item -> + when (item) { + FOLLOW -> onFollowClick() + UNFOLLOW -> onUnfollowClick() + BROWSE -> onBrowseClick() + } + }, + itemText = { item -> + when (item) { + FOLLOW -> followText() + UNFOLLOW -> unFollowText() + BROWSE -> browseText() + } + } + ) + } +} + +private const val FOLLOW = 1 +private const val UNFOLLOW = 2 +private const val BROWSE = 3 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt new file mode 100644 index 000000000..19f0a4bc2 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ToggleButton.kt @@ -0,0 +1,87 @@ +/* + * 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.layout.Box +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp + +/** + * Now in Android toggle button with icon and checked icon content slots. Wraps Material 3 + * [IconButton]. + * + * @param checked Whether the toggle button is currently checked. + * @param onCheckedChange Called when the user clicks the toggle button and toggles checked. + * @param modifier Modifier to be applied to the toggle button. + * @param enabled Controls the enabled state of the toggle button. When `false`, this toggle button + * will not be clickable and will appear disabled to accessibility services. + * @param icon The icon content to show when unchecked. + * @param checkedIcon The icon content to show when checked. + */ +@Composable +fun NiaToggleButton( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + icon: @Composable () -> Unit, + checkedIcon: @Composable () -> Unit = icon +) { + val checkedColor = MaterialTheme.colorScheme.primaryContainer + val checkedRadius = with(LocalDensity.current) { + (NiaToggleButtonDefaults.ToggleButtonSize / 2).toPx() + } + IconButton( + onClick = { onCheckedChange(!checked) }, + modifier = Modifier + .toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {}) + .drawBehind { + if (checked) drawCircle( + color = checkedColor, + radius = checkedRadius + ) + } + .then(modifier), + enabled = enabled, + content = { + Box( + modifier = Modifier.sizeIn( + maxWidth = NiaToggleButtonDefaults.ToggleButtonIconSize, + maxHeight = NiaToggleButtonDefaults.ToggleButtonIconSize + ) + ) { + if (checked) checkedIcon() else icon() + } + } + ) +} + +/** + * Now in Android toggle button default values. + */ +object NiaToggleButtonDefaults { + val ToggleButtonSize = 40.dp + val ToggleButtonIconSize = 18.dp +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt new file mode 100644 index 000000000..e9779ad70 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/component/ViewToggle.kt @@ -0,0 +1,57 @@ +/* + * 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.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons + +/** + * Now in Android view toggle button with included trailing icon as well as compact and expanded + * text label content slots. + * + * @param expanded Whether the view toggle is currently in expanded mode or compact mode. + * @param onExpandedChange Called when the user clicks the button and toggles the mode. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param compactText The text label content to show in expanded mode. + * @param expandedText The text label content to show in compact mode. + */ +@Composable +fun NiaViewToggleButton( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + compactText: @Composable () -> Unit, + expandedText: @Composable () -> Unit +) { + NiaTextButton( + onClick = { onExpandedChange(!expanded) }, + modifier = modifier, + enabled = enabled, + text = if (expanded) expandedText else compactText, + trailingIcon = { + Icon( + imageVector = if (expanded) NiaIcons.ViewDay else NiaIcons.ShortText, + contentDescription = null + ) + } + ) +} diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt new file mode 100644 index 000000000..c89407059 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/icon/NiaIcons.kt @@ -0,0 +1,69 @@ +/* + * 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.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.AccountCircle +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.ArrowDropDown +import androidx.compose.material.icons.rounded.ArrowDropUp +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.PlayArrow +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.ShortText +import androidx.compose.material.icons.rounded.Tag +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 + +/** + * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. + */ +object NiaIcons { + val AccountCircle = Icons.Outlined.AccountCircle + val Add = Icons.Rounded.Add + val ArrowBack = Icons.Rounded.ArrowBack + val ArrowDropDown = Icons.Rounded.ArrowDropDown + val ArrowDropUp = Icons.Rounded.ArrowDropUp + val Bookmark = R.drawable.ic_bookmark + val BookmarkBorder = R.drawable.ic_bookmark_border + val Bookmarks = R.drawable.ic_bookmarks + val BookmarksBorder = R.drawable.ic_bookmarks_border + val Check = Icons.Rounded.Check + val Close = Icons.Rounded.Close + val ExpandLess = Icons.Rounded.ExpandLess + val Fullscreen = Icons.Rounded.Fullscreen + val MenuBook = R.drawable.ic_menu_book + val MenuBookBorder = R.drawable.ic_menu_book_border + val MoreVert = Icons.Default.MoreVert + val PlayArrow = Icons.Rounded.PlayArrow + val Search = Icons.Rounded.Search + val ShortText = Icons.Rounded.ShortText + val Tag = Icons.Rounded.Tag + val Upcoming = R.drawable.ic_upcoming + val UpcomingBorder = R.drawable.ic_upcoming_border + val ViewDay = Icons.Rounded.ViewDay + val VolumeOff = Icons.Rounded.VolumeOff + val VolumeUp = Icons.Rounded.VolumeUp +} diff --git a/core-ui/src/main/res/drawable/ic_bookmark.xml b/core-ui/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 000000000..5a5eaabdd --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,24 @@ + + + + diff --git a/core-ui/src/main/res/drawable/ic_bookmark_border.xml b/core-ui/src/main/res/drawable/ic_bookmark_border.xml new file mode 100644 index 000000000..d5db05793 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_bookmark_border.xml @@ -0,0 +1,24 @@ + + + + diff --git a/core-ui/src/main/res/drawable/ic_bookmarks.xml b/core-ui/src/main/res/drawable/ic_bookmarks.xml new file mode 100644 index 000000000..0c2c56f13 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_bookmarks.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_bookmarks_border.xml b/core-ui/src/main/res/drawable/ic_bookmarks_border.xml new file mode 100644 index 000000000..9e342a7e3 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_bookmarks_border.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_menu_book.xml b/core-ui/src/main/res/drawable/ic_menu_book.xml new file mode 100644 index 000000000..57861d546 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_menu_book.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_menu_book_border.xml b/core-ui/src/main/res/drawable/ic_menu_book_border.xml new file mode 100644 index 000000000..9d8244f0c --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_menu_book_border.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_upcoming.xml b/core-ui/src/main/res/drawable/ic_upcoming.xml new file mode 100644 index 000000000..27e25b850 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_upcoming.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_upcoming_border.xml b/core-ui/src/main/res/drawable/ic_upcoming_border.xml new file mode 100644 index 000000000..4275e7571 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_upcoming_border.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 005d19586..f5073961d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidxActivity = "1.4.0" androidxAppCompat = "1.3.0" androidxCompose = "1.2.0-alpha03" androidxMaterialWindow = "1.2.0-SNAPSHOT" -androidxComposeMaterial3 = "1.0.0-alpha03" +androidxComposeMaterial3 = "1.0.0-alpha07" androidxCore = "1.7.0" androidxDataStore = "1.0.0" androidxEspresso = "3.3.0"