🚀 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
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.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.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 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.domain.model.ButtonItem
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
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
|
@Composable
|
||||||
fun SegmentedButtonWithTitle(
|
fun SegmentedButtonWithTitle(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String,
|
title: String,
|
||||||
buttons: List<ButtonItem>,
|
buttons: List<ButtonItem>,
|
||||||
locked: Boolean,
|
enabled: Boolean = true,
|
||||||
horizontalPadding: Dp = 18.dp,
|
horizontalPadding: Dp = 18.dp,
|
||||||
verticalPadding: Dp = 8.dp,
|
verticalPadding: Dp = 8.dp,
|
||||||
onClick: (ButtonItem) -> Unit
|
onClick: (ButtonItem) -> Unit
|
||||||
) {
|
) {
|
||||||
val colors = SegmentedButtonDefaults.colors()
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -42,12 +74,22 @@ fun SegmentedButtonWithTitle(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
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 ->
|
buttons.forEachIndexed { index, buttonItem ->
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
enabled = locked,
|
button = buttonItem,
|
||||||
selected = buttonItem.selected,
|
enabled = enabled,
|
||||||
onClick = { onClick(buttonItem) },
|
|
||||||
shape = when (index) {
|
shape = when (index) {
|
||||||
buttons.lastIndex -> RoundedCornerShape(
|
buttons.lastIndex -> RoundedCornerShape(
|
||||||
topEndPercent = 100,
|
topEndPercent = 100,
|
||||||
|
|
@ -61,28 +103,86 @@ fun SegmentedButtonWithTitle(
|
||||||
|
|
||||||
else -> RoundedCornerShape(0)
|
else -> RoundedCornerShape(0)
|
||||||
},
|
},
|
||||||
colors = SegmentedButtonDefaults.colors(
|
onClick = { onClick(buttonItem) }
|
||||||
disabledInactiveBorderColor = colors.activeBorderColor,
|
)
|
||||||
disabledInactiveContentColor = colors.inactiveContentColor,
|
}
|
||||||
disabledActiveContainerColor = colors.activeContainerColor,
|
}
|
||||||
disabledActiveContentColor = colors.activeContentColor,
|
}
|
||||||
disabledInactiveContainerColor = colors.inactiveContainerColor,
|
}
|
||||||
disabledActiveBorderColor = colors.activeBorderColor,
|
}
|
||||||
),
|
}
|
||||||
label = {
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
text = buttonItem.title,
|
text = button.title,
|
||||||
maxLines = 1,
|
style = button.textStyle,
|
||||||
overflow = TextOverflow.Ellipsis
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
color = if (button.selected) colors.activeContentColor
|
||||||
}
|
else colors.inactiveContentColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,16 @@ fun LazyItemScope.DarkThemeSetting(
|
||||||
SegmentedButtonWithTitle(
|
SegmentedButtonWithTitle(
|
||||||
title = stringResource(id = R.string.dark_theme_option),
|
title = stringResource(id = R.string.dark_theme_option),
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
locked = true,
|
|
||||||
buttons = DarkTheme.entries.map {
|
buttons = DarkTheme.entries.map {
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
it.toString(),
|
it.toString(),
|
||||||
when (it) {
|
title = when (it) {
|
||||||
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
|
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
|
||||||
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
|
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
|
||||||
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
|
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
|
||||||
},
|
},
|
||||||
MaterialTheme.typography.labelLarge,
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
it == state.value.darkTheme
|
selected = it == state.value.darkTheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,16 @@ fun LazyItemScope.PureDarkSetting(
|
||||||
) {
|
) {
|
||||||
SegmentedButtonWithTitle(
|
SegmentedButtonWithTitle(
|
||||||
title = stringResource(id = R.string.pure_dark_option),
|
title = stringResource(id = R.string.pure_dark_option),
|
||||||
locked = true,
|
|
||||||
buttons = PureDark.entries.map {
|
buttons = PureDark.entries.map {
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
it.toString(),
|
id = it.toString(),
|
||||||
when (it) {
|
title = when (it) {
|
||||||
PureDark.OFF -> stringResource(id = R.string.pure_dark_off)
|
PureDark.OFF -> stringResource(id = R.string.pure_dark_off)
|
||||||
PureDark.ON -> stringResource(id = R.string.pure_dark_on)
|
PureDark.ON -> stringResource(id = R.string.pure_dark_on)
|
||||||
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
|
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
|
||||||
},
|
},
|
||||||
MaterialTheme.typography.labelLarge,
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
it == state.value.pureDark
|
selected = it == state.value.pureDark
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,17 @@ fun LazyItemScope.ThemeContrastSetting(
|
||||||
) {
|
) {
|
||||||
SegmentedButtonWithTitle(
|
SegmentedButtonWithTitle(
|
||||||
title = stringResource(id = R.string.theme_contrast_option),
|
title = stringResource(id = R.string.theme_contrast_option),
|
||||||
locked = state.value.theme != Theme.DYNAMIC,
|
enabled = state.value.theme != Theme.DYNAMIC,
|
||||||
buttons = ThemeContrast.entries.map {
|
buttons = ThemeContrast.entries.map {
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
it.toString(),
|
id = it.toString(),
|
||||||
when (it) {
|
title = when (it) {
|
||||||
ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
|
ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
|
||||||
ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
|
ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
|
||||||
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
|
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
|
||||||
},
|
},
|
||||||
MaterialTheme.typography.labelLarge,
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
it == state.value.themeContrast
|
selected = it == state.value.themeContrast
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,12 @@ fun LazyItemScope.FontFamilySetting(
|
||||||
chips = Constants.FONTS
|
chips = Constants.FONTS
|
||||||
.map {
|
.map {
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
it.id,
|
id = it.id,
|
||||||
it.fontName.asString(),
|
title = it.fontName.asString(),
|
||||||
MaterialTheme.typography.labelLarge.copy(
|
textStyle = MaterialTheme.typography.labelLarge.copy(
|
||||||
fontFamily = it.font
|
fontFamily = it.font
|
||||||
),
|
),
|
||||||
it.id == fontFamily.id
|
selected = it.id == fontFamily.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
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.domain.util.Constants
|
||||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||||
import ua.acclorite.book_story.presentation.data.MainState
|
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.
|
* Font Style setting.
|
||||||
|
|
@ -30,27 +30,27 @@ fun LazyItemScope.FontStyleSetting(
|
||||||
} ?: Constants.FONTS[0]
|
} ?: Constants.FONTS[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
ChipsWithTitle(
|
SegmentedButtonWithTitle(
|
||||||
title = stringResource(id = R.string.font_style_option),
|
title = stringResource(id = R.string.font_style_option),
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
chips = listOf(
|
buttons = listOf(
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
"normal",
|
id = "normal",
|
||||||
stringResource(id = R.string.font_style_normal),
|
title = stringResource(id = R.string.font_style_normal),
|
||||||
MaterialTheme.typography.labelLarge.copy(
|
textStyle = MaterialTheme.typography.labelLarge.copy(
|
||||||
fontFamily = fontFamily.font,
|
fontFamily = fontFamily.font,
|
||||||
fontStyle = FontStyle.Normal
|
fontStyle = FontStyle.Normal
|
||||||
),
|
),
|
||||||
!state.value.isItalic!!
|
selected = !state.value.isItalic!!
|
||||||
),
|
),
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
"italic",
|
id = "italic",
|
||||||
stringResource(id = R.string.font_style_italic),
|
title = stringResource(id = R.string.font_style_italic),
|
||||||
MaterialTheme.typography.labelLarge.copy(
|
textStyle = MaterialTheme.typography.labelLarge.copy(
|
||||||
fontFamily = fontFamily.font,
|
fontFamily = fontFamily.font,
|
||||||
fontStyle = FontStyle.Italic
|
fontStyle = FontStyle.Italic
|
||||||
),
|
),
|
||||||
state.value.isItalic!!
|
selected = state.value.isItalic!!
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@ fun StartSettings(
|
||||||
val languages = remember(mainState.value.language) {
|
val languages = remember(mainState.value.language) {
|
||||||
Constants.LANGUAGES.sortedBy { it.second }.map {
|
Constants.LANGUAGES.sortedBy { it.second }.map {
|
||||||
ButtonItem(
|
ButtonItem(
|
||||||
it.first,
|
id = it.first,
|
||||||
it.second,
|
title = it.second,
|
||||||
TextStyle(),
|
textStyle = TextStyle(),
|
||||||
it.first == mainState.value.language
|
selected = it.first == mainState.value.language
|
||||||
)
|
)
|
||||||
}.sortedBy { it.title }
|
}.sortedBy { it.title }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue