Reputation: 4126
I am trying to achieve something like this but with Jetpack Compose. In other words swipe to delete like we could do in RecyclerView
with ItemTouchHelper
and class DiffCallBack : DiffUtil.ItemCallback<RvModel>()
where we could see enter - exit animations
and then the list moving gracefully up or down where the item has been inserted or removed.
This is what I have tried:
LazyColumn(state = listState) {
items(products, {listItem:InventoryEntity -> listItem.inventoryId}) { item ->
var unread by remember { mutableStateOf(false) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToEnd) unread = !unread
it != DismissValue.DismissedToEnd
}
)
val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart)
if (dismissState.isDismissed(DismissDirection.EndToStart)){
LaunchedEffect(Unit) {
delay(300)
viewModel.deleteProduct(item.inventoryId)
}
}
var itemAppeared by remember { mutableStateOf(!columnAppeared) }
LaunchedEffect(Unit) {
itemAppeared = true
}
AnimatedVisibility(
visible = itemAppeared && !isDismissed,
exit = shrinkVertically(
animationSpec = tween(
durationMillis = 300,
)
),
enter = expandVertically(
animationSpec = tween(
durationMillis = 300
)
)
) {
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.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.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
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 = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
ProductRow(product = item, number = item.inventoryId)
}
}
)
}
}
}
Even though it works. Scrolling is not smooth, and when I scroll up it jumps to the top. What is the right way to implement this function?
Upvotes: 4
Views: 5982
Reputation: 4126
Recently google anounced compose Version 1.1.0-beta03
. Now we have a new way on how we can animate items. They have introduced a new modifier: Modifier.animateItemPlacement()
. You may find latest compose version in this link.
I will try to post an example with minimum code so that you can reproduce it and see how you could achieve a SwipeToDismiss inside LazyColumn with animation.
Data Class To store information:
data class DataSet(
val itemId: Int,
val itemName: String,
val itemQty: String
)
Comparator to compare list items:
private val ListComparator = Comparator<DataSet> { left, right ->
left.itemId.compareTo(right.itemId)
}
The row of each of our items:
@Composable
fun ItemRow(
modifier: Modifier = Modifier,
product: DataSet,
number: Int
) {
Card(
shape = RoundedCornerShape(4.dp),
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
backgroundColor = Color.LightGray
) {
Row(modifier = modifier) {
Text(
text = "$number.", modifier = Modifier
.weight(2f)
.padding(start = 8.dp, end = 4.dp)
)
Text(
text = product.itemName, modifier = Modifier
.weight(10f)
.padding(end = 4.dp)
)
Text(
text = product.itemQty, modifier = Modifier
.weight(2f)
.padding(end = 4.dp)
)
}
}
}
Putting all together to our composable:
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@Composable
fun helloWorld() {
var list by remember { mutableStateOf(listOf<DataSet>()) }
val comparator by remember { mutableStateOf(ListComparator) }
LazyColumn {
item {
Button(onClick = {
list = list + listOf(DataSet((0..1111).random(), "A random item", "100"))
}) {
Text("Add an item to the list")
}
}
val sortedList = list.sortedWith(comparator)
items(sortedList, key = { it.itemId }) { item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(DismissDirection.EndToStart)) {
list = list.toMutableList().also { it.remove(item) } // remove
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier
.padding(vertical = 1.dp)
.animateItemPlacement(),
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.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
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 = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
ItemRow(
product = item,
number = item.itemId
)
}
}
)
}
}
}
For reference and see also other ways on how you could use Modifier.animateItemPlacement()
you may check this example posted from Google.
Upvotes: 6