Simplify search / profile UX.

This commit is contained in:
Thomas Andres Gomez 2022-10-18 15:46:07 +02:00
parent c864c4b8e4
commit af89e153ef
6 changed files with 97 additions and 123 deletions

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
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.material.Card import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -28,9 +29,10 @@ fun SearchScaffold(
Layout( Layout(
modifier = modifier, modifier = modifier,
content = { content = {
// TODO : remember transition.
val transition = updateTransition( val transition = updateTransition(
label = "Collapse transition", label = "Collapse transition",
targetState = state.isCollapsed() targetState = state.isCollapsed
) )
val horizontal by transition.animateDp(label = "horizontal") { val horizontal by transition.animateDp(label = "horizontal") {
when (it) { when (it) {
@ -52,7 +54,8 @@ fun SearchScaffold(
Column { Column {
topBar() topBar()
AnimatedVisibility( AnimatedVisibility(
visible = state.isCollapsed().not(), modifier = Modifier.fillMaxWidth(),
visible = state.isExpended,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
@ -87,51 +90,34 @@ fun SearchScaffold(
@Composable @Composable
fun rememberSearchScaffoldState( fun rememberSearchScaffoldState(
expended: Boolean = false, expended: Boolean = false,
content: SearchScaffoldState.ContentState = SearchScaffoldState.ContentState.INITIAL,
): SearchScaffoldState { ): SearchScaffoldState {
return rememberSaveable(saver = SearchScaffoldState.Saver) { return rememberSaveable(saver = SearchScaffoldState.Saver) {
SearchScaffoldState( SearchScaffoldState(expended = expended)
expended = expended,
state = content,
)
} }
} }
@Stable @Stable
class SearchScaffoldState( class SearchScaffoldState(
expended: Boolean, expended: Boolean,
state: ContentState = ContentState.INITIAL,
) { ) {
var isExpended: Boolean by mutableStateOf(expended) var isExpended: Boolean by mutableStateOf(expended)
private set private set
var content: ContentState by mutableStateOf(state) val isCollapsed: Boolean
private set get() = isExpended.not()
fun isCollapsed(): Boolean = isExpended.not() fun expand() {
fun expand(state: ContentState) {
content = state
isExpended = true isExpended = true
} }
fun collapse() { fun collapse() {
isExpended = false isExpended = false
content = ContentState.INITIAL
} }
companion object { companion object {
val Saver: Saver<SearchScaffoldState, Pair<Boolean, Int>> = Saver( val Saver: Saver<SearchScaffoldState, Boolean> = Saver(
save = { it.isExpended to it.content.ordinal }, save = { it.isExpended },
restore = { SearchScaffoldState(it.first, ContentState.values()[it.second]) }, restore = { SearchScaffoldState(it) },
) )
} }
@Stable
@Immutable
enum class ContentState {
INITIAL,
SEARCH,
PROFILE,
}
} }

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController
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.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
@ -25,7 +26,6 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.scaffold.* import com.pixelized.biblib.ui.scaffold.*
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState.ContentState
import com.pixelized.biblib.ui.screen.home.common.connectivity.ConnectivityHeader import com.pixelized.biblib.ui.screen.home.common.connectivity.ConnectivityHeader
import com.pixelized.biblib.ui.screen.home.common.connectivity.ConnectivityViewModel import com.pixelized.biblib.ui.screen.home.common.connectivity.ConnectivityViewModel
import com.pixelized.biblib.ui.screen.home.page.Page import com.pixelized.biblib.ui.screen.home.page.Page
@ -40,14 +40,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn( @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class)
ExperimentalComposeUiApi::class,
ExperimentalAnimationApi::class,
ExperimentalMaterialApi::class
)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
accountViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
connectivityViewModel: ConnectivityViewModel = hiltViewModel(), connectivityViewModel: ConnectivityViewModel = hiltViewModel(),
keyboard: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current, keyboard: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
searchScaffoldState: SearchScaffoldState = rememberSearchScaffoldState(), searchScaffoldState: SearchScaffoldState = rememberSearchScaffoldState(),
@ -65,70 +61,33 @@ fun HomeScreen(
val viewModel = LocalBookSearchViewModel.current val viewModel = LocalBookSearchViewModel.current
Search( Search(
state = searchScaffoldState, state = searchScaffoldState,
avatar = accountViewModel.avatar,
focusRequester = focusRequester, focusRequester = focusRequester,
avatar = homeViewModel.avatar,
searchValue = viewModel.search ?: "",
onSearchValueChange = {
viewModel.filterSearch(criteria = it)
},
onCloseTap = { onCloseTap = {
focusManager.clearFocus(force = true) focusManager.clearFocus(force = true)
keyboard?.hide() keyboard?.hide()
searchScaffoldState.collapse() searchScaffoldState.collapse()
}, },
searchValue = viewModel.search ?: "", onAvatarTap = {
onSearchValueChange = { homeViewModel.showProfileDialog()
viewModel.filterSearch(criteria = it)
}, },
onSearchTap = { onSearchTap = {
if (searchScaffoldState.content != ContentState.SEARCH || searchScaffoldState.isCollapsed()) { if (searchScaffoldState.isCollapsed) {
searchScaffoldState.expand(ContentState.SEARCH) searchScaffoldState.expand()
scope.launch { scope.launch {
delay(100) // let the animation play before requesting the focus delay(100) // let the animation play before requesting the focus
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} else {
focusManager.clearFocus(force = true)
keyboard?.hide()
searchScaffoldState.collapse()
}
},
onAvatarTap = {
if (searchScaffoldState.content != ContentState.PROFILE || searchScaffoldState.isCollapsed()) {
focusManager.clearFocus(force = true)
keyboard?.hide()
searchScaffoldState.expand(ContentState.PROFILE)
} else {
searchScaffoldState.collapse()
} }
} }
) )
}, },
search = { search = {
Box(modifier = Modifier.fillMaxSize()) { SearchPage()
AnimatedContent(
targetState = searchScaffoldState.content,
transitionSpec = {
when {
targetState == ContentState.INITIAL -> {
EnterTransition.None with fadeOut()
}
targetState.ordinal < initialState.ordinal -> {
val enter = slideInHorizontally { -it } + fadeIn()
val exit = slideOutHorizontally { +it } + fadeOut()
enter with exit
}
else -> {
val enter = slideInHorizontally { +it } + fadeIn()
val exit = slideOutHorizontally { -it } + fadeOut()
enter with exit
}
}
}
) {
when (it) {
ContentState.SEARCH -> SearchPage()
ContentState.PROFILE -> ProfilePage()
else -> Unit
}
}
}
}, },
content = { content = {
HomeScreenContent( HomeScreenContent(
@ -155,6 +114,21 @@ fun HomeScreen(
} }
} }
} }
ProfileHandler(viewModel = homeViewModel)
}
@Composable
fun ProfileHandler(
viewModel: HomeViewModel
) {
if (viewModel.shouldDisplayProfileDialog) {
Dialog(
onDismissRequest = { viewModel.dismissProfileDialog() }
) {
ProfilePage()
}
}
} }
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)

View file

@ -2,9 +2,14 @@ package com.pixelized.biblib.ui.screen.home
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -12,4 +17,15 @@ class HomeViewModel @Inject constructor(
account: IGoogleSingInRepository, account: IGoogleSingInRepository,
) : ViewModel() { ) : ViewModel() {
val avatar by mutableStateOf(account.account?.photoUrl?.toString()) val avatar by mutableStateOf(account.account?.photoUrl?.toString())
var shouldDisplayProfileDialog by mutableStateOf(false)
private set
fun showProfileDialog() {
shouldDisplayProfileDialog = true
}
fun dismissProfileDialog() {
shouldDisplayProfileDialog = false
}
} }

View file

@ -29,21 +29,25 @@ fun ProfilePage(
val context = LocalContext.current val context = LocalContext.current
val navigation = LocalScreenNavHostController.current val navigation = LocalScreenNavHostController.current
when (val user = viewModel.user) { Card {
is StateUio.Progress -> Unit when (val user = viewModel.user) {
is StateUio.Success -> ProfileScreenContent( is StateUio.Progress -> Unit
modifier = Modifier.padding(MaterialTheme.bibLib.dimen.dp16), is StateUio.Success -> ProfileScreenContent(
user = user.value, modifier = Modifier
onEditClick = { .fillMaxWidth()
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE)) .padding(MaterialTheme.bibLib.dimen.dp16),
context.startActivity(intent) user = user.value,
}, onEditClick = {
onLogoutClick = { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE))
viewModel.logout() context.startActivity(intent)
navigation.navigateToAuthentication() },
} onLogoutClick = {
) viewModel.logout()
is StateUio.Failure -> Unit navigation.navigateToAuthentication()
}
)
is StateUio.Failure -> Unit
}
} }
} }
@ -54,14 +58,14 @@ private fun ProfileScreenContent(
onEditClick: () -> Unit = default(), onEditClick: () -> Unit = default(),
onLogoutClick: () -> Unit = default(), onLogoutClick: () -> Unit = default(),
) { ) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier) {
Text( Text(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = "Welcome" text = "Welcome"
) )
Text( Text(
style = MaterialTheme.typography.h4, style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
text = user.username, text = user.username,
) )
@ -94,7 +98,7 @@ private fun ProfileScreenContent(
) )
user.amazonEmails.forEach { user.amazonEmails.forEach {
Text( Text(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = it, text = it,
) )
@ -119,7 +123,7 @@ private fun ProfileScreenContent(
Button( Button(
modifier = Modifier modifier = Modifier
.padding(top = MaterialTheme.bibLib.dimen.dp8)
.align(Alignment.End), .align(Alignment.End),
colors = ButtonDefaults.outlinedButtonColors(), colors = ButtonDefaults.outlinedButtonColors(),
onClick = onLogoutClick, onClick = onLogoutClick,
@ -138,7 +142,9 @@ private fun ProfileScreenContentPreview() {
BibLibTheme { BibLibTheme {
Box { Box {
ProfileScreenContent( ProfileScreenContent(
modifier = Modifier.padding(MaterialTheme.bibLib.dimen.dp16), modifier = Modifier
.fillMaxWidth()
.padding(all = MaterialTheme.bibLib.dimen.dp16),
user = UserUio( user = UserUio(
username = "DefinitelyNotARobot", username = "DefinitelyNotARobot",
firstname = "R. Daneel", firstname = "R. Daneel",

View file

@ -2,10 +2,7 @@ package com.pixelized.biblib.ui.screen.home.page.search
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -50,7 +47,7 @@ fun SearchPage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
SearchPageContent( SearchPageContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxSize(),
search = bookSearchViewModel.paging, search = bookSearchViewModel.paging,
filters = filters(bookSearchViewModel), filters = filters(bookSearchViewModel),
onFilter = { onFilter = {

View file

@ -13,7 +13,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.draw.clip
@ -25,7 +27,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState import com.pixelized.biblib.ui.scaffold.SearchScaffoldState
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState.ContentState
import com.pixelized.biblib.ui.scaffold.rememberSearchScaffoldState import com.pixelized.biblib.ui.scaffold.rememberSearchScaffoldState
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib import com.pixelized.biblib.utils.extention.bibLib
@ -46,7 +47,7 @@ fun Search(
onSearchTap: () -> Unit = default(), onSearchTap: () -> Unit = default(),
) { ) {
val horizontalPadding by animateDpAsState( val horizontalPadding by animateDpAsState(
targetValue = when (state.isCollapsed()) { targetValue = when (state.isCollapsed) {
true -> MaterialTheme.bibLib.dimen.default true -> MaterialTheme.bibLib.dimen.default
else -> MaterialTheme.bibLib.dimen.dp4 else -> MaterialTheme.bibLib.dimen.dp4
} }
@ -60,11 +61,12 @@ fun Search(
) { ) {
IconButton( IconButton(
modifier = Modifier.padding(start = horizontalPadding), modifier = Modifier.padding(start = horizontalPadding),
onClick = if (state.content != ContentState.INITIAL) onCloseTap else onSearchTap, enabled = state.isExpended,
onClick = onCloseTap,
) { ) {
Icon( Icon(
imageVector = when (state.content) { imageVector = when (state.isCollapsed) {
ContentState.INITIAL -> Icons.Default.Search true -> Icons.Default.Search
else -> Icons.Default.Close else -> Icons.Default.Close
}, },
tint = MaterialTheme.colors.onSurface, tint = MaterialTheme.colors.onSurface,
@ -79,17 +81,13 @@ fun Search(
placeholder = { placeholder = {
Text( Text(
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = if (state.content != ContentState.PROFILE) { text = stringResource(id = R.string.search_title),
stringResource(id = R.string.search_title)
} else {
stringResource(id = R.string.profile_title)
}
) )
}, },
value = if (state.content == ContentState.SEARCH) searchValue else "", value = if (state.isExpended) searchValue else "",
singleLine = true, singleLine = true,
enabled = state.content == ContentState.SEARCH, enabled = state.isExpended,
readOnly = state.content != ContentState.SEARCH, readOnly = state.isCollapsed,
colors = TextFieldDefaults.outlinedTextFieldColors( colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent, unfocusedBorderColor = Color.Transparent,
@ -141,10 +139,7 @@ private fun SearchContentEmptyDeployPreview() {
BibLibTheme { BibLibTheme {
Search( Search(
avatar = "", avatar = "",
state = rememberSearchScaffoldState( state = rememberSearchScaffoldState(expended = true),
expended = true,
content = ContentState.SEARCH,
),
) )
} }
} }