Reset row of switches in Jetpack compose - android

I have a page with a row of switches. I save the state of each row inside the composable using remember. And I update the ViewModel about the status only when the user navigates back from the page.
Now, I have a reset button at the top app bar which allows the user to reset all the user selections. Since Reset is at the upper level compose, I can't enforce the state change to the rows as the values are stored using remember. What would be the best strategy here to recompose the rows to the original state when the user clicks the reset button?
Here is the screen that I am implementing,
My Switch row compose
#Composable
fun SwitchRow(
name: String,
checked: Boolean,
onToggle: (name: String, checked: Boolean) -> Unit
) {
var checkedState by remember { mutableStateOf(checked) }
Surface(
modifier = Modifier.heightIn(min = 56.dp)
)
{
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = name, modifier = Modifier.weight(1f))
Switch(
checked = checkedState,
onCheckedChange = {
onToggle(name, it)
checkedState = it
}
)
}
}
}
A top App bar with a reset button,
#Composable
fun SwitchScreen(state: UiState,
onBackClicked: () -> Unit,
onToggleChange: (String, Boolean) -> Unit,
onReset: () -> Unit,
modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(0.dp)) {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = stringResource(R.string.settings_switch_rows))
TextButton(onClick = onReset) {
Text(stringResource(R.string.settings_reset))
}
}
},
backgroundColor = MaterialTheme.colors.background,
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.go_back)
)
}
}
)
when (state) {
is UiState.Loading -> Loading()
is State.Success -> SwitchRowList(
state = state,
onToggle = onToggleChange,
)
}
}
}

Related

State Hoisting in LazyColumn Single Selection. Jetpack Compose

I have a LazyColumn with expandable items. And when i click on the item he expands or collapses, but i need that when i click on closed element it opens and others in the list close.(the one that open). So first i moved state of item from remember variable in the item's composable function to the list of dataClass elements. And when i click on item he execute callback that change value of it' selement in the list, but it doesn't recompose.
#Preview
#Composable
fun ExpandableCardList() {
val list = remember { mutableStateListOf(PointsCard(0),PointsCard(1),PointsCard(2),PointsCard(3),PointsCard(4),PointsCard(5))}
val state = rememberLazyListState()
Scaffold { paddingValues ->
LazyColumn(
state = state,
modifier = Modifier
.fillMaxWidth(),
contentPadding = paddingValues
) {
items(list, {it.key}) { item ->
ExpandableCard(
expanded = item.state,
state = {
bool -> item.state = bool
}
)
}
}
}
}
#Composable
fun ExpandableCard(expanded: Boolean, state: (bool:Boolean) -> Unit ){
Card(
Modifier
.fillMaxWidth()
.clickable { state(!expanded) }
.background(Color(Color.BLACK))
) {
Column(Modifier.background(Color(Color.BLACK)))
{
Box(
Modifier
.fillMaxWidth()
.height(54.dp)
.background(Color(Color.BLACK))
)
AnimatedVisibility(expanded) {
Box(
Modifier
.fillMaxWidth()
.padding(start = 10.dp)
.heightIn(56.dp, 300.dp)
.background(Color(Color.BLACK), shape = RoundedCornerShape(bottomStart = 8.dp)
)
)
}
}
}
}
data class PointsCard(
val key: Int = 0,
var state: Boolean = false
)
If you want a single selection, you should probably consider hoisting your list in a class and perform all the structural changes via the list's iterator.
class CardListState {
val list = mutableStateListOf(PointsCard(0),PointsCard(1),PointsCard(2),PointsCard(3),PointsCard(4),PointsCard(5))
fun onSelected(isSelected: Boolean, item: PointsCard) {
val iterator = list.listIterator()
while (iterator.hasNext()) {
val obj = iterator.next()
if (obj.key != item.key) {
iterator.set(obj.copy(state = false))
} else {
iterator.set(obj.copy(state = isSelected))
}
}
}
}
#Composable
fun ExpandableCardList() {
val cardListState = remember { CardListState() }
val state = rememberLazyListState()
Scaffold { paddingValues ->
LazyColumn(
state = state,
modifier = Modifier
.fillMaxWidth(),
contentPadding = paddingValues
) {
items(cardListState.list, {it.key} ) { item ->
ExpandableCard(
expanded = item.state,
state = { bool ->
cardListState.onSelected(bool, item)
}
)
}
}
}
}
#Composable
fun ExpandableCard(
expanded: Boolean,
state: (bool:Boolean) -> Unit
) {
Card(
Modifier
.fillMaxWidth()
.clickable {
state(!expanded)
}
.background(Color.Black)
) {
Column(Modifier.background(Color.Black))
{
Box(
Modifier
.fillMaxWidth()
.height(54.dp)
.background(Color.Red)
)
AnimatedVisibility(expanded) {
Box(
Modifier
.fillMaxWidth()
.padding(start = 10.dp)
.heightIn(56.dp, 300.dp)
.background(Color.Green)
)
}
}
}
}

Jetpack Compose rememberSwipeableState saves old state when the list is updated

I have a list which is showing items by category. If i choose the first category and swipe the first item to the left then change a category to another one. New list is updating but the first item remains as swiped state on new list ui which should display all swipeable items as default state. How can i set default swipe state when category is changed?
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun SwipeRevealDismissItem(
context: Context,
itemDataClass: itemDataClass,
onDelete: (itemDataClass: itemDataClass) -> Unit,
isEnabled: Boolean,
defaultState: Int = SwipeState.DEFAULT.value,
onItemSelected: ((itemDataClass?) -> Unit)? = null,
content: #Composable () -> Unit
) {
val swipeState = rememberSwipeableState(
initialValue = defaultState,
)
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.swipeable(
state = swipeState,
anchors = mapOf(
0f to SwipeState.DEFAULT.value,
-Utils
.dp2px(128, context)
.toFloat() to SwipeState.SWIPED.value,
),
thresholds = { _, _ ->
FractionalThreshold(0.3f)
},
orientation = Orientation.Horizontal,
enabled = isEnabled
)
) {
Card(
modifier = Modifier
.align(Alignment.CenterEnd)
.width(96.dp)
.height(108.dp)
.padding(4.dp)
.clickable {
// onDelete(itemDataClass)
scope.launch {
swipeState.animateTo(0, tween(200, 0))
}
},
shape = RoundedCornerShape(8.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_delete_white_24dp),
modifier = Modifier
.size(16.dp)
.padding(32.dp),
tint = colorResource(id = R.color.colorRed),
contentDescription = null,
)
}
Box(modifier = Modifier
.offset {
IntOffset(swipeState.offset.value.roundToInt(), 0)
}
.clickable {
onItemSelected?.invoke(itemDataClass)
if (swipeState.currentValue == SwipeState.SWIPED.value) {
scope.launch {
swipeState.animateTo(SwipeState.DEFAULT.value)
}
}
}
.fillMaxWidth()
.wrapContentHeight()
) {
content()
}
}
}
private enum class SwipeState(val value: Int) {
DEFAULT(0),
SWIPED(1);
}

How to know when a LazyColumn (or any Composable for that matter) has been touched?

I have the following screen built in Compose -
#Composable
fun DashboardScreen(heroesViewModel: HeroesViewModel = get()) {
val searchState by heroesViewModel.searchState.collectAsState()
val uiState by heroesViewModel.uiState.collectAsState()
val focusRequester = remember { FocusRequester() }
Column(modifier = Modifier.fillMaxSize()) {
SearchBar(
searchState = searchState,
onQueryChanged = { text ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchQueryChanged(text))
},
onSearchFocusChange = { focused ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchBarFocusChanged(focused))
},
onClearQueryClicked = {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ClearQueryClicked)
},
onBack = {},
focusRequester
)
LazyColumn {
items(uiState.modelsListResponse ?: listOf()) { model ->
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
}
}
And here is my SearchBar -
#Composable
fun SearchBar(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
onBack: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val focused = searchState.focused
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
BackButton(focusManager, keyboardController, onBack)
}
SearchTextField(
searchState,
onQueryChanged,
onSearchFocusChange,
onClearQueryClicked,
focusRequester
)
}
}
#Composable
fun SearchTextField(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChanged: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focused = searchState.focused
var query = searchState.query
val searching = searchState.searching
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp, bottom = 8.dp,
start = if (focused.not()) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.isEmpty()) {
SearchHint(modifier = modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = {
query = it
onQueryChanged(it)
},
modifier = Modifier
.fillMaxSize()
.weight(1f)
.onFocusChanged { focusState ->
onSearchFocusChanged(focusState.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 16.dp)
.width(25.dp)
.size(24.dp)
)
}
query.isNotEmpty() -> {
IconButton(onClick = onClearQueryClicked) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null
)
}
}
}
}
}
}
}
}
#Composable
fun SearchHint(
modifier: Modifier = Modifier,
hint: String = "Enter a hero name"
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
text = hint,
color = Color(0xff757575)
)
}
}
class SearchState(
query: String,
focused: Boolean,
searching: Boolean,
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
}
What I want to achieve is the ability to know when the user has tapped or click anywhere outside of my SearchBar Composable. I want to send an event to the ViewModel so that he recomposes the screen, removing the keyboard and removing the cursor I have on my SearchBar (basically just resetting the focus).
I have tried using the focusRequester like I did in my SearchBar but without success as nothing happened, tried using the clickable {} block which is not what I need (I need the tap and not the click) and tried using Modifier.pointerInput with detectTapGestures and it did not work, not at the root LazyColumn and not at the ListItem level.
What I am looking for should be something really easy.
Found the answer - I ended up using isScrollInProgress variable from LazyListState class that provides a boolean indicating if the list is currently in scrolling. When the value was true I removed the focus from where I needed to remove it and it worked :)
Attached the solution -
val listState = rememberLazyListState()
...
LazyColumn(state = listState) {
items(uiState.modelsListResponse ?: listOf()) { model ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListIsScrolling(listState.isScrollInProgress))
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
Use PointerInputModifier which provides access to the underlying MotionEvents originally dispatched to Compose.
Text(modifier = Modifier
.pointerInteropFilter { motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
// When the user touches the composable
}
MotionEvent.ACTION_UP -> {
// When the user removes touch from the composable
}
}
true
},
text = "Click Me!"
)
Inside pointerInteropFilter block MotionEvent.ACTION_DOWN is triggered when user touches on the composable.

How can Navigation Drawer be opened with a drag when a list item is draggable in compose?

So I am rewriting an app's UI using Jetpack Compose. I have implemented a Navigation Drawer using the regular Scaffold function. Out of the box this provides two ways of opening the drawer: either press the navigationIcon or drag towards End of screen. The screen in question is a LazyColumn of list items.
I have at a later date implemented the SwipeToDismiss pattern on these list items. The swipe to dismiss works fine but it is no longer possible to drag anywhere to open the navigation drawer.
In the old View-based system, the navigation drawer would reserve a small width inside which you could always drag to open the drawer - regardless of child items having drag support. I am unsure how to achieve the same using Compose. It seems like it should be the job of the navigation drawer to handle this - and not a screen inside it.
The screen with navigation drawer:
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState(
rememberDrawerState(initialValue = DrawerValue.Closed)
)
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text(screenTitle) },
navigationIcon = {
IconButton(
onClick = {
coroutineScope.launch {
scaffoldState.drawerState.open()
}
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Drawer toggle button"
)
}
},
actions = {
...
}
)
},
drawerContent = {
// List of stuff
...
},
floatingActionButton = {
...
}
) { padding ->
/// Layout with a LazyColumn with elements having SwipeToDismiss
...
}
and swipe to dismiss item (displayed inside LazyColumn)
#OptIn(
ExperimentalFoundationApi::class,
ExperimentalMaterialApi::class,
ExperimentalAnimationApi::class
)
#Composable
fun SwipeableFeedItemPreview(
onSwipe: suspend () -> Unit,
onlyUnread: Boolean,
item: FeedListItem,
showThumbnail: Boolean,
imagePainter: #Composable (String) -> Unit,
onMarkAboveAsRead: () -> Unit,
onMarkBelowAsRead: () -> Unit,
onItemClick: () -> Unit
) {
val animatedVisibilityState = remember { MutableTransitionState(true) }
val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE)
// Needs to be set once layout is complete
var itemSize by remember { mutableStateOf(Size(1f, 1f)) }
val anchors = mapOf(
0f to FeedItemSwipeState.NONE,
-itemSize.width to FeedItemSwipeState.LEFT,
itemSize.width to FeedItemSwipeState.RIGHT
)
AnimatedVisibility(
visibleState = animatedVisibilityState,
enter = fadeIn(1f),
exit = shrinkVertically(Alignment.CenterVertically) + fadeOut()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { layoutCoordinates ->
itemSize = layoutCoordinates.size.toSize()
}
.swipeable(
state = swipeableState,
anchors = anchors,
orientation = Orientation.Horizontal,
thresholds = { _, _ ->
FractionalThreshold(0.25f)
}
)
) {
Box(
contentAlignment = swipeIconAlignment,
modifier = Modifier
.matchParentSize()
.background(color)
.padding(horizontal = 24.dp)
) {
AnimatedVisibility(
visible = swipeableState.targetValue != FeedItemSwipeState.NONE,
enter = fadeIn(),
exit = fadeOut()
) {
Icon(
when (item.unread) {
true -> Icons.Default.VisibilityOff
false -> Icons.Default.Visibility
},
contentDescription = stringResource(id = R.string.toggle_read_status)
)
}
}
FeedItemPreview(
item = item,
showThumbnail = showThumbnail,
imagePainter = imagePainter,
onMarkAboveAsRead = onMarkAboveAsRead,
onMarkBelowAsRead = onMarkBelowAsRead,
onItemClick = onItemClick,
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
)
}
}
}
You can easily decrease swipeable range using padding, like this:
enum class FeedItemSwipeState {
NONE, LEFT, RIGHT,
}
#Composable
fun TestView(
) {
val scaffoldState = rememberScaffoldState(
rememberDrawerState(initialValue = DrawerValue.Closed)
)
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
},
) {
val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE)
// Needs to be set once layout is complete
var itemSize by remember { mutableStateOf(Size(1f, 1f)) }
val anchors = mapOf(
0f to FeedItemSwipeState.NONE,
-itemSize.width to FeedItemSwipeState.LEFT,
itemSize.width to FeedItemSwipeState.RIGHT
)
Box(
modifier = Modifier
.fillMaxWidth()
) {
Box(Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.matchParentSize()
.clickable { // clickable on whole view
}
.padding(start = 30.dp) // left distance for drawer
.onGloballyPositioned { layoutCoordinates ->
itemSize = layoutCoordinates.size.toSize()
}
.swipeable( // swipeable after padding to allow drawerContent work
state = swipeableState,
anchors = anchors,
orientation = Orientation.Horizontal,
thresholds = { _, _ ->
FractionalThreshold(0.25f)
}
)
)
Text(
"item",
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
)
}
}
}
}
I'm not sure if that's something Scaffold should be responsible for, if you think it should - create an issue on compose issue tracker
This is the approach I ended up using after Philip gave his answer. It's slightly "less boxy". In summary - the key was simply to let the parent box handle the clicking - allowing a separate box to focus solely on the swiping - and the feeditem itself doesn't handle clicking
enum class FeedItemSwipeState {
NONE, LEFT, RIGHT,
}
#Composable
fun TestView(
) {
val scaffoldState = rememberScaffoldState(
rememberDrawerState(initialValue = DrawerValue.Closed)
)
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
},
) {
val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE)
// Needs to be set once layout is complete
var itemSize by remember { mutableStateOf(Size(1f, 1f)) }
val anchors = mapOf(
0f to FeedItemSwipeState.NONE,
-itemSize.width to FeedItemSwipeState.LEFT,
itemSize.width to FeedItemSwipeState.RIGHT
)
Box(
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { layoutCoordinates ->
itemSize = layoutCoordinates.size.toSize()
}
.combinedClickable(
onLongClick = { ... },
onClick = { ... },
)
) {
Box(
modifier = Modifier
.padding(start = 48.dp)
.matchParentSize()
.swipeable(
state = swipeableState,
anchors = anchors,
orientation = Orientation.Horizontal,
thresholds = { _, _ ->
FractionalThreshold(0.25f)
}
)
)
FeedItemPreview(
item = "item",
swipeableModifier = Modifier
.padding(start = 30.dp) // left distance for drawer
.onGloballyPositioned { layoutCoordinates ->
itemSize = layoutCoordinates.size.toSize()
}
.swipeable(
state = swipeableState,
anchors = anchors,
orientation = Orientation.Horizontal,
thresholds = { _, _ ->
FractionalThreshold(0.25f)
}
)
,
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
)
}
}
}
#Composable
fun FeedItemPreview(
item: String,
modifier: Modifier,
) {
Text(
item,
modifier = modifier
)
}
With example in app where swipeable area is highlighted by a border:

Jetpack Compose UI: How to create SearchView?

I want to create SearchView using jetpack compose, but I can't found any example that could helps me. Thanks in Advance.
This is a complex but full implementation for a SearchView from scratch. And the result will be as in the gif below, you can customize or remove InitialResults or Suggestions if you don't want your initial Composable to be displayed when SearchView is not focused and empty
Full implementation is available in github repository.
1- Create search states with
/**
* Enum class with different values to set search state based on text, focus, initial state and
* results from search.
*
* **InitialResults** represents the initial state before search is initiated. This represents
* the whole screen
*
*/
enum class SearchDisplay {
InitialResults, Suggestions, Results, NoResults
}
2- Then create class where you define your search logic
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<SuggestionModel>,
searchResults: List<TutorialSectionModel>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
override fun toString(): String {
return "🚀 State query: $query, focused: $focused, searching: $searching " +
"suggestions: ${suggestions.size}, " +
"searchResults: ${searchResults.size}, " +
" searchDisplay: $searchDisplay"
}
}
3- remember state to not update in every composition but only when our seach state changes
#Composable
fun rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<SuggestionModel> = emptyList(),
searchResults: List<TutorialSectionModel> = emptyList()
): SearchState {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
TutorialSectionModel is the model i used it can be generic type T or specific type you wish to display
4- Create a hint to be displayed when not focused
#Composable
private fun SearchHint(modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
color = Color(0xff757575),
text = "Search a Tag or Description",
)
}
}
I didn't use an Icon but if you wish you can add one
5- Create a SearchTextfield that can has cancel button, CircularProgressIndicator to display loading and BasicTextField to input
/**
* This is a stateless TextField for searching with a Hint when query is empty,
* and clear and loading [IconButton]s to clear query or show progress indicator when
* a query is in progress.
*/
#Composable
fun SearchTextField(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp,
bottom = 8.dp,
start = if (!focused) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50),
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.text.isEmpty()) {
SearchHint(modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.onFocusChanged {
onSearchFocusChange(it.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 6.dp)
.size(36.dp)
)
}
query.text.isNotEmpty() -> {
IconButton(onClick = onClearQuery) {
Icon(imageVector = Icons.Filled.Cancel, contentDescription = null)
}
}
}
}
}
}
}
}
You can remove CircularProgressBar or add Icon to Row which contains BasicTextField
6- SearchBar with SearchTextField above and back arrow to return back feature with. AnimatedVisibility makes sure arrow is animated when we focus BasicTextField in SearchTextField, it can also be used with Icon as magnifying glass.
#ExperimentalAnimationApi
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SearchBar(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
onBack: ()-> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
// Back button
IconButton(
modifier = Modifier.padding(start =2.dp),
onClick = {
focusManager.clearFocus()
keyboardController?.hide()
onBack()
}) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
}
SearchTextField(
query,
onQueryChange,
onSearchFocusChange,
onClearQuery,
searching,
focused,
modifier.weight(1f)
)
}
}
7- To use SearchBar create a rememberSearchState and update state as
Column is used here because rest of the screen is updated based on SearchState
LaunchedEffect or setting mutableState in ViewModel can be used to set query result or searching field of state to display loading
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
This is the SearchView you have in that image :
val (value, onValueChange) = remember { mutableStateOf("") }
TextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(fontSize = 17.sp),
leadingIcon = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
modifier = Modifier
.padding(10.dp)
.background(Color(0xFFE7F1F1), RoundedCornerShape(16.dp)),
placeholder = { Text(text = "Bun") },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
backgroundColor = Color.Transparent,
cursorColor = Color.DarkGray
)
)
TextField(
startingIcon = Icon(bitmap = searchIcon),
placeholder = { Text(...) }
)
Just create component, with FlexRow if you want to create UI like those.
FlexRow(crossAxisAlignment = CrossAxisAlignment.Start) {
inflexible {
drawImageResource(R.drawable.image_search)
}
expanded(1.0f) {
SingleLineEditText(
state,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search,
editorStyle = EditorStyle(textStyle = TextStyle(fontSize = 16.sp)),
onImeActionPerformed = {
onSearch(state.value.text)
}
)
}
}

Categories

Resources