Simplify search / profile UX.
This commit is contained in:
parent
c864c4b8e4
commit
af89e153ef
6 changed files with 97 additions and 123 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue