Better unit selection screens

Animated list items
Animated change between "has units" / "no units" states
Left and right unit selection screen are now completely separate composable. Easier to maintain.
This commit is contained in:
Sad Ellie 2022-08-18 21:39:14 +03:00
parent d1038eb4b2
commit 6170726749
3 changed files with 144 additions and 123 deletions

View File

@ -120,21 +120,21 @@ fun UnittoApp(
composable(LEFT_LIST_SCREEN) {
LeftSideScreen(
viewModel = secondViewModel,
currentUnit = mainViewModel.unitFrom,
navigateUp = { navController.navigateUp() },
selectAction = { mainViewModel.changeUnitFrom(it) },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
viewModel = secondViewModel
selectAction = { mainViewModel.changeUnitFrom(it) }
)
}
composable(RIGHT_LIST_SCREEN) {
RightSideScreen(
viewModel = secondViewModel,
currentUnit = mainViewModel.unitTo,
navigateUp = { navController.navigateUp() },
selectAction = { mainViewModel.changeUnitTo(it) },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
viewModel = secondViewModel,
selectAction = { mainViewModel.changeUnitTo(it) },
inputValue = mainViewModel.mainUIState.inputValue.toBigDecimal(),
unitFrom = mainViewModel.unitFrom
)

View File

@ -18,15 +18,16 @@
package com.sadellie.unitto.screens.second
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
@ -53,37 +54,28 @@ import com.sadellie.unitto.screens.second.components.UnitListItem
import java.math.BigDecimal
/**
* Basic Unit list screen for left and right sides screens.
* Left side screen. Unit to convert from.
*
* @param viewModel [SecondViewModel].
* @param currentUnit Currently selected [AbstractUnit].
* @param navigateUp Action to navigate up. Called when user click back button.
* @param navigateToSettingsActtion Action to perform when clicking open settings in placeholder.
* @param navigateToSettingsActtion Action to perform when clicking settings chip at the end.
* @param selectAction Action to perform when user clicks on [UnitListItem].
* @param viewModel [SecondViewModel].
* @param chipsRow Composable that is placed under TopAppBar. See [ChipsRow]
* @param unitsListItem Composable that holds all units. See [UnitListItem].
* @param noBrokenCurrencies When True will hide [AbstractUnit] with [AbstractUnit.isEnabled] set
* to False.
* @param title TopAppBar text.
*/
@Composable
private fun BasicUnitListScreen(
fun LeftSideScreen(
viewModel: SecondViewModel,
currentUnit: AbstractUnit,
navigateUp: () -> Unit,
navigateToSettingsActtion: () -> Unit,
selectAction: (AbstractUnit) -> Unit,
viewModel: SecondViewModel,
chipsRow: @Composable (UnitGroup?, LazyListState) -> Unit = { _, _ -> },
unitsListItem: @Composable (AbstractUnit, (AbstractUnit) -> Unit) -> Unit,
noBrokenCurrencies: Boolean,
title: String,
selectAction: (AbstractUnit) -> Unit
) {
val uiState = viewModel.uiState
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val focusManager = LocalFocusManager.current
val chipsRowLazyListState = rememberLazyListState()
val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
val focusManager = LocalFocusManager.current
val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
val chipsBackground = animateColorAsState(
if (scrollBehavior.state.overlappedFraction > 0.01f) {
elevatedColor
@ -103,51 +95,60 @@ private fun BasicUnitListScreen(
Modifier.background(chipsBackground.value)
) {
SearchBar(
title = title,
title = stringResource(R.string.units_screen_from),
value = uiState.searchQuery,
onValueChange = {
viewModel.onSearchQueryChange(it)
viewModel.loadUnitsToShow(noBrokenCurrencies)
viewModel.loadUnitsToShow(true)
},
favoritesOnly = uiState.favoritesOnly,
favoriteAction = {
viewModel.toggleFavoritesOnly()
viewModel.loadUnitsToShow(noBrokenCurrencies)
viewModel.loadUnitsToShow(true)
},
navigateUpAction = navigateUp,
focusManager = focusManager,
scrollBehavior = scrollBehavior
)
chipsRow(
viewModel.uiState.chosenUnitGroup,
chipsRowLazyListState
ChipsRow(
chosenUnitGroup = viewModel.uiState.chosenUnitGroup,
items = uiState.shownUnitGroups,
selectAction = {
viewModel.toggleSelectedChip(it)
viewModel.loadUnitsToShow(true)
},
lazyListState = chipsRowLazyListState,
navigateToSettingsActtion = navigateToSettingsActtion
)
}
}
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
if (uiState.unitsToShow.isEmpty()) {
item { SearchPlaceholder(navigateToSettingsActtion) }
return@LazyColumn
}
Crossfade(
targetState = uiState.unitsToShow.isEmpty(),
modifier = Modifier.padding(paddingValues)
) { noUnits ->
if (noUnits) {
SearchPlaceholder(navigateToSettingsActtion = navigateToSettingsActtion)
} else {
LazyColumn(Modifier.fillMaxSize()) {
uiState.unitsToShow.forEach { (unitGroup, listOfUnits) ->
item {
Header(
text = stringResource(unitGroup.res),
paddingValues = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 8.dp,
bottom = 12.dp
)
)
item(unitGroup.name) {
UnitGroupHeader(Modifier.animateItemPlacement(), unitGroup)
}
items(items = listOfUnits, key = { it.unitId }) { unit ->
unitsListItem(unit) {
items(listOfUnits, { it.unitId }) { unit ->
UnitListItem(
modifier = Modifier.animateItemPlacement(),
unit = unit,
isSelected = currentUnit == unit,
selectAction = {
selectAction(it)
viewModel.onSearchQueryChange("")
focusManager.clearFocus(true)
navigateUp()
},
favoriteAction = { viewModel.favoriteUnit(it) },
)
}
}
}
}
@ -160,7 +161,7 @@ private fun BasicUnitListScreen(
* Telling viewModel that it needs to update the list
*/
viewModel.setSelectedChip(currentUnit.group)
viewModel.loadUnitsToShow(noBrokenCurrencies)
viewModel.loadUnitsToShow(true)
val groupToSelect = uiState.shownUnitGroups.indexOf(currentUnit.group)
if (groupToSelect > -1) {
@ -169,83 +170,75 @@ private fun BasicUnitListScreen(
}
}
/**
* Left side screen. Unit to convert from.
*
* @param currentUnit Currently selected [AbstractUnit].
* @param navigateUp Action to navigate up. Called when user click back button.
* @param navigateToSettingsActtion Action to perform when clicking settings chip at the end.
* @param selectAction Action to perform when user clicks on [UnitListItem].
* @param viewModel [SecondViewModel].
*/
@Composable
fun LeftSideScreen(
currentUnit: AbstractUnit,
navigateUp: () -> Unit,
navigateToSettingsActtion: () -> Unit,
selectAction: (AbstractUnit) -> Unit,
viewModel: SecondViewModel
) = BasicUnitListScreen(
currentUnit = currentUnit,
navigateUp = navigateUp,
navigateToSettingsActtion = navigateToSettingsActtion,
selectAction = selectAction,
viewModel = viewModel,
chipsRow = { unitGroup, lazyListState ->
ChipsRow(
items = viewModel.uiState.shownUnitGroups,
chosenUnitGroup = unitGroup,
selectAction = {
viewModel.toggleSelectedChip(it)
viewModel.loadUnitsToShow(true)
},
navigateToSettingsActtion = navigateToSettingsActtion,
lazyListState = lazyListState
)
},
unitsListItem = { unit, selectUnitAction ->
UnitListItem(
unit = unit,
isSelected = currentUnit == unit,
selectAction = selectUnitAction,
favoriteAction = { viewModel.favoriteUnit(it) },
)
},
noBrokenCurrencies = true,
title = stringResource(R.string.units_screen_from)
)
/**
* Right side screen. Unit to convert to.
*
* @param viewModel [SecondViewModel].
* @param currentUnit Currently selected [AbstractUnit].
* @param navigateUp Action to navigate up. Called when user click back button.
* @param navigateToSettingsActtion Action to perform when clicking settings chip at the end.
* @param selectAction Action to perform when user clicks on [UnitListItem].
* @param viewModel [SecondViewModel].
* @param inputValue Current input value (upper text field on MainScreen)
* @param unitFrom Unit we are converting from. Need it for conversion.
*/
@Composable
fun RightSideScreen(
viewModel: SecondViewModel,
currentUnit: AbstractUnit,
navigateUp: () -> Unit,
navigateToSettingsActtion: () -> Unit,
selectAction: (AbstractUnit) -> Unit,
viewModel: SecondViewModel,
inputValue: BigDecimal,
unitFrom: AbstractUnit
) = BasicUnitListScreen(
currentUnit = currentUnit,
navigateUp = navigateUp,
navigateToSettingsActtion = navigateToSettingsActtion,
selectAction = selectAction,
viewModel = viewModel,
unitsListItem = { unit, selectUnitAction ->
) {
val uiState = viewModel.uiState
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val focusManager = LocalFocusManager.current
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
SearchBar(
title = stringResource(R.string.units_screen_from),
value = uiState.searchQuery,
onValueChange = {
viewModel.onSearchQueryChange(it)
viewModel.loadUnitsToShow(false)
},
favoritesOnly = uiState.favoritesOnly,
favoriteAction = {
viewModel.toggleFavoritesOnly()
viewModel.loadUnitsToShow(false)
},
navigateUpAction = navigateUp,
focusManager = focusManager,
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
Crossfade(
targetState = uiState.unitsToShow.isEmpty(),
modifier = Modifier.padding(paddingValues)
) { noUnits ->
if (noUnits) {
SearchPlaceholder(navigateToSettingsActtion = navigateToSettingsActtion)
} else {
LazyColumn(Modifier.fillMaxSize()) {
uiState.unitsToShow.forEach { (unitGroup, listOfUnits) ->
item(unitGroup.name) {
UnitGroupHeader(Modifier.animateItemPlacement(), unitGroup)
}
items(listOfUnits, { it.unitId }) { unit ->
UnitListItem(
modifier = Modifier.animateItemPlacement(),
unit = unit,
isSelected = currentUnit == unit,
selectAction = selectUnitAction,
selectAction = {
selectAction(it)
viewModel.onSearchQueryChange("")
focusManager.clearFocus(true)
navigateUp()
},
favoriteAction = { viewModel.favoriteUnit(it) },
convertValue = {
Formatter.format(
@ -253,7 +246,28 @@ fun RightSideScreen(
)
}
)
},
noBrokenCurrencies = false,
title = stringResource(R.string.units_screen_to)
)
}
}
}
}
}
}
// This block is called only once on initial composition
LaunchedEffect(Unit) {
/**
* Telling viewModel that it needs to update the list
*/
viewModel.setSelectedChip(currentUnit.group)
viewModel.loadUnitsToShow(false)
}
}
@Composable
private fun UnitGroupHeader(modifier: Modifier, unitGroup: UnitGroup) {
Header(
text = stringResource(unitGroup.res),
modifier = modifier,
paddingValues = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp)
)
}

View File

@ -27,6 +27,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -66,6 +67,7 @@ import com.sadellie.unitto.data.units.AbstractUnit
*/
@Composable
private fun BasicUnitListItem(
modifier: Modifier,
unit: AbstractUnit,
isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit,
@ -73,8 +75,9 @@ private fun BasicUnitListItem(
shortNameLabel: String
) {
var isFavorite: Boolean by rememberSaveable { mutableStateOf(unit.isFavorite) }
Column(
modifier = Modifier
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.surface)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
@ -145,11 +148,13 @@ private fun BasicUnitListItem(
*/
@Composable
fun UnitListItem(
modifier: Modifier,
unit: AbstractUnit,
isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit,
favoriteAction: (AbstractUnit) -> Unit,
) = BasicUnitListItem(
modifier = modifier,
unit = unit,
isSelected = isSelected,
selectAction = selectAction,
@ -168,12 +173,14 @@ fun UnitListItem(
*/
@Composable
fun UnitListItem(
modifier: Modifier,
unit: AbstractUnit,
isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit,
favoriteAction: (AbstractUnit) -> Unit,
convertValue: (AbstractUnit) -> String
) = BasicUnitListItem(
modifier = modifier,
unit = unit,
isSelected = isSelected,
selectAction = selectAction,