🚀 Custom implementation of SegmentedButton
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
This commit is contained in:
parent
7ccddd680e
commit
3766e4b230
7 changed files with 170 additions and 72 deletions
|
|
@ -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<ButtonItem>,
|
||||
locked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
horizontalPadding: Dp = 18.dp,
|
||||
verticalPadding: Dp = 8.dp,
|
||||
onClick: (ButtonItem) -> Unit
|
||||
) {
|
||||
val colors = SegmentedButtonDefaults.colors()
|
||||
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -42,12 +74,22 @@ fun SegmentedButtonWithTitle(
|
|||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
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(
|
||||
enabled = locked,
|
||||
selected = buttonItem.selected,
|
||||
onClick = { onClick(buttonItem) },
|
||||
button = buttonItem,
|
||||
enabled = enabled,
|
||||
shape = when (index) {
|
||||
buttons.lastIndex -> RoundedCornerShape(
|
||||
topEndPercent = 100,
|
||||
|
|
@ -61,28 +103,86 @@ fun SegmentedButtonWithTitle(
|
|||
|
||||
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 = {
|
||||
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 = buttonItem.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
text = button.title,
|
||||
style = button.textStyle,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (button.selected) colors.activeContentColor
|
||||
else colors.inactiveContentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue