From 3766e4b2309bb4d763e352ca19d1013f211ce97d Mon Sep 17 00:00:00 2001 From: Acclorite <140836141+Acclorite@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:15:36 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Custom=20implementation=20of=20S?= =?UTF-8?q?egmentedButton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced material3 SegmentedButton with custom implementation (following Material3 guidelines). Fixed many flaws such as: - No horizontal scrolling if overlapping - If text is too long, can reach border --- .../components/SegmentedButtonWithTitle.kt | 178 ++++++++++++++---- .../components/settings/DarkThemeSetting.kt | 7 +- .../components/settings/PureDarkSetting.kt | 9 +- .../settings/ThemeContrastSetting.kt | 10 +- .../components/settings/FontFamilySetting.kt | 8 +- .../components/settings/FontStyleSetting.kt | 22 +-- .../screens/start/components/StartSettings.kt | 8 +- 7 files changed, 170 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt index 7fa647e2..6fddf8cc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt @@ -1,38 +1,70 @@ package ua.acclorite.book_story.presentation.screens.settings.components +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.SegmentedButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Icon +import androidx.compose.material3.SegmentedButtonColors import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.presentation.components.CategoryTitle +import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility /** - * Segmented Button with Title. Contains [buttons]. + * Segmented Button with Title. + * Uses custom implementation(material3 one has many flaws). + * Contains [buttons]. + * + * @param modifier Modifier. + * @param title Title of the buttons. + * @param buttons [ButtonItem]s. + * @param enabled Whether button is enabled. + * @param horizontalPadding Horizontal item padding. + * @param verticalPadding Vertical item padding. + * @param onClick OnClick callback. */ @Composable fun SegmentedButtonWithTitle( modifier: Modifier = Modifier, title: String, buttons: List, - locked: Boolean, + enabled: Boolean = true, horizontalPadding: Dp = 18.dp, verticalPadding: Dp = 8.dp, onClick: (ButtonItem) -> Unit ) { - val colors = SegmentedButtonDefaults.colors() - Column( modifier .fillMaxWidth() @@ -42,50 +74,118 @@ fun SegmentedButtonWithTitle( Spacer(modifier = Modifier.height(8.dp)) - SingleChoiceSegmentedButtonRow { - buttons.forEachIndexed { index, buttonItem -> - SegmentedButton( - enabled = locked, - selected = buttonItem.selected, - onClick = { onClick(buttonItem) }, - shape = when (index) { - buttons.lastIndex -> RoundedCornerShape( - topEndPercent = 100, - bottomEndPercent = 100 + LazyRow(Modifier.fillMaxWidth()) { + item { + Row( + Modifier + .clip(CircleShape) + .border( + width = 0.5.dp, + color = SegmentedButtonDefaults.colors().activeBorderColor, + shape = CircleShape ) + .padding(0.5.dp) + ) { + buttons.forEachIndexed { index, buttonItem -> + SegmentedButton( + button = buttonItem, + enabled = enabled, + shape = when (index) { + buttons.lastIndex -> RoundedCornerShape( + topEndPercent = 100, + bottomEndPercent = 100 + ) - 0 -> RoundedCornerShape( - topStartPercent = 100, - bottomStartPercent = 100 - ) + 0 -> RoundedCornerShape( + topStartPercent = 100, + bottomStartPercent = 100 + ) - else -> RoundedCornerShape(0) - }, - colors = SegmentedButtonDefaults.colors( - disabledInactiveBorderColor = colors.activeBorderColor, - disabledInactiveContentColor = colors.inactiveContentColor, - disabledActiveContainerColor = colors.activeContainerColor, - disabledActiveContentColor = colors.activeContentColor, - disabledInactiveContainerColor = colors.inactiveContainerColor, - disabledActiveBorderColor = colors.activeBorderColor, - ), - label = { - Text( - text = buttonItem.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis + else -> RoundedCornerShape(0) + }, + onClick = { onClick(buttonItem) } ) } - ) + } } } } } +/** + * Custom Segmented button. + * Adjusts width based on component name. + * + * @param button [ButtonItem]. + * @param enabled Whether can be clicked. + * @param shape Shape of the button. For proper implementation should have 100% corners on first and last buttons(start and end). + * @param colors [SegmentedButtonColors]. + * @param onClick OnClick callback. + */ +@Composable +private fun SegmentedButton( + button: ButtonItem, + enabled: Boolean, + shape: RoundedCornerShape, + colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(), + onClick: () -> Unit +) { + Row( + modifier = Modifier + .height(40.dp) + .clip(shape) + .clickable(enabled = enabled && !button.selected) { + onClick() + } + .border( + width = 0.5.dp, + color = colors.activeBorderColor, + shape = shape + ) + .padding(0.5.dp) + .background( + if (button.selected) colors.activeContainerColor + else Color.Transparent, + shape = shape + ) + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CustomAnimatedVisibility( + visible = button.selected, + enter = expandHorizontally() + + slideInVertically(initialOffsetY = { it / 2 }) + + scaleIn() + + fadeIn(), + exit = shrinkHorizontally() + + slideOutVertically(targetOffsetY = { it / 2 }) + + scaleOut() + + fadeOut() + ) { + Row { + Icon( + imageVector = Icons.Default.Done, + contentDescription = stringResource(id = R.string.selected_content_desc), + modifier = Modifier + .size(18.dp), + tint = colors.activeContentColor + ) + Spacer(modifier = Modifier.width(8.dp)) + } + } + + Text( + text = button.title, + style = button.textStyle, + fontWeight = FontWeight.Medium, + color = if (button.selected) colors.activeContentColor + else colors.inactiveContentColor + ) + } +} + - - diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt index b661da8c..4a534ec6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt @@ -25,17 +25,16 @@ fun LazyItemScope.DarkThemeSetting( SegmentedButtonWithTitle( title = stringResource(id = R.string.dark_theme_option), modifier = Modifier.animateItem(), - locked = true, buttons = DarkTheme.entries.map { ButtonItem( it.toString(), - when (it) { + title = when (it) { DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off) DarkTheme.ON -> stringResource(id = R.string.dark_theme_on) DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system) }, - MaterialTheme.typography.labelLarge, - it == state.value.darkTheme + textStyle = MaterialTheme.typography.labelLarge, + selected = it == state.value.darkTheme ) } ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt index 0b1815ee..37a47365 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt @@ -33,17 +33,16 @@ fun LazyItemScope.PureDarkSetting( ) { SegmentedButtonWithTitle( title = stringResource(id = R.string.pure_dark_option), - locked = true, buttons = PureDark.entries.map { ButtonItem( - it.toString(), - when (it) { + id = it.toString(), + title = when (it) { PureDark.OFF -> stringResource(id = R.string.pure_dark_off) PureDark.ON -> stringResource(id = R.string.pure_dark_on) PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver) }, - MaterialTheme.typography.labelLarge, - it == state.value.pureDark + textStyle = MaterialTheme.typography.labelLarge, + selected = it == state.value.pureDark ) } ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt index 7b21d698..ae1c910c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt @@ -54,17 +54,17 @@ fun LazyItemScope.ThemeContrastSetting( ) { SegmentedButtonWithTitle( title = stringResource(id = R.string.theme_contrast_option), - locked = state.value.theme != Theme.DYNAMIC, + enabled = state.value.theme != Theme.DYNAMIC, buttons = ThemeContrast.entries.map { ButtonItem( - it.toString(), - when (it) { + id = it.toString(), + title = when (it) { ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard) ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium) ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high) }, - MaterialTheme.typography.labelLarge, - it == state.value.themeContrast + textStyle = MaterialTheme.typography.labelLarge, + selected = it == state.value.themeContrast ) } ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt index 9f89d94a..ee3ddde3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt @@ -35,12 +35,12 @@ fun LazyItemScope.FontFamilySetting( chips = Constants.FONTS .map { ButtonItem( - it.id, - it.fontName.asString(), - MaterialTheme.typography.labelLarge.copy( + id = it.id, + title = it.fontName.asString(), + textStyle = MaterialTheme.typography.labelLarge.copy( fontFamily = it.font ), - it.id == fontFamily.id + selected = it.id == fontFamily.id ) }, onClick = { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt index 57044070..05e2c796 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt @@ -13,7 +13,7 @@ import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState -import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle +import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle /** * Font Style setting. @@ -30,27 +30,27 @@ fun LazyItemScope.FontStyleSetting( } ?: Constants.FONTS[0] } - ChipsWithTitle( + SegmentedButtonWithTitle( title = stringResource(id = R.string.font_style_option), modifier = Modifier.animateItem(), - chips = listOf( + buttons = listOf( ButtonItem( - "normal", - stringResource(id = R.string.font_style_normal), - MaterialTheme.typography.labelLarge.copy( + id = "normal", + title = stringResource(id = R.string.font_style_normal), + textStyle = MaterialTheme.typography.labelLarge.copy( fontFamily = fontFamily.font, fontStyle = FontStyle.Normal ), - !state.value.isItalic!! + selected = !state.value.isItalic!! ), ButtonItem( - "italic", - stringResource(id = R.string.font_style_italic), - MaterialTheme.typography.labelLarge.copy( + id = "italic", + title = stringResource(id = R.string.font_style_italic), + textStyle = MaterialTheme.typography.labelLarge.copy( fontFamily = fontFamily.font, fontStyle = FontStyle.Italic ), - state.value.isItalic!! + selected = state.value.isItalic!! ), ), onClick = { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt index 40b8f799..3d3aa75f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt @@ -64,10 +64,10 @@ fun StartSettings( val languages = remember(mainState.value.language) { Constants.LANGUAGES.sortedBy { it.second }.map { ButtonItem( - it.first, - it.second, - TextStyle(), - it.first == mainState.value.language + id = it.first, + title = it.second, + textStyle = TextStyle(), + selected = it.first == mainState.value.language ) }.sortedBy { it.title } }