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) { composable(LEFT_LIST_SCREEN) {
LeftSideScreen( LeftSideScreen(
viewModel = secondViewModel,
currentUnit = mainViewModel.unitFrom, currentUnit = mainViewModel.unitFrom,
navigateUp = { navController.navigateUp() }, navigateUp = { navController.navigateUp() },
selectAction = { mainViewModel.changeUnitFrom(it) },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) }, navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
viewModel = secondViewModel selectAction = { mainViewModel.changeUnitFrom(it) }
) )
} }
composable(RIGHT_LIST_SCREEN) { composable(RIGHT_LIST_SCREEN) {
RightSideScreen( RightSideScreen(
viewModel = secondViewModel,
currentUnit = mainViewModel.unitTo, currentUnit = mainViewModel.unitTo,
navigateUp = { navController.navigateUp() }, navigateUp = { navController.navigateUp() },
selectAction = { mainViewModel.changeUnitTo(it) },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) }, navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
viewModel = secondViewModel, selectAction = { mainViewModel.changeUnitTo(it) },
inputValue = mainViewModel.mainUIState.inputValue.toBigDecimal(), inputValue = mainViewModel.mainUIState.inputValue.toBigDecimal(),
unitFrom = mainViewModel.unitFrom unitFrom = mainViewModel.unitFrom
) )

View File

@ -18,15 +18,16 @@
package com.sadellie.unitto.screens.second package com.sadellie.unitto.screens.second
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
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.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -53,37 +54,28 @@ import com.sadellie.unitto.screens.second.components.UnitListItem
import java.math.BigDecimal 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 currentUnit Currently selected [AbstractUnit].
* @param navigateUp Action to navigate up. Called when user click back button. * @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 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 @Composable
private fun BasicUnitListScreen( fun LeftSideScreen(
viewModel: SecondViewModel,
currentUnit: AbstractUnit, currentUnit: AbstractUnit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
navigateToSettingsActtion: () -> Unit, navigateToSettingsActtion: () -> Unit,
selectAction: (AbstractUnit) -> Unit, selectAction: (AbstractUnit) -> Unit
viewModel: SecondViewModel,
chipsRow: @Composable (UnitGroup?, LazyListState) -> Unit = { _, _ -> },
unitsListItem: @Composable (AbstractUnit, (AbstractUnit) -> Unit) -> Unit,
noBrokenCurrencies: Boolean,
title: String,
) { ) {
val uiState = viewModel.uiState val uiState = viewModel.uiState
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val focusManager = LocalFocusManager.current
val chipsRowLazyListState = rememberLazyListState() val chipsRowLazyListState = rememberLazyListState()
val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) val focusManager = LocalFocusManager.current
val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
val chipsBackground = animateColorAsState( val chipsBackground = animateColorAsState(
if (scrollBehavior.state.overlappedFraction > 0.01f) { if (scrollBehavior.state.overlappedFraction > 0.01f) {
elevatedColor elevatedColor
@ -103,51 +95,60 @@ private fun BasicUnitListScreen(
Modifier.background(chipsBackground.value) Modifier.background(chipsBackground.value)
) { ) {
SearchBar( SearchBar(
title = title, title = stringResource(R.string.units_screen_from),
value = uiState.searchQuery, value = uiState.searchQuery,
onValueChange = { onValueChange = {
viewModel.onSearchQueryChange(it) viewModel.onSearchQueryChange(it)
viewModel.loadUnitsToShow(noBrokenCurrencies) viewModel.loadUnitsToShow(true)
}, },
favoritesOnly = uiState.favoritesOnly, favoritesOnly = uiState.favoritesOnly,
favoriteAction = { favoriteAction = {
viewModel.toggleFavoritesOnly() viewModel.toggleFavoritesOnly()
viewModel.loadUnitsToShow(noBrokenCurrencies) viewModel.loadUnitsToShow(true)
}, },
navigateUpAction = navigateUp, navigateUpAction = navigateUp,
focusManager = focusManager, focusManager = focusManager,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
chipsRow( ChipsRow(
viewModel.uiState.chosenUnitGroup, chosenUnitGroup = viewModel.uiState.chosenUnitGroup,
chipsRowLazyListState items = uiState.shownUnitGroups,
selectAction = {
viewModel.toggleSelectedChip(it)
viewModel.loadUnitsToShow(true)
},
lazyListState = chipsRowLazyListState,
navigateToSettingsActtion = navigateToSettingsActtion
) )
} }
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) { Crossfade(
if (uiState.unitsToShow.isEmpty()) { targetState = uiState.unitsToShow.isEmpty(),
item { SearchPlaceholder(navigateToSettingsActtion) } modifier = Modifier.padding(paddingValues)
return@LazyColumn ) { noUnits ->
} if (noUnits) {
SearchPlaceholder(navigateToSettingsActtion = navigateToSettingsActtion)
} else {
LazyColumn(Modifier.fillMaxSize()) {
uiState.unitsToShow.forEach { (unitGroup, listOfUnits) -> uiState.unitsToShow.forEach { (unitGroup, listOfUnits) ->
item { item(unitGroup.name) {
Header( UnitGroupHeader(Modifier.animateItemPlacement(), unitGroup)
text = stringResource(unitGroup.res),
paddingValues = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 8.dp,
bottom = 12.dp
)
)
} }
items(items = listOfUnits, key = { it.unitId }) { unit -> items(listOfUnits, { it.unitId }) { unit ->
unitsListItem(unit) { UnitListItem(
modifier = Modifier.animateItemPlacement(),
unit = unit,
isSelected = currentUnit == unit,
selectAction = {
selectAction(it) selectAction(it)
viewModel.onSearchQueryChange("") viewModel.onSearchQueryChange("")
focusManager.clearFocus(true) focusManager.clearFocus(true)
navigateUp() navigateUp()
},
favoriteAction = { viewModel.favoriteUnit(it) },
)
}
} }
} }
} }
@ -160,7 +161,7 @@ private fun BasicUnitListScreen(
* Telling viewModel that it needs to update the list * Telling viewModel that it needs to update the list
*/ */
viewModel.setSelectedChip(currentUnit.group) viewModel.setSelectedChip(currentUnit.group)
viewModel.loadUnitsToShow(noBrokenCurrencies) viewModel.loadUnitsToShow(true)
val groupToSelect = uiState.shownUnitGroups.indexOf(currentUnit.group) val groupToSelect = uiState.shownUnitGroups.indexOf(currentUnit.group)
if (groupToSelect > -1) { 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. * Right side screen. Unit to convert to.
* *
* @param viewModel [SecondViewModel].
* @param currentUnit Currently selected [AbstractUnit]. * @param currentUnit Currently selected [AbstractUnit].
* @param navigateUp Action to navigate up. Called when user click back button. * @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 navigateToSettingsActtion Action to perform when clicking settings chip at the end.
* @param selectAction Action to perform when user clicks on [UnitListItem]. * @param selectAction Action to perform when user clicks on [UnitListItem].
* @param viewModel [SecondViewModel].
* @param inputValue Current input value (upper text field on MainScreen) * @param inputValue Current input value (upper text field on MainScreen)
* @param unitFrom Unit we are converting from. Need it for conversion. * @param unitFrom Unit we are converting from. Need it for conversion.
*/ */
@Composable @Composable
fun RightSideScreen( fun RightSideScreen(
viewModel: SecondViewModel,
currentUnit: AbstractUnit, currentUnit: AbstractUnit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
navigateToSettingsActtion: () -> Unit, navigateToSettingsActtion: () -> Unit,
selectAction: (AbstractUnit) -> Unit, selectAction: (AbstractUnit) -> Unit,
viewModel: SecondViewModel,
inputValue: BigDecimal, inputValue: BigDecimal,
unitFrom: AbstractUnit unitFrom: AbstractUnit
) = BasicUnitListScreen( ) {
currentUnit = currentUnit, val uiState = viewModel.uiState
navigateUp = navigateUp, val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
navigateToSettingsActtion = navigateToSettingsActtion, val focusManager = LocalFocusManager.current
selectAction = selectAction,
viewModel = viewModel, Scaffold(
unitsListItem = { unit, selectUnitAction -> 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( UnitListItem(
modifier = Modifier.animateItemPlacement(),
unit = unit, unit = unit,
isSelected = currentUnit == unit, isSelected = currentUnit == unit,
selectAction = selectUnitAction, selectAction = {
selectAction(it)
viewModel.onSearchQueryChange("")
focusManager.clearFocus(true)
navigateUp()
},
favoriteAction = { viewModel.favoriteUnit(it) }, favoriteAction = { viewModel.favoriteUnit(it) },
convertValue = { convertValue = {
Formatter.format( 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.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -66,6 +67,7 @@ import com.sadellie.unitto.data.units.AbstractUnit
*/ */
@Composable @Composable
private fun BasicUnitListItem( private fun BasicUnitListItem(
modifier: Modifier,
unit: AbstractUnit, unit: AbstractUnit,
isSelected: Boolean, isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit, selectAction: (AbstractUnit) -> Unit,
@ -73,8 +75,9 @@ private fun BasicUnitListItem(
shortNameLabel: String shortNameLabel: String
) { ) {
var isFavorite: Boolean by rememberSaveable { mutableStateOf(unit.isFavorite) } var isFavorite: Boolean by rememberSaveable { mutableStateOf(unit.isFavorite) }
Column( Box(
modifier = Modifier modifier = modifier
.background(MaterialTheme.colorScheme.surface)
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(), indication = rememberRipple(),
@ -145,11 +148,13 @@ private fun BasicUnitListItem(
*/ */
@Composable @Composable
fun UnitListItem( fun UnitListItem(
modifier: Modifier,
unit: AbstractUnit, unit: AbstractUnit,
isSelected: Boolean, isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit, selectAction: (AbstractUnit) -> Unit,
favoriteAction: (AbstractUnit) -> Unit, favoriteAction: (AbstractUnit) -> Unit,
) = BasicUnitListItem( ) = BasicUnitListItem(
modifier = modifier,
unit = unit, unit = unit,
isSelected = isSelected, isSelected = isSelected,
selectAction = selectAction, selectAction = selectAction,
@ -168,12 +173,14 @@ fun UnitListItem(
*/ */
@Composable @Composable
fun UnitListItem( fun UnitListItem(
modifier: Modifier,
unit: AbstractUnit, unit: AbstractUnit,
isSelected: Boolean, isSelected: Boolean,
selectAction: (AbstractUnit) -> Unit, selectAction: (AbstractUnit) -> Unit,
favoriteAction: (AbstractUnit) -> Unit, favoriteAction: (AbstractUnit) -> Unit,
convertValue: (AbstractUnit) -> String convertValue: (AbstractUnit) -> String
) = BasicUnitListItem( ) = BasicUnitListItem(
modifier = modifier,
unit = unit, unit = unit,
isSelected = isSelected, isSelected = isSelected,
selectAction = selectAction, selectAction = selectAction,