New UI of toggle group buttons

This commit is contained in:
T8RIN 2025-05-16 01:46:41 +03:00
parent 32a8c8a8ec
commit 7abec4fa5a
6 changed files with 202 additions and 226 deletions

View File

@ -17,8 +17,8 @@
package ru.tech.imageresizershrinker.core.ui.widget.buttons
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
@ -26,19 +26,19 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -53,9 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp
import com.gigamole.composeshadowsplus.rsblur.rsBlurShadow
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
import ru.tech.imageresizershrinker.core.ui.theme.outlineVariant
import ru.tech.imageresizershrinker.core.ui.utils.helper.ProvidesValue
@ -130,7 +128,6 @@ fun ToggleGroupButton(
selectedIndex: Int,
itemContent: @Composable (item: Int) -> Unit,
title: @Composable RowScope.() -> Unit = {},
buttonIcon: (@Composable () -> Unit)? = null,
onIndexChange: (Int) -> Unit,
inactiveButtonColor: Color = MaterialTheme.colorScheme.surface,
activeButtonColor: Color = MaterialTheme.colorScheme.secondary,
@ -160,93 +157,70 @@ fun ToggleGroupButton(
)
val scrollState = rememberScrollState()
LocalMinimumInteractiveComponentSize.ProvidesValue(Dp.Unspecified) {
SingleChoiceSegmentedButtonRow(
space = max(settingsState.borderWidth, 1.dp),
modifier = Modifier
.height(IntrinsicSize.Max)
.then(
if (isScrollable) {
Modifier
.fadingEdges(scrollState)
.horizontalScroll(scrollState)
} else Modifier.fillMaxWidth()
)
.padding(start = 6.dp, end = 6.dp, bottom = 8.dp, top = 8.dp)
MaterialTheme(
motionScheme = object : MotionScheme by MotionScheme.expressive() {
override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> = tween(400)
}
) {
repeat(itemCount) { index ->
val shape = SegmentedButtonDefaults.itemShape(index, itemCount)
val activeContainerColor = if (enabled) {
activeButtonColor
} else {
MaterialTheme.colorScheme.surfaceContainer
}
val selected = index == selectedIndex
val focus = LocalFocusManager.current
Row(
modifier = Modifier
.height(IntrinsicSize.Max)
.then(
if (isScrollable) {
Modifier
.fadingEdges(scrollState)
.horizontalScroll(scrollState)
} else Modifier.fillMaxWidth()
)
.padding(
start = 6.dp,
end = 6.dp,
bottom = 8.dp,
top = 8.dp
),
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
) {
repeat(itemCount) { index ->
val activeContainerColor = if (enabled) {
activeButtonColor
} else {
MaterialTheme.colorScheme.surfaceContainer
}
SegmentedButton(
enabled = enabled,
onClick = {
focus.clearFocus()
haptics.performHapticFeedback(
HapticFeedbackType.LongPress
)
onIndexChange(index)
},
icon = {
if (buttonIcon == null) SegmentedButtonDefaults.Icon(index == selectedIndex)
else buttonIcon()
},
border = BorderStroke(
width = settingsState.borderWidth,
color = MaterialTheme.colorScheme.outlineVariant()
),
selected = true,
colors = SegmentedButtonDefaults.colors(
activeBorderColor = animateColorAsState(
if (selected) {
MaterialTheme.colorScheme.outlineVariant()
} else MaterialTheme.colorScheme.outline
).value,
activeContainerColor = animateColorAsState(
if (selected) {
activeContainerColor
} else inactiveButtonColor
).value,
activeContentColor = animateColorAsState(
contentColorFor(
if (selected) {
activeContainerColor
} else inactiveButtonColor
val selected = index == selectedIndex
val focus = LocalFocusManager.current
ToggleButton(
enabled = enabled,
onCheckedChange = {
focus.clearFocus()
haptics.performHapticFeedback(
HapticFeedbackType.LongPress
)
onIndexChange(index)
},
border = BorderStroke(
width = settingsState.borderWidth,
color = MaterialTheme.colorScheme.outlineVariant(
onTopOf = if (selected) activeContainerColor
else inactiveButtonColor
)
).value,
disabledInactiveContainerColor = MaterialTheme.colorScheme.outlineVariant.copy(
0.38f
).compositeOver(MaterialTheme.colorScheme.surface),
disabledActiveContainerColor = MaterialTheme.colorScheme.outlineVariant.copy(
0.38f
).compositeOver(MaterialTheme.colorScheme.surface)
),
modifier = Modifier
.fillMaxHeight()
.then(
if (!(settingsState.borderWidth >= 0.dp || !settingsState.drawButtonShadows)) {
Modifier.rsBlurShadow(
shape = SegmentedButtonDefaults.itemShape(
index = itemCount - 1 - index,
count = itemCount
),
radius = animateDpAsState(
if (selected) 2.dp
else 1.dp
).value
)
} else {
Modifier
}
),
shape = shape
) {
itemContent(index)
colors = ToggleButtonDefaults.toggleButtonColors(
containerColor = inactiveButtonColor,
contentColor = contentColorFor(inactiveButtonColor),
checkedContainerColor = activeContainerColor,
checkedContentColor = contentColorFor(activeContainerColor)
),
checked = selected,
shapes = when (index) {
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
itemCount - 1 -> ButtonGroupDefaults.connectedTrailingButtonShapes()
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
}
) {
itemContent(index)
}
}
}
}

View File

@ -59,6 +59,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.core.ui.theme.inverse
import ru.tech.imageresizershrinker.core.ui.utils.helper.ContextUtils.pasteColorFromClipboard
import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalContainerColor
import ru.tech.imageresizershrinker.core.ui.utils.provider.ProvideContainerDefaults
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.hapticsClickable
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.hapticsCombinedClickable
import ru.tech.imageresizershrinker.core.ui.widget.modifier.animateShape
@ -101,152 +103,94 @@ fun ColorSelectionRow(
val itemSize = 42.dp
LazyRow(
state = listState,
modifier = modifier
.fillMaxWidth()
.height(64.dp)
.fadingEdges(listState),
userScrollEnabled = allowScroll,
contentPadding = contentPadding,
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
ProvideContainerDefaults(
color = LocalContainerColor.current
) {
item {
val background = customColor ?: MaterialTheme.colorScheme.primary
val isSelected = customColor != null
val shape = animateShape(
if (isSelected) RoundedCornerShape(8.dp)
else RoundedCornerShape(itemSize / 2)
)
LazyRow(
state = listState,
modifier = modifier
.fillMaxWidth()
.height(64.dp)
.fadingEdges(listState),
userScrollEnabled = allowScroll,
contentPadding = contentPadding,
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
item {
val background = customColor ?: MaterialTheme.colorScheme.primary
val isSelected = customColor != null
val shape = animateShape(
if (isSelected) RoundedCornerShape(8.dp)
else RoundedCornerShape(itemSize / 2)
)
Box(
Modifier
.size(itemSize)
.aspectRatio(1f)
.scale(
animateFloatAsState(
targetValue = if (isSelected) 0.7f else 1f,
animationSpec = tween(400)
).value
)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) 45f else 0f,
animationSpec = tween(400)
).value
)
.container(
shape = shape,
color = background,
resultPadding = 0.dp
)
.transparencyChecker()
.background(background, shape)
.hapticsCombinedClickable(
onLongClick = {
context.pasteColorFromClipboard(
onPastedColor = {
val color = if (allowAlpha) Color(it)
else Color(it).copy(1f)
onValueChange(color)
customColor = color
},
onPastedColorFailure = { message ->
scope.launch {
toastHostState.showToast(
message = message,
icon = Icons.Outlined.Error
)
}
}
)
},
onClick = {
showColorPicker = true
}
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Palette,
contentDescription = null,
tint = background.inverse(
fraction = {
if (it) 0.8f
else 0.5f
},
darkMode = background.luminance() < 0.3f
),
Box(
modifier = Modifier
.size(32.dp)
.background(
color = background.copy(alpha = 1f),
shape = shape
)
.padding(4.dp)
.rotate(
.size(itemSize)
.aspectRatio(1f)
.scale(
animateFloatAsState(
targetValue = if (isSelected) -45f else 0f,
targetValue = if (isSelected) 0.7f else 1f,
animationSpec = tween(400)
).value
)
)
}
}
items(
items = defaultColors,
key = { it.toArgb() }
) { color ->
val isSelected = value == color && customColor == null
val shape = animateShape(
if (isSelected) RoundedCornerShape(8.dp)
else RoundedCornerShape(itemSize / 2)
)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) 45f else 0f,
animationSpec = tween(400)
).value
)
.container(
shape = shape,
color = background,
resultPadding = 0.dp
)
.transparencyChecker()
.background(background, shape)
.hapticsCombinedClickable(
onLongClick = {
context.pasteColorFromClipboard(
onPastedColor = {
val color = if (allowAlpha) Color(it)
else Color(it).copy(1f)
Box(
Modifier
.size(itemSize)
.aspectRatio(1f)
.scale(
animateFloatAsState(
targetValue = if (isSelected) 0.7f else 1f,
animationSpec = tween(400)
).value
)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) 45f else 0f,
animationSpec = tween(400)
).value
)
.container(
shape = shape,
color = color,
resultPadding = 0.dp
)
.transparencyChecker()
.background(color, shape)
.hapticsClickable {
onValueChange(color.copy(if (allowAlpha) color.alpha else 1f))
customColor = null
},
contentAlignment = Alignment.Center
) {
AnimatedVisibility(isSelected) {
onValueChange(color)
customColor = color
},
onPastedColorFailure = { message ->
scope.launch {
toastHostState.showToast(
message = message,
icon = Icons.Outlined.Error
)
}
}
)
},
onClick = {
showColorPicker = true
}
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.DoneAll,
imageVector = Icons.Rounded.Palette,
contentDescription = null,
tint = color.inverse(
tint = background.inverse(
fraction = {
if (it) 0.8f
else 0.5f
},
darkMode = color.luminance() < 0.3f
darkMode = background.luminance() < 0.3f
),
modifier = Modifier
.size(24.dp)
.size(32.dp)
.background(
color = background.copy(alpha = 1f),
shape = shape
)
.padding(4.dp)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) -45f else 0f,
@ -256,6 +200,68 @@ fun ColorSelectionRow(
)
}
}
items(
items = defaultColors,
key = { it.toArgb() }
) { color ->
val isSelected = value == color && customColor == null
val shape = animateShape(
if (isSelected) RoundedCornerShape(8.dp)
else RoundedCornerShape(itemSize / 2)
)
Box(
Modifier
.size(itemSize)
.aspectRatio(1f)
.scale(
animateFloatAsState(
targetValue = if (isSelected) 0.7f else 1f,
animationSpec = tween(400)
).value
)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) 45f else 0f,
animationSpec = tween(400)
).value
)
.container(
shape = shape,
color = color,
resultPadding = 0.dp
)
.transparencyChecker()
.background(color, shape)
.hapticsClickable {
onValueChange(color.copy(if (allowAlpha) color.alpha else 1f))
customColor = null
},
contentAlignment = Alignment.Center
) {
AnimatedVisibility(isSelected) {
Icon(
imageVector = Icons.Rounded.DoneAll,
contentDescription = null,
tint = color.inverse(
fraction = {
if (it) 0.8f
else 0.5f
},
darkMode = color.luminance() < 0.3f
),
modifier = Modifier
.size(24.dp)
.rotate(
animateFloatAsState(
targetValue = if (isSelected) -45f else 0f,
animationSpec = tween(400)
).value
)
)
}
}
}
}
}

View File

@ -129,7 +129,6 @@ fun DrawLineStyleSelector(
selectedIndex = values.indexOfFirst {
value::class.isInstance(it)
},
buttonIcon = {},
itemContent = {
Icon(
imageVector = values[it].getIcon(),

View File

@ -136,7 +136,6 @@ fun DrawModeSelector(
selectedIndex = values.indexOfFirst {
value::class.isInstance(it)
},
buttonIcon = {},
itemContent = {
Icon(
imageVector = values[it].getIcon(),

View File

@ -99,7 +99,6 @@ fun DrawPathModeSelector(
}
}
}.value,
buttonIcon = {},
activeButtonColor = MaterialTheme.colorScheme.surfaceContainerHighest,
itemContent = {
Icon(

View File

@ -74,7 +74,6 @@ fun DefaultDrawPathModeSettingItem(
itemCount = 17,
title = {},
selectedIndex = settingsState.defaultDrawPathMode,
buttonIcon = {},
activeButtonColor = MaterialTheme.colorScheme.surfaceContainerHighest,
inactiveButtonColor = MaterialTheme.colorScheme.surfaceContainer,
itemContent = {