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 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
@ -40,6 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -48,41 +49,37 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun FilterChip( fun FilterChip(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selected: Boolean, isSelected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
label: String, 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( Row(
modifier = modifier modifier = modifier
.background( .padding(vertical = 8.dp)
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, .clip(FilterChipDefaults.shape)
shape = FilterChipDefaults.shape .clickable { onClick() }
) .background(backgroundColor.value)
.border( .border(
width = 1.dp, width = 1.dp,
color = if (selected) Color.Transparent else MaterialTheme.colorScheme.outline, color = borderColor.value,
shape = FilterChipDefaults.shape shape = FilterChipDefaults.shape
) )
.height(FilterChipDefaults.Height) .height(FilterChipDefaults.Height)
.clickable { onClick() } .padding(horizontal = 16.dp),
.padding(start = 8.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AnimatedVisibility(visible = selected) {
Icon(
modifier = Modifier.height(FilterChipDefaults.IconSize),
imageVector = imageVector,
contentDescription = contentDescription,
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text( Text(
modifier = Modifier.padding(start = 8.dp),
text = label, text = label,
style = MaterialTheme.typography.labelLarge, 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( Row(
modifier = modifier modifier = modifier
.background( .padding(vertical = 8.dp)
color = MaterialTheme.colorScheme.surface, .clip(FilterChipDefaults.shape)
shape = AssistChipDefaults.shape .clickable { onClick() }
) .background(MaterialTheme.colorScheme.surface)
.border( .border(
width = 1.dp, width = 1.dp,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
shape = AssistChipDefaults.shape shape = AssistChipDefaults.shape
) )
.height(32.dp) .height(32.dp)
.clickable { onClick() }
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -135,10 +131,8 @@ fun PreviewFilterChip() {
var isSelected by remember { mutableStateOf(true) } var isSelected by remember { mutableStateOf(true) }
FilterChip( FilterChip(
selected = isSelected, isSelected = isSelected,
onClick = { isSelected = !isSelected }, onClick = { isSelected = !isSelected },
label = "Label", 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.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme 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.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.EmptyScreen import com.sadellie.unitto.core.ui.common.EmptyScreen
@ -114,10 +116,12 @@ private fun LeftSideScreen(
) )
ChipsRow( ChipsRow(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp)
.fillMaxWidth(),
chosenUnitGroup = uiState.unitGroup, chosenUnitGroup = uiState.unitGroup,
items = uiState.shownUnitGroups, items = uiState.shownUnitGroups,
selectAction = updateUnitGroup, selectAction = updateUnitGroup,
lazyListState = chipsRowLazyListState,
navigateToSettingsAction = navigateToUnitGroups navigateToSettingsAction = navigateToUnitGroups
) )
} }
@ -161,7 +165,7 @@ private fun LeftSideScreenPreview() {
units = units, units = units,
query = TextFieldValue("test"), query = TextFieldValue("test"),
favorites = false, favorites = false,
shownUnitGroups = listOf(UnitGroup.LENGTH, UnitGroup.TEMPERATURE, UnitGroup.CURRENCY), shownUnitGroups = UnitGroup.entries,
unitGroup = units.keys.toList().first(), unitGroup = units.keys.toList().first(),
sorting = UnitsListSorting.USAGE, sorting = UnitsListSorting.USAGE,
), ),

View File

@ -18,23 +18,30 @@
package com.sadellie.unitto.feature.converter.components package com.sadellie.unitto.feature.converter.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.PaddingValues 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.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.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.material.icons.filled.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -50,51 +57,156 @@ import com.sadellie.unitto.data.model.UnitGroup
* @param chosenUnitGroup Currently selected [UnitGroup] * @param chosenUnitGroup Currently selected [UnitGroup]
* @param selectAction Action to perform when a chip is clicked * @param selectAction Action to perform when a chip is clicked
* @param navigateToSettingsAction Action to perform when clicking settings chip at the end * @param navigateToSettingsAction Action to perform when clicking settings chip at the end
* @param lazyListState Used for animated scroll when entering unit selection screen
*/ */
@Composable @Composable
internal fun ChipsRow( internal fun ChipsRow(
items: List<UnitGroup> = UnitGroup.entries, modifier: Modifier,
items: List<UnitGroup>,
chosenUnitGroup: UnitGroup?, chosenUnitGroup: UnitGroup?,
selectAction: (UnitGroup?) -> Unit, selectAction: (UnitGroup?) -> Unit,
navigateToSettingsAction: () -> Unit, navigateToSettingsAction: () -> Unit,
lazyListState: LazyListState
) { ) {
LazyRow( var expanded by remember { mutableStateOf(false) }
modifier = Modifier val chipModifier = Modifier.padding(horizontal = 4.dp)
.padding(bottom = 4.dp)
.fillMaxWidth(), AnimatedContent(
state = lazyListState, targetState = expanded,
contentPadding = PaddingValues(8.dp), transitionSpec = {
horizontalArrangement = Arrangement.spacedBy(8.dp) expandVertically(expandFrom = Alignment.Top) { it } + fadeIn() togetherWith
) { shrinkVertically(shrinkTowards = Alignment.Top) { it } + fadeOut()
items(items, key = { it.name }) { item -> }
val selected: Boolean = item == chosenUnitGroup ) { isExpanded ->
FilterChip( FlexRow(
selected = selected, modifier = modifier,
onClick = { selectAction(if (selected) null else item) }, maxRows = if (isExpanded) Int.MAX_VALUE else 2,
label = stringResource(item.res), mainContent = {
imageVector = Icons.Default.Check, items.forEach { item ->
contentDescription = stringResource(R.string.checked_filter_description) 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
} }
/** val flattenPlaceables = placeables.flatten()
* Usually this chip is placed at the start, but since we scroll to currently selected val layoutHeight = placeables.size * (flattenPlaceables.maxByOrNull { it.height }?.height ?: 0)
* chip, user may never find it. There is higher chance that user will notice this chip when
* scrolling right (to the last one). layout(layoutWidth, layoutHeight) {
*/ var yPos = 0
item("settings") { placeables.forEach { row ->
AssistChip( var xPos = 0
onClick = navigateToSettingsAction, row.forEach { placeable ->
imageVector = Icons.Default.Settings, placeable.place(x = xPos, y = yPos)
contentDescription = stringResource(R.string.open_settings_label) 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 @Composable
fun PreviewUnittoChips() { fun PreviewUnittoChips() {
var selected by remember { mutableStateOf<UnitGroup?>(UnitGroup.LENGTH) } var selected by remember { mutableStateOf<UnitGroup?>(UnitGroup.LENGTH) }
@ -103,11 +215,25 @@ fun PreviewUnittoChips() {
selected = unitGroup selected = unitGroup
} }
ChipsRow( Column {
items = UnitGroup.entries, ChipsRow(
chosenUnitGroup = selected, modifier = Modifier
selectAction = { selectAction(it) }, .background(MaterialTheme.colorScheme.background)
navigateToSettingsAction = {}, .fillMaxWidth(),
lazyListState = rememberLazyListState() 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 = {},
)
}
} }