What is the correct way to use SwipeToDismiss
and LazyColumn
in android compose alpha09
?
My approach:
LazyColumn(
modifier = Modifier.padding(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
items(items = items) {
TrackedActivityRecord(it.activity, it.record, scaffoldState)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TrackedActivityRecord(
activity: TrackedActivity,
record: TrackedActivityRecord,
scaffoldState: ScaffoldState,
vm: TimelineVM = viewModel()
){
val dismissState = rememberDismissState()
if (dismissState.value != DismissValue.Default){
LaunchedEffect(subject = activity){
val deleted = scaffoldState.snackbarHostState.showSnackbar("Awesome", "do it")
if (deleted == SnackbarResult.Dismissed){
vm.rep.deleteRecordById(activity.id, record.id)
}
dismissState.snapTo(DismissValue.Default)
}
}
SwipeToDismiss(
state = dismissState,
background = {
Box(Modifier.size(20.dp). background(Color.Red))
},
) {
Record(activity = activity, record = record)
}
}
There a is problem when the LazyColumn
is recomposed the item on the deleted position is Dismissed
- not visible. I hacked it with dismissState.snapTo(DismissValue.Default)
. But for split of a second you can see the old item visible. If I do not use remember but DismissState I get: java.lang.IllegalArgumentException: Cannot round NaN value.
caused by androidx.compose.material.SwipeToDismissKt$SwipeToDismiss$2$1$1$1.invoke-nOcc-ac(SwipeToDismiss.kt:244)
modified from https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss:
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.draw.scale
import androidx.compose.material.DismissValue.*
import androidx.compose.material.DismissDirection.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
// This is an example of a list of dismissible items, similar to what you would see in an
// email app. Swiping left reveals a 'delete' icon and swiping right reveals a 'done' icon.
// The background will start as grey, but once the dismiss threshold is reached, the colour
// will animate to red if you're swiping left or green if you're swiping right. When you let
// go, the item will animate out of the way if you're swiping left (like deleting an email) or
// back to its default position if you're swiping right (like marking an email as read/unread).
@ExperimentalMaterialApi
@Composable
fun MyContent(
items: List<ListItem>,
dismissed: (listItem: ListItem) -> Unit
) {
val context = LocalContext.current
LazyColumn {
items(items, {listItem: ListItem -> listItem.id}) { item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(EndToStart)){
dismissed(item)
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 1.dp),
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
Text(item.text)
}
}
)
}
}
}
data class ListItem(val id:String, val text:String)
The main problem in the original is that the dismiss-state is remembered by the item's position. When the list changes (which is quite obvious when deleting an item), the remembered dismissState will then apply to the next item (which is wrong of course). To remedy this use items(items, {listItem: MyRoutesViewModel.ListItem -> listItem.id} ) instead of just items(items)
Here you can find an example of how to use LazyColumn with SwipeToDismiss:
// This is an example of a list of dismissible items, similar to what you would see in an
// email app. Swiping left reveals a 'delete' icon and swiping right reveals a 'done' icon.
// The background will start as grey, but once the dismiss threshold is reached, the colour
// will animate to red if you're swiping left or green if you're swiping right. When you let
// go, the item will animate out of the way if you're swiping left (like deleting an email) or
// back to its default position if you're swiping right (like marking an email as read/unread).
LazyColumn {
items(items) { item ->
var unread by remember { mutableStateOf(false) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissedToEnd) unread = !unread
it != DismissedToEnd
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.75f else 1f
)
Box(
Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
ListItem(
text = {
Text(item, fontWeight = if (unread) FontWeight.Bold else null)
},
secondaryText = { Text("Swipe me left or right!") }
)
}
}
)
}
}
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss
Try to pass the key inside lazy column. Then rememberDismissState will work according to the item id instead of the list position.
LazyColumn(modifier = Modifier
.background(Background)
.padding(bottom = SpaceLarge + 20.dp),
state = bottomListScrollState
) {
if (newsList.value.isNotEmpty()) {
items(
items = newsList.value,
// Apply the key like below
key = { news -> news.url },
itemContent = { news ->
var isDeleted by remember { mutableStateOf(false) }
val dismissState = rememberDismissState(
confirmStateChange = {
Timber.d("dismiss value ${it.name}")
if (it == DismissValue.DismissedToEnd) isDeleted =
!isDeleted
else if (it == DismissValue.DismissedToStart) isDeleted =
!isDeleted
it != DismissValue.DismissedToStart || it != DismissValue.DismissedToEnd
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 2.dp),
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
},
background = {
val direction =
dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Red
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Delete
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
}, dismissContent = {
if (isDeleted) {
viewModel.deleteNews(news)
Timber.d("Deleted ${news.url}")
snackbarController.getScope().launch {
snackbarController.showSnackbar(
scaffoldState = scaffoldState,
message = "Article successfully Deleted",
actionLabel = "Undo"
)
viewModel.result = news
}
} else {
NewsColumnItem(news = news) {
viewModel.result = news
actions.gotoNewsViewScreen(news.url.encode())
}
}
}
)
})
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With