Flex chips

This commit is contained in:
Sad Ellie 2024-02-08 23:12:19 +03:00
parent 3d2a63fe95
commit 9d76c168ec
3 changed files with 202 additions and 78 deletions

View File

@ -18,7 +18,8 @@
package com.sadellie.unitto.core.ui.common
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.FilterChipDefaults
@ -40,6 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
@ -48,41 +49,37 @@ import androidx.compose.ui.unit.dp
@Composable
fun FilterChip(
modifier: Modifier = Modifier,
selected: Boolean,
isSelected: Boolean,
onClick: () -> Unit,
label: String,
imageVector: ImageVector,
contentDescription: String,
) {
val transition = updateTransition(targetState = isSelected, label = "Selected transition")
val backgroundColor = transition.animateColor(label = "Background color") {
if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface
}
val borderColor = transition.animateColor(label = "Border color") {
if (it) Color.Transparent else MaterialTheme.colorScheme.outline
}
Row(
modifier = modifier
.background(
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
shape = FilterChipDefaults.shape
)
.padding(vertical = 8.dp)
.clip(FilterChipDefaults.shape)
.clickable { onClick() }
.background(backgroundColor.value)
.border(
width = 1.dp,
color = if (selected) Color.Transparent else MaterialTheme.colorScheme.outline,
color = borderColor.value,
shape = FilterChipDefaults.shape
)
.height(FilterChipDefaults.Height)
.clickable { onClick() }
.padding(start = 8.dp, end = 16.dp),
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = selected) {
Icon(
modifier = Modifier.height(FilterChipDefaults.IconSize),
imageVector = imageVector,
contentDescription = contentDescription,
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(start = 8.dp),
text = label,
style = MaterialTheme.typography.labelLarge,
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@ -96,17 +93,16 @@ fun AssistChip(
) {
Row(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = AssistChipDefaults.shape
)
.padding(vertical = 8.dp)
.clip(FilterChipDefaults.shape)
.clickable { onClick() }
.background(MaterialTheme.colorScheme.surface)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = AssistChipDefaults.shape
)
.height(32.dp)
.clickable { onClick() }
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
@ -135,10 +131,8 @@ fun PreviewFilterChip() {
var isSelected by remember { mutableStateOf(true) }
FilterChip(
selected = isSelected,
isSelected = isSelected,
onClick = { isSelected = !isSelected },
label = "Label",
imageVector = Icons.Default.Check,
contentDescription = ""
)
}

View File

@ -20,6 +20,7 @@ package com.sadellie.unitto.feature.converter
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
@ -32,6 +33,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.EmptyScreen
@ -114,10 +116,12 @@ private fun LeftSideScreen(
)
ChipsRow(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp)
.fillMaxWidth(),
chosenUnitGroup = uiState.unitGroup,
items = uiState.shownUnitGroups,
selectAction = updateUnitGroup,
lazyListState = chipsRowLazyListState,
navigateToSettingsAction = navigateToUnitGroups
)
}
@ -161,7 +165,7 @@ private fun LeftSideScreenPreview() {
units = units,
query = TextFieldValue("test"),
favorites = false,
shownUnitGroups = listOf(UnitGroup.LENGTH, UnitGroup.TEMPERATURE, UnitGroup.CURRENCY),
shownUnitGroups = UnitGroup.entries,
unitGroup = units.keys.toList().first(),
sorting = UnitsListSorting.USAGE,
),

View File

@ -18,23 +18,30 @@
package com.sadellie.unitto.feature.converter.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -50,51 +57,156 @@ import com.sadellie.unitto.data.model.UnitGroup
* @param chosenUnitGroup Currently selected [UnitGroup]
* @param selectAction Action to perform when a chip is clicked
* @param navigateToSettingsAction Action to perform when clicking settings chip at the end
* @param lazyListState Used for animated scroll when entering unit selection screen
*/
@Composable
internal fun ChipsRow(
items: List<UnitGroup> = UnitGroup.entries,
modifier: Modifier,
items: List<UnitGroup>,
chosenUnitGroup: UnitGroup?,
selectAction: (UnitGroup?) -> Unit,
navigateToSettingsAction: () -> Unit,
lazyListState: LazyListState
) {
LazyRow(
modifier = Modifier
.padding(bottom = 4.dp)
.fillMaxWidth(),
state = lazyListState,
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items, key = { it.name }) { item ->
val selected: Boolean = item == chosenUnitGroup
FilterChip(
selected = selected,
onClick = { selectAction(if (selected) null else item) },
label = stringResource(item.res),
imageVector = Icons.Default.Check,
contentDescription = stringResource(R.string.checked_filter_description)
)
var expanded by remember { mutableStateOf(false) }
val chipModifier = Modifier.padding(horizontal = 4.dp)
AnimatedContent(
targetState = expanded,
transitionSpec = {
expandVertically(expandFrom = Alignment.Top) { it } + fadeIn() togetherWith
shrinkVertically(shrinkTowards = Alignment.Top) { it } + fadeOut()
}
) { isExpanded ->
FlexRow(
modifier = modifier,
maxRows = if (isExpanded) Int.MAX_VALUE else 2,
mainContent = {
items.forEach { item ->
val selected: Boolean = item == chosenUnitGroup
FilterChip(
modifier = chipModifier,
isSelected = selected,
onClick = {
selectAction(if (selected) null else item)
expanded = false
},
label = stringResource(item.res),
)
}
AssistChip(
modifier = chipModifier,
onClick = navigateToSettingsAction,
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.open_settings_label)
)
if (expanded) {
AssistChip(
modifier = chipModifier,
onClick = { expanded = false },
imageVector = Icons.Default.ExpandLess,
contentDescription = "" // TODO
)
}
},
expandContent = {
AssistChip(
modifier = chipModifier,
onClick = { expanded = true },
imageVector = Icons.Default.ExpandMore,
contentDescription = "" // TODO
)
},
)
}
}
/**
* Foldable row that places a specified element if overflown.
*
* @param modifier [Modifier] to be applied to this layout.
* @param maxRows Max amount of rows including with [expandContent].
* @param mainContent Main content (list of items) that will be folded.
* @param expandContent Item that will be placed at the end if given [maxRows] wasn't high enough to
* place all [mainContent] items.
*/
@Composable
private fun FlexRow(
modifier: Modifier = Modifier,
maxRows: Int = Int.MAX_VALUE,
mainContent: @Composable () -> Unit,
expandContent: @Composable () -> Unit,
) {
SubcomposeLayout(
modifier = modifier
) { constraints ->
val localConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val layoutWidth = localConstraints.maxWidth
val mainMeasurables = subcompose(FlexRowSlots.Main, mainContent)
val expandMeasurables = subcompose(
slotId = FlexRowSlots.Expand,
content = { expandContent() }
).map {
it.measure(localConstraints)
}
val expandContentWidth = expandMeasurables.sumOf { it.measuredWidth }
val placeables = mutableListOf<MutableList<Placeable>>(mutableListOf())
var widthLeft = layoutWidth
var index = 0
for (measurable in mainMeasurables) {
val mainPlaceable = measurable.measure(localConstraints)
val lastAvailableRow = placeables.size >= maxRows
val notLastItem = index < mainMeasurables.lastIndex
// count expandContent width only for last row and not last main placeable
val measuredWidth = if (lastAvailableRow and notLastItem) {
mainPlaceable.measuredWidth + expandContentWidth
} else {
mainPlaceable.measuredWidth
}
// need new row
if (widthLeft <= measuredWidth) {
// Can't add more rows, add expandContent
if (lastAvailableRow) {
expandMeasurables.forEach {
placeables.last().add(it)
}
break
}
placeables.add(mutableListOf())
widthLeft = layoutWidth
}
placeables.last().add(mainPlaceable)
index++
widthLeft -= mainPlaceable.measuredWidth
}
/**
* Usually this chip is placed at the start, but since we scroll to currently selected
* chip, user may never find it. There is higher chance that user will notice this chip when
* scrolling right (to the last one).
*/
item("settings") {
AssistChip(
onClick = navigateToSettingsAction,
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.open_settings_label)
)
val flattenPlaceables = placeables.flatten()
val layoutHeight = placeables.size * (flattenPlaceables.maxByOrNull { it.height }?.height ?: 0)
layout(layoutWidth, layoutHeight) {
var yPos = 0
placeables.forEach { row ->
var xPos = 0
row.forEach { placeable ->
placeable.place(x = xPos, y = yPos)
xPos += placeable.width
}
yPos += row.maxByOrNull { it.height }?.height ?: 0
}
}
}
}
@Preview
private enum class FlexRowSlots { Main, Expand }
@Preview(device = "spec:width=380dp,height=850.9dp,dpi=440")
@Composable
fun PreviewUnittoChips() {
var selected by remember { mutableStateOf<UnitGroup?>(UnitGroup.LENGTH) }
@ -103,11 +215,25 @@ fun PreviewUnittoChips() {
selected = unitGroup
}
ChipsRow(
items = UnitGroup.entries,
chosenUnitGroup = selected,
selectAction = { selectAction(it) },
navigateToSettingsAction = {},
lazyListState = rememberLazyListState()
)
Column {
ChipsRow(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
items = UnitGroup.entries.take(7),
chosenUnitGroup = selected,
selectAction = { selectAction(it) },
navigateToSettingsAction = {},
)
ChipsRow(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
items = UnitGroup.entries.take(10),
chosenUnitGroup = selected,
selectAction = { selectAction(it) },
navigateToSettingsAction = {},
)
}
}