简体   繁体   中英

SwipeToDismiss inside LazyColumn with animation

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?

Last week 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM