Compose LazyColumn select one item - android

I want to select one item of my LazyColumn and change the textcolor.
How is it possible to identify which item is selected?
Code:
val items = listOf(Pair("A", 1), Pair("AA", 144), Pair("BA", 99))
var selectedItem by mutableStateOf(items[0])
LazyColumn {
this.items(items = items) {
Row(modifier = Modifier.clickable(onClick = {selectedItem = it}) {
if (selectedItem == it) {
Text(it.first, color = Color.Red)
} else {
Text(it.first)
}
}
}
}
Depending how I save it (with remember or without) they just highlight both if I click on one and not just the one I clicked the last.

You can use the the .selectable modifier instead of .clickable
Something like:
data class Message(val id: Int,
val message : String)
val messages : List<Message> = listOf(...))
val listState = rememberLazyListState()
var selectedIndex by remember{mutableStateOf(-1)}
LazyColumn(state = listState) {
items(items = messages) { message ->
Text(
text = message.message,
modifier = Modifier
.fillMaxWidth()
.background(
if (message.id == selectedIndex)
Color.Red else Color.Yellow
)
.selectable(
selected = message.id == selectedIndex,
onClick = { if (selectedIndex != message.id)
selectedIndex = message.id else selectedIndex = -1})
)
}
}
In your case you can use:
var selectedItem by remember{mutableStateOf( "")}
LazyColumn {
this.items(items = items) {
Row(modifier = Modifier.selectable(
selected = selectedItem == it.first,
onClick = { selectedItem = it.first}
)
) {
if (selectedItem == it.first) {
Text(it.first, color = Color.Red)
} else {
Text(it.first)
}
}
}
}

Note that in the accepted answer, all the item views will be recomposed every time the selection changes, because the lambdas passed in onClick and content (of Row) are not stable (https://developer.android.com/jetpack/compose/lifecycle#skipping).
Here's one way to do it so that only the deselected and selected items are recomposed:
#Composable
fun ItemView(index: Int, selected: Boolean, onClick: (Int) -> Unit){
Text(
text = "Item $index",
modifier = Modifier
.clickable {
onClick.invoke(index)
}
.background(if (selected) MaterialTheme.colors.secondary else Color.Transparent)
.fillMaxWidth()
.padding(12.dp)
)
}
#Composable
fun LazyColumnWithSelection(){
var selectedIndex by remember { mutableStateOf(0) }
val onItemClick = { index: Int -> selectedIndex = index}
LazyColumn(
modifier = Modifier.fillMaxSize(),
){
items(100){ index ->
ItemView(
index = index,
selected = selectedIndex == index,
onClick = onItemClick
)
}
}
}
Note how the arguments passed to ItemView only change for the items whose selected state changes. This is because the onItemClick lambda is the same all the time.

There is a clickable modifier which allows easy detection of a click, and it also provides accessibility features and displays visual indicators when tapped (such as ripples).
#Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
For more information see https://developer.android.com/jetpack/compose/gestures

Related

How to select multiple items in LazyColumn in JetpackCompose

How to select multiple items in LazyColumn and finally add the selected items in a seperate list.
GettingTags(tagsContent ={ productTags ->
val flattenList = productTags.flatMap {
it.tags_list
}
Log.i(TAG,"Getting the flattenList $flattenList")
LazyColumn{
items(flattenList){
ListItem(text = {Text(it) })
if(selectedTagItem) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
modifier = Modifier.size(20.dp)
)
}
}
}
})
Mutable variable state
var selectedTagItem by remember{
mutableStateOf(false)
}
First create a class with selected variable to toggle
#Immutable
data class MyItem(val text: String, val isSelected: Boolean = false)
Then create a SnapshotStateList via mutableStateListOf that contains all of the items, and can trigger recomposition when we update any item with new instance, add or remove items also. I used a ViewModel but it's not mandatory. We can toggle items using index or get selected items by filtering isSelected flag
class MyViewModel : ViewModel() {
val myItems = mutableStateListOf<MyItem>()
.apply {
repeat(15) {
add(MyItem(text = "Item$it"))
}
}
fun getSelectedItems() = myItems.filter { it.isSelected }
fun toggleSelection(index: Int) {
val item = myItems[index]
val isSelected = item.isSelected
if (isSelected) {
myItems[index] = item.copy(isSelected = false)
} else {
myItems[index] = item.copy(isSelected = true)
}
}
}
Create LazyColumn with key, key makes sure that only updated items are recomposed, as can be seen in performance document
#Composable
private fun SelectableLazyListSample(myViewModel: MyViewModel) {
val selectedItems = myViewModel.getSelectedItems().map { it.text }
Text(text = "Selected items: $selectedItems")
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(
myViewModel.myItems,
key = { _, item: MyItem ->
item.hashCode()
}
) { index, item ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Red, RoundedCornerShape(8.dp))
.clickable {
myViewModel.toggleSelection(index)
}
.padding(8.dp)
) {
Text("Item $index", color = Color.White, fontSize = 20.sp)
if (item.isSelected) {
Icon(
modifier = Modifier
.align(Alignment.CenterEnd)
.background(Color.White, CircleShape),
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
)
}
}
}
}
}
Result

Change background color of surface item when click in jetpack compose

I want to make a background color change when user select a item on Surface. I am using Column in my parent, so I can't make LazyColumn. So I am using Foreach to make a list of view. By default no view will be selected, when user click on item then I want change the color. Note: Only one item will select at a time.
ScanDeviceList
#Composable
fun ColumnScope.ScanDeviceList(
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}
ScanDeviceItem
#Composable
fun ScanDeviceItem(
index: Int,
scanResult: ScanResult,
scanDeviceList: List<ScanResult>
) {
Column {
if (index == 0) {
Divider(color = Cloudy, thickness = 1.dp)
}
Text(
text = scanResult.device.name,
modifier = Modifier.padding(vertical = 10.dp)
)
if (index <= scanDeviceList.lastIndex) {
Divider(color = Cloudy, thickness = 1.dp)
}
}
}
ClickableItemContainer
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction() },
interactionSource = interactionSource,
color = White
) {
content(interactionSource)
}
}
)
}
I want to something like this
Now above solution is only working on ripple effect, Now I want to extend my function to select a single item at a time. Many Thanks
You can store the selected index and use the clickAction to update its value when the user clicks on each item.
Something like:
#Composable
fun MyList(
var selectedIndex by remember { mutableStateOf(-1) }
Column() {
itemsList.forEachIndexed() { index, item ->
MyItem(
selected = selectedIndex == index,
clickAction = { selectedIndex = index }
)
}
}
#Composable
fun MyItem(
selected : Boolean = false,
clickAction: () -> Unit
){
Surface(
onClick = { clickAction() },
color = if (selected) Color.Red else Color.Yellow
) {
Text("Item...")
}
}
To have only item selected at a time, you can follow a behavior that is similar. You save your item selected in a mutableState variable, like:
var itemSelected by remember { mutableState(ScanDeviceResult()) }
After that, when you click the item, you update the itemSelected, something like:
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ newItemSelected ->
itemSelected = newItemSelected
}
Now, each ClickableItemContainer needs to have an associated ScanResult.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
itemSelected: ScanResult,
scanResult: ScanResult,
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: (ScanResult) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction.invoke(scanResult) },
interactionSource = interactionSource,
color = if (itemSelected == scanResult) YOUR_BACKGROUND-SELECTED_COLOR else White
) {
content(interactionSource)
}
}
)
}
Now, on your ScanDeviceList method:
#Composable
fun ColumnScope.ScanDeviceList(
itemSelected: MutableState<ScanResult>,
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
itemSelected = itemSelected,
scanResult = scanResult,
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ scanResultToSelect ->
itemSelected = scanResultToSelect
}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}

Single Selection - DeSelection in Lazy Column

PROBLEM ::: I want to create a lazy column where I can select or deselect only one option at a time. Right now, whenever I click on row component inside lazy column, all the rows get selected.
CODE :::
#Composable
fun LazyColumnWithSelection() {
var isSelected by remember {
mutableStateOf(false)
}
var selectedIndex by remember { mutableStateOf(0) }
val onItemClick = { index: Int -> selectedIndex = index }
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(100) { index ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick.invoke(index)
if (selectedIndex == index) {
isSelected = !isSelected
}
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(text = "Item $index", modifier = Modifier.padding(12.dp), color = Color.White)
if (isSelected) {
Icon(imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
modifier = Modifier.size(20.dp))
}
}
}
}
}
CURRENT RESULT :::
Before Clicking ->
After Clicking ->
You can see all the items are getting selected but I should be able to select or deselect one item at a time not all.
I tried to use remember state for selection but I think I'm doing wrong something in the index selection or maybe if statement.
This should probably give you a head start.
So we have 4 components here:
Data Class
Class state holder
Item Composable
ItemList Composable
ItemData
data class ItemData(
val id : Int,
val display: String,
val isSelected: Boolean = false
)
State holder
class ItemDataState {
val itemDataList = mutableStateListOf(
ItemData(1, "Item 1"),
ItemData(2, "Item 2"),
ItemData(3, "Item 3"),
ItemData(4, "Item 4"),
ItemData(5, "Item 5")
)
// were updating the entire list in a single pass using its iterator
fun onItemSelected(selectedItemData: ItemData) {
val iterator = itemDataList.listIterator()
while (iterator.hasNext()) {
val listItem = iterator.next()
iterator.set(
if (listItem.id == selectedItemData.id) {
selectedItemData
} else {
listItem.copy(isSelected = false)
}
)
}
}
}
Item Composable
#Composable
fun ItemDisplay(
itemData: ItemData,
onCheckChanged: (ItemData) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.border(BorderStroke(Dp.Hairline, Color.Gray)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = if (itemData.isSelected) "I'm selected!" else itemData.display)
Checkbox(
checked = itemData.isSelected,
onCheckedChange = {
onCheckChanged(itemData.copy(isSelected = !itemData.isSelected))
}
)
}
}
Finally the ItemList (LazyColumn)
#Composable
fun ItemList() {
val itemDataState = remember { ItemDataState() }
LazyColumn {
items(itemDataState.itemDataList, key = { it.id } ) { item ->
ItemDisplay(
itemData = item,
onCheckChanged = itemDataState::onItemSelected
)
}
}
}
All of these are copy-and-pasteable so you can run it quickly. The codes should be simple enough for you to dissect them easily and use them as a reference for your own use-case.
Notice that we use a data class here which has an id property to be unique and we're using it as a key parameter for LazyColumn's item.
I usually implement my UI collection components with a unique identifier to save me from potential headaches such as UI showing/removing/recycling wrong items.
Remember index instead of Boolean (isSelected).

How to set the initial start screen for a tab with jetpack compose

Right now, the start screen is the "Camera Screen" (at index 0)
I want to set the start screen to the "Chat Screen" (i.e index 1 not 0)
That is: When the user opens the app, the "Chat Screen" is the active screen and NOT the "Camera Screen" -- Just like how it is on whatsApp
private val tabs = listOf(
TabItem.Camera,
TabItem.Chat,
TabItem.Status,
TabItem.Call
)
#Composable
fun TabLayout(
modifier: Modifier = Modifier,
tabs: List<TabItem>,
selectedIndex: Int = 1,
onPageSelected: ((tabItem: TabItem) -> Unit)
) {
TabRow(
selectedTabIndex = selectedIndex,
divider = { }
) {
tabs.forEachIndexed{index, tabItem ->
Tab(
selected = index == selectedIndex,
modifier = modifier.background(MaterialTheme.colors.primary),
onClick = {
onPageSelected(tabItem)
},
text =
{
if (tabItem == TabItem.Camera) {
Icon(painter = painterResource(id = R.drawable.ic_camera), stringResource(id = R.string.icon)).toString()
}
else {
Text(
text = stringResource(id = tabItem.title).uppercase(Locale.ROOT),
style = MaterialTheme.typography.caption,
)
}
},
)
}
}
}
USAGE
val pagerState = rememberPagerState()
//tab layout
TabLayout(tabs = tabs, selectedIndex = pagerState.currentPage ,
onPageSelected = { tabItem->
coroutineScope.launch {
pagerState.animateScrollToPage(tabItem.index)
}
})
So, this was pretty easy.
All I had to do was override the default initial state of pagerState
val pagerState = rememberPagerState(initialPage = 1)
You need to initialize your selectedIndex variable that you're passing into TabRow as 1.
You can try to use state for the selected tab:
#Composable
fun TabLayout(
modifier: Modifier = Modifier,
tabs: List<TabItem>,
selectedIndex: Int = 1,
onPageSelected: ((tabItem: TabItem) -> Unit)
) {
var tabIndex by remember { mutableStateOf(selectedIndex) }
TabRow(
selectedTabIndex = tabIndex,
divider = { }
) {
tabs.forEachIndexed{index, tabItem ->
Tab(
selected = index == tabIndex,
modifier = modifier.background(MaterialTheme.colors.primary),
onClick = {
tabIndex = index
onPageSelected(tabItem)
},
replace Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
to Modifier.tabIndicatorOffset(tabPositions[tabIndex])
tabIndex value is equals to 1
see https://androidx.tech/artifacts/compose.material3/material3/1.0.0-alpha08-source/androidx/compose/material3/TabRow.kt.html

State hoisting for each item in LazyColumn

I've gone through this codelab. In step number 7, when clicking on single row's text it's changing its color, but function will not keep track of it, meaning it will disappear after re-composition.
I want list to remember color of single item thus I've move state hoisting to the NameList function level.
Unfortunately it's not working.
Where's the bug?
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name, ->
val isSelected = remember { mutableStateOf(false)}
Greeting(name = name,isSelected.value){ newSelected -> isSelected.value = newSelected}
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String,isSelected : Boolean, updateSelected : (Boolean) -> Unit) {
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { updateSelected(!isSelected)}),
text = "Hello $name",
)
}
You should hoist your selection state to the caller of NameList function.
#Composable
fun MyScreen() {
// Fake list of names
val namesList = (1..100).map { "Item $it" }
// Here, we're keeping the selected positions.
// At the beginning, all names are not selected.
val selection = remember {
mutableStateListOf(*namesList.map { false }.toTypedArray())
}
NameList(
// list of names
names = namesList,
// list of selected items
selectedItems = selection,
// this function will update the list above
onSelected = { index, selected -> selection[index] = selected },
// just to occupy the whole screen
modifier = Modifier.fillMaxSize()
)
}
Then, your NameList will look like this:
#Composable
fun NameList(
names: List<String>,
selectedItems: List<Boolean>,
onSelected: (index: Int, selected: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedItems[index],
updateSelected = { onSelected(index, it) }
)
Divider(color = Color.Black)
}
}
}
Nothing changes on Greeting function.
Here is the result:

Categories

Resources