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.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
@ -28,9 +29,10 @@ fun SearchScaffold(
Layout(
modifier = modifier,
content = {
// TODO : remember transition.
val transition = updateTransition(
label = "Collapse transition",
targetState = state.isCollapsed()
targetState = state.isCollapsed
)
val horizontal by transition.animateDp(label = "horizontal") {
when (it) {
@ -52,7 +54,8 @@ fun SearchScaffold(
Column {
topBar()
AnimatedVisibility(
visible = state.isCollapsed().not(),
modifier = Modifier.fillMaxWidth(),
visible = state.isExpended,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
@ -87,51 +90,34 @@ fun SearchScaffold(
@Composable
fun rememberSearchScaffoldState(
expended: Boolean = false,
content: SearchScaffoldState.ContentState = SearchScaffoldState.ContentState.INITIAL,
): SearchScaffoldState {
return rememberSaveable(saver = SearchScaffoldState.Saver) {
SearchScaffoldState(
expended = expended,
state = content,
)
SearchScaffoldState(expended = expended)
}
}
@Stable
class SearchScaffoldState(
expended: Boolean,
state: ContentState = ContentState.INITIAL,
) {
var isExpended: Boolean by mutableStateOf(expended)
private set
var content: ContentState by mutableStateOf(state)
private set
val isCollapsed: Boolean
get() = isExpended.not()
fun isCollapsed(): Boolean = isExpended.not()
fun expand(state: ContentState) {
content = state
fun expand() {
isExpended = true
}
fun collapse() {
isExpended = false
content = ContentState.INITIAL
}
companion object {
val Saver: Saver<SearchScaffoldState, Pair<Boolean, Int>> = Saver(
save = { it.isExpended to it.content.ordinal },
restore = { SearchScaffoldState(it.first, ContentState.values()[it.second]) },
val Saver: Saver<SearchScaffoldState, Boolean> = Saver(
save = { it.isExpended },
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
@ -25,7 +26,6 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
import com.pixelized.biblib.R
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.ConnectivityViewModel
import com.pixelized.biblib.ui.screen.home.page.Page
@ -40,14 +40,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(
ExperimentalComposeUiApi::class,
ExperimentalAnimationApi::class,
ExperimentalMaterialApi::class
)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class)
@Composable
fun HomeScreen(
accountViewModel: HomeViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
connectivityViewModel: ConnectivityViewModel = hiltViewModel(),
keyboard: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
searchScaffoldState: SearchScaffoldState = rememberSearchScaffoldState(),
@ -65,70 +61,33 @@ fun HomeScreen(
val viewModel = LocalBookSearchViewModel.current
Search(
state = searchScaffoldState,
avatar = accountViewModel.avatar,
focusRequester = focusRequester,
avatar = homeViewModel.avatar,
searchValue = viewModel.search ?: "",
onSearchValueChange = {
viewModel.filterSearch(criteria = it)
},
onCloseTap = {
focusManager.clearFocus(force = true)
keyboard?.hide()
searchScaffoldState.collapse()
},
searchValue = viewModel.search ?: "",
onSearchValueChange = {
viewModel.filterSearch(criteria = it)
onAvatarTap = {
homeViewModel.showProfileDialog()
},
onSearchTap = {
if (searchScaffoldState.content != ContentState.SEARCH || searchScaffoldState.isCollapsed()) {
searchScaffoldState.expand(ContentState.SEARCH)
if (searchScaffoldState.isCollapsed) {
searchScaffoldState.expand()
scope.launch {
delay(100) // let the animation play before requesting the focus
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 = {
Box(modifier = Modifier.fillMaxSize()) {
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
}
}
}
SearchPage()
},
content = {
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)

View file

@ -2,9 +2,14 @@ package com.pixelized.biblib.ui.screen.home
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@ -12,4 +17,15 @@ class HomeViewModel @Inject constructor(
account: IGoogleSingInRepository,
) : ViewModel() {
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 navigation = LocalScreenNavHostController.current
when (val user = viewModel.user) {
is StateUio.Progress -> Unit
is StateUio.Success -> ProfileScreenContent(
modifier = Modifier.padding(MaterialTheme.bibLib.dimen.dp16),
user = user.value,
onEditClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE))
context.startActivity(intent)
},
onLogoutClick = {
viewModel.logout()
navigation.navigateToAuthentication()
}
)
is StateUio.Failure -> Unit
Card {
when (val user = viewModel.user) {
is StateUio.Progress -> Unit
is StateUio.Success -> ProfileScreenContent(
modifier = Modifier
.fillMaxWidth()
.padding(MaterialTheme.bibLib.dimen.dp16),
user = user.value,
onEditClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE))
context.startActivity(intent)
},
onLogoutClick = {
viewModel.logout()
navigation.navigateToAuthentication()
}
)
is StateUio.Failure -> Unit
}
}
}
@ -54,14 +58,14 @@ private fun ProfileScreenContent(
onEditClick: () -> Unit = default(),
onLogoutClick: () -> Unit = default(),
) {
Column(modifier = modifier.fillMaxWidth()) {
Column(modifier = modifier) {
Text(
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = "Welcome"
)
Text(
style = MaterialTheme.typography.h4,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.primary,
text = user.username,
)
@ -94,7 +98,7 @@ private fun ProfileScreenContent(
)
user.amazonEmails.forEach {
Text(
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface,
text = it,
)
@ -119,7 +123,7 @@ private fun ProfileScreenContent(
Button(
modifier = Modifier
.padding(top = MaterialTheme.bibLib.dimen.dp8)
.align(Alignment.End),
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onLogoutClick,
@ -138,7 +142,9 @@ private fun ProfileScreenContentPreview() {
BibLibTheme {
Box {
ProfileScreenContent(
modifier = Modifier.padding(MaterialTheme.bibLib.dimen.dp16),
modifier = Modifier
.fillMaxWidth()
.padding(all = MaterialTheme.bibLib.dimen.dp16),
user = UserUio(
username = "DefinitelyNotARobot",
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_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
@ -50,7 +47,7 @@ fun SearchPage(
val scope = rememberCoroutineScope()
SearchPageContent(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxSize(),
search = bookSearchViewModel.paging,
filters = filters(bookSearchViewModel),
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.Person
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.Modifier
import androidx.compose.ui.draw.clip
@ -25,7 +27,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.pixelized.biblib.R
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.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
@ -46,7 +47,7 @@ fun Search(
onSearchTap: () -> Unit = default(),
) {
val horizontalPadding by animateDpAsState(
targetValue = when (state.isCollapsed()) {
targetValue = when (state.isCollapsed) {
true -> MaterialTheme.bibLib.dimen.default
else -> MaterialTheme.bibLib.dimen.dp4
}
@ -60,11 +61,12 @@ fun Search(
) {
IconButton(
modifier = Modifier.padding(start = horizontalPadding),
onClick = if (state.content != ContentState.INITIAL) onCloseTap else onSearchTap,
enabled = state.isExpended,
onClick = onCloseTap,
) {
Icon(
imageVector = when (state.content) {
ContentState.INITIAL -> Icons.Default.Search
imageVector = when (state.isCollapsed) {
true -> Icons.Default.Search
else -> Icons.Default.Close
},
tint = MaterialTheme.colors.onSurface,
@ -79,17 +81,13 @@ fun Search(
placeholder = {
Text(
color = MaterialTheme.colors.onSurface,
text = if (state.content != ContentState.PROFILE) {
stringResource(id = R.string.search_title)
} else {
stringResource(id = R.string.profile_title)
}
text = stringResource(id = R.string.search_title),
)
},
value = if (state.content == ContentState.SEARCH) searchValue else "",
value = if (state.isExpended) searchValue else "",
singleLine = true,
enabled = state.content == ContentState.SEARCH,
readOnly = state.content != ContentState.SEARCH,
enabled = state.isExpended,
readOnly = state.isCollapsed,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
@ -141,10 +139,7 @@ private fun SearchContentEmptyDeployPreview() {
BibLibTheme {
Search(
avatar = "",
state = rememberSearchScaffoldState(
expended = true,
content = ContentState.SEARCH,
),
state = rememberSearchScaffoldState(expended = true),
)
}
}