如何在Jetpack Compose Android的LazyColumn中展示动画项视图

24

我有一个懒加载列视图的项目列表。

我们如何在从列表中删除一个项目时显示动画。

我需要为即将被删除的视图添加动画效果。删除操作是通过在视图内部按下删除图标完成的。

我尝试使用AnimationVisibility,但它并没有按照预期工作。

8个回答

33

新增了对惰性列表项位置进行动画处理的实验性功能。

LazyItemScope 中现在有一个名为 Modifier.animateItemPlacement() 的新修饰符可用。如果您为每个项目提供唯一的 key,则此方法可以使用。

用法示例:

var list by remember { mutableStateOf(listOf("A", "B", "C")) }
LazyColumn {
    item {
        Button(onClick = { list = list.shuffled() }) {
            Text("Shuffle")
        }
    }
    items(list, key = { it }) {
        Text("Item $it", Modifier.animateItemPlacement())
    }
}

当您通过LazyListScope.itemLazyListScope.items提供键时,此修饰符将启用项目重新排序动画。除了项目重新排序外,由排列或对齐更改等事件引起的所有其他位置更改也将被动画化。


7
我已经提供了密钥,但没有任何变化。当我打开屏幕时,它们只是出现,没有任何动画。 - user924

30

Compose 1.1.0 更新:

现在可以通过 Modifier.animateItemPlacement() 来实现动画元素位置的更改,但删除/插入动画仍然不可行,需要使用我所解释的方法手动实现。

要实现项目位置更改的动画效果,只需在列表中提供项目键值对,例如 key={ it.id }

原回答:

目前官方还没有正式支持这一功能,但正在研究中。您可能可以使用一种“hacky”方法来实现。

当列表更新时,您的Composable将被重新创建,但它尚不支持用于项目的动画,因此您必须在项目上添加一个布尔变量,并在其被“删除”时更改该值,而不是从列表中删除。显示更新后的列表后,您可以延迟动画删除该项目,然后在动画结束后将其从列表中删除。

我个人没有测试过此方法,因此它可能无法按预期工作,但这是我能想到的唯一的方法,因为lazy lists不支持更新动画。


1
是的,请在此跟踪:https://issuetracker.google.com/issues/150812265 - Ajay Venugopal
现在实际上有一种方法:https://dev59.com/-cLra4cB1Zd3GeqPSNHc#70073933 - F.Mysir
我刚刚测试了你的方法,发现在延迟后元素被删除时会出现奇怪的故障,因为lazylist被重新组合了。 - mama
@mama 是的,它可能不会完美无缺,但你应该通过调整细节使其几乎完美。在Compose中得到适当支持之前,您必须接受这些问题。尽管在我看来,缺少这些功能确实是不可接受的。 - Yasan
1
@YASAN - 我通过在视图模型中添加 bulk_delete 函数并在释放 lazyList 时触发它,使其更加流畅。 - mama
显示剩余2条评论

5

YASAN 所说的那样,通过在你的项目数据类中添加 isVisible 属性,然后将你的项目视图包装在 AnimatedVisibility 组合中,你可以使用 AnimatedVisibility 来在 LazyColumn 项上制作 'slideOut' + 'fadeOut' 动画。由于该 API 仍处于实验阶段,请小心使用。

如果您或其他人可能需要查找它,我会在此处附上我的代码片段供参考。

对于 LazyColumn

LazyColumn {
    items(
        items = notes,
        key = { item: Note -> item.id }
    ) { item ->
        AnimatedVisibility(
            visible = item.isVisible,
            exit = fadeOut(
                animationSpec = TweenSpec(200, 200, FastOutLinearInEasing)
            )
        ) {
            ItemNote(
                item
            ) {
                notes = changeNoteVisibility(notes, it)
            }
        }
    }
}

对于 Item Composable

@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Composable
fun ItemNote(
    note: Note,
    onSwipeNote: (Note) -> Unit
) {
    val iconSize = (-68).dp
    val swipeableState = rememberSwipeableState(0)
    val iconPx = with(LocalDensity.current) { iconSize.toPx() }
    val anchors = mapOf(0f to 0, iconPx to 1)

    val coroutineScope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(75.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                orientation = Orientation.Horizontal
            )
            .background(Color(0xFFDA5D5D))
    ) {
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .align(Alignment.CenterEnd)
                .padding(end = 10.dp)
        ) {
            IconButton(
                modifier = Modifier.align(Alignment.Center),
                onClick = {
                    Log.d("Note", "Deleted")
                    coroutineScope.launch {
                        onSwipeNote(note)
                    }
                }
            ) {
                Icon(
                    Icons.Default.Delete,
                    contentDescription = "Delete this note",
                    tint = Color.White
                )
            }
        }

        AnimatedVisibility(
            visible = note.isVisible,
            exit = slideOutHorizontally(
                targetOffsetX = { -it },
                animationSpec = TweenSpec(200, 0, FastOutLinearInEasing)
            )
        ) {
            ConstraintLayout(
                modifier = Modifier
                    .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                    .fillMaxHeight()
                    .fillMaxWidth()
                    .background(Color.White)
            ) {
                val (titleText, contentText, divider) = createRefs()

                Text(
                    modifier = Modifier.constrainAs(titleText) {
                        top.linkTo(parent.top, margin = 12.dp)
                        start.linkTo(parent.start, margin = 18.dp)
                        end.linkTo(parent.end, margin = 18.dp)
                        width = Dimension.fillToConstraints
                    },
                    text = note.title,
                    fontSize = 16.sp,
                    fontWeight = FontWeight(500),
                    textAlign = TextAlign.Start
                )

                Text(
                    modifier = Modifier.constrainAs(contentText) {
                        bottom.linkTo(parent.bottom, margin = 12.dp)
                        start.linkTo(parent.start, margin = 18.dp)
                        end.linkTo(parent.end, margin = 18.dp)
                        width = Dimension.fillToConstraints
                    },
                    text = note.content,
                    textAlign = TextAlign.Start
                )

                Divider(
                    modifier = Modifier.constrainAs(divider) {
                        start.linkTo(parent.start)
                        end.linkTo(parent.end)
                        bottom.linkTo(parent.bottom)
                        width = Dimension.fillToConstraints
                    },
                    thickness = 1.dp,
                    color = Color.DarkGray
                )
            }
        }
    }
}

此外,数据类Note

data class Note(
    var id: Int,
    var title: String = "",
    var content: String = "",
    var isVisible: Boolean = true
)

1
你可能想要为“Note”类实现一个DTO转换器,并将你的“isVisible”字段放在那里,而不是直接放在“Note”类中。你不希望在数据库中存储与UI相关的字段。 - Calamity
或者维护可见项目ID的可变状态列表。 - Sheikh Zakir Ahmad

3

我只是试验一下,但或许这可以在他们建立出正式解决方案之前帮上忙。

    @Composable
fun <T> T.AnimationBox(
    enter: EnterTransition = expandVertically() + fadeIn(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    content: @Composable T.() -> Unit
) {
    val state = remember {
        MutableTransitionState(false).apply {
            // Start the animation immediately.
            targetState = true
        }
    }

    AnimatedVisibility(
        visibleState = state,
        enter = enter,
        exit = exit
    ) { content() }
}


LazyColumn(
  state = lazyState
) {
    item {
        AnimationBox {
            Item(text = "TEST1!")
        }
        AnimationBox {
            Item(text = "TEST2!")
        }
    }
}

1

0

0

0
根据当前的Compose版本,您需要将animateItemPlacement放在SwipeToDismiss组合中的一个修饰符中,就像这样:
LazyColumn {
    items(...) { 
        val dismissState = rememberDismissState()
        SwipeToDismiss(
            modifier = Modifier.animateItemPlacement(),
            ...
        ) {
          ...
        }
    }
}

在撰写本文时,这是一个实验性的功能。您需要在您的组合函数中添加@OptIn(ExperimentalFoundationApi::class)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接