我正在使用Jetpack Compose实现一个简单的TODO应用程序.我有以下问题: 当我试图从我的LazyColumn中通过从右向左滑动来删除元素(卡片)时,动画看起来不太好.当它结束时,看起来几乎就像被删除的那张下面的卡片跳到了被删除的那张留下的空白处,看GIF可以更好地理解:

swipe to delete animation

我想动画结束"顺利"没有卡下面删除的一个跳起来. 我怀疑这个问题可能与我在Lazy柱形上设置的verticalArrangement = Arrangement.spacedBy(32.dp)有关(也许是因为动画没有考虑这个额外的空间),但我需要它来将每张卡彼此分开.

下面是我的代码:

MainScren.kt

package com.pochopsp.dailytasks.presentation.screen

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.pochopsp.dailytasks.R
import com.pochopsp.dailytasks.data.database.TaskEvent
import com.pochopsp.dailytasks.data.database.TaskState
import com.pochopsp.dailytasks.presentation.AddTaskDialog
import com.pochopsp.dailytasks.presentation.common.SwipeToDeleteContainer
import com.pochopsp.dailytasks.presentation.tasks.TaskCard

@Composable
fun MainScreen(
    state: TaskState,
    onEvent: (TaskEvent) -> Unit
) {

    Scaffold(
        modifier = Modifier,
        content = { paddingValues ->

            if(state.isAddingTask){
                AddTaskDialog(state = state, onEvent = onEvent)
            }

            Column(
                modifier = Modifier.padding(start = 30.dp, end = 30.dp,
                    bottom = paddingValues.calculateBottomPadding(),
                    top = paddingValues.calculateTopPadding())
            ) {
                Row (
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.weight(1f)
                ){
                    Text(
                        text = stringResource(R.string.done_tasks_count,
                            state.tasks.filter { t -> t.done }.size, state.tasks.size),
                        fontWeight = FontWeight.SemiBold,
                        modifier = Modifier.weight(2f)
                    )
                    Spacer(modifier = Modifier.weight(1.5f))
                    ElevatedButton(
                        elevation = ButtonDefaults.buttonElevation(
                            defaultElevation = 2.dp
                        ),
                        shape = RoundedCornerShape(8.dp),
                        contentPadding = PaddingValues(0.dp),
                        modifier = Modifier
                            .weight(0.9f)
                            .defaultMinSize(minWidth = 1.dp, minHeight = 30.dp),
                        onClick = { /*TODO*/ }
                    ) {
                        Icon(
                            Icons.Filled.FilterAlt,
                            contentDescription = "Localized description",
                        )
                    }
                    Spacer(modifier = Modifier.weight(0.5f))
                    ElevatedButton(
                        elevation = ButtonDefaults.buttonElevation(
                            defaultElevation = 2.dp
                        ),
                        shape = RoundedCornerShape(8.dp),
                        contentPadding = PaddingValues(0.dp),
                        modifier = Modifier
                            .weight(0.9f)
                            .defaultMinSize(minWidth = 1.dp, minHeight = 30.dp),
                        onClick = { /*TODO*/ }
                    ) {
                        Icon(
                            Icons.AutoMirrored.Filled.Sort,
                            contentDescription = "Localized description",
                        )
                    }
                }
                Spacer(modifier = Modifier.weight(0.2f))
                LazyColumn(
                    contentPadding = PaddingValues(bottom = 10.dp, top = 10.dp),
                    verticalArrangement = Arrangement.spacedBy(32.dp),
                    modifier = Modifier.weight(10f)
                ) {
                    items(
                        items = state.tasks,
                        key = { it.id }
                    ) { task ->
                        SwipeToDeleteContainer(
                            item = task,
                            onDelete = {
                                onEvent(TaskEvent.DeleteTask(task.id))
                            }
                        ) {
                            TaskCard(
                                taskCardDto = task,
                                onCheckedChange = { id, done -> onEvent(TaskEvent.SetDone(id, done)) }
                            )
                        }
                    }
                }
            }
        },
        bottomBar = {
            BottomAppBar(
                modifier = Modifier.graphicsLayer { shadowElevation = 80f },
                containerColor = Color(0xFFFFFFFF),
                contentColor = Color(0xFFA0A0A0),
                actions = {
                    IconButton(onClick = { /* do something */ }) {
                        Icon(
                            Icons.Outlined.Settings,
                            contentDescription = "Localized description"
                        )
                    }
                    IconButton(onClick = { /* do something */ }) {
                        Icon(
                            Icons.Outlined.Search,
                            contentDescription = "Localized description",
                        )
                    }
                },
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = {
                            onEvent(TaskEvent.ShowDialog)
                        },
                        containerColor = Color(0xFF2984BA),
                        contentColor = Color(0xFFFFFFFF),
                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
                    ) {
                        Icon(Icons.Filled.Add, "Localized description")
                    }
                }
            )
        }
    )
}

TaskCard.kt

package com.pochopsp.dailytasks.presentation.tasks

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.pochopsp.dailytasks.R
import com.pochopsp.dailytasks.data.database.TaskCardDto
import com.pochopsp.dailytasks.presentation.theme.Constants

@Composable
fun TaskCard(taskCardDto: TaskCardDto, onCheckedChange: (Int, Boolean) -> Unit) {

    ElevatedCard(
        elevation = CardDefaults.cardElevation(
            defaultElevation = 6.dp
        ),
        modifier = Modifier
            .height(70.dp)
            .fillMaxWidth(),
        shape = Constants.cardShape
    ) {
        Row(
            modifier = Modifier
                .padding(vertical = 12.dp, horizontal = 20.dp)
                .fillMaxWidth()
                .fillMaxHeight(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Image(
                painter = painterResource(id = R.drawable.postit),
                contentDescription = "default task icon",
                modifier = Modifier.weight(0.7f)
            )
            Text(
                text = taskCardDto.title,
                fontWeight = FontWeight.Medium,
                style = if (taskCardDto.done) {
                    LocalTextStyle.current.copy(textDecoration = TextDecoration.LineThrough)
                } else LocalTextStyle.current.copy(),
                modifier = Modifier
                    .weight(4f)
                    .padding(horizontal = 20.dp)
            )
            Box (
                modifier = Modifier
                    .background(Color.White)
                    .weight(0.5f)
                    .aspectRatio(1f)
            )
            {
                Checkbox(
                    checked = taskCardDto.done,
                    onCheckedChange = { isChecked -> onCheckedChange(taskCardDto.id, isChecked) },
                    modifier = Modifier.scale(1.5f),
                    colors = CheckboxDefaults.colors(
                        checkedColor = Color(0xFF2984BA),
                        uncheckedColor = Color(0xFF2984BA),
                        checkmarkColor = Color.White,
                        )
                )
            }
        }
    }
}

SwipeToDeleteContainer.kt

package com.pochopsp.dailytasks.presentation.common

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissState
import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.pochopsp.dailytasks.presentation.theme.Constants
import kotlinx.coroutines.delay

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> SwipeToDeleteContainer(
    item: T,
    onDelete: (T) -> Unit,
    animationDuration: Int = 500,
    content: @Composable (T) -> Unit
) {
    var isRemoved by remember {
        mutableStateOf(false)
    }
    val state = rememberDismissState(
        confirmValueChange = { value ->
            if (value == DismissValue.DismissedToStart) {
                isRemoved = true
                true
            } else {
                false
            }
        }
    )

    LaunchedEffect(key1 = isRemoved) {
        if(isRemoved) {
            delay(animationDuration.toLong())
            onDelete(item)
        }
    }

    AnimatedVisibility(
        visible = !isRemoved,
        exit = shrinkVertically(
            animationSpec = tween(durationMillis = animationDuration),
            shrinkTowards = Alignment.Top
        ) + fadeOut()
    ) {
        SwipeToDismiss(
            state = state,
            background = {
                DeleteBackground(swipeDismissState = state)
            },
            dismissContent = { content(item) },
            directions = setOf(DismissDirection.EndToStart)
        )
    }
}



@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DeleteBackground(
    swipeDismissState: DismissState
) {
    val color = if (swipeDismissState.dismissDirection == DismissDirection.EndToStart) {
        Color.Red
    } else Color.Transparent

    Box(
        modifier = Modifier
            .clip(Constants.cardShape)
            .background(color)
            .padding(16.dp)
            .fillMaxSize(),
        contentAlignment = Alignment.CenterEnd
    ) {
        Icon(
            imageVector = Icons.Default.Delete,
            contentDescription = null,
            tint = Color.White
        )
    }
}

Constants.kt

package com.pochopsp.dailytasks.presentation.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp

object Constants {
    val cardShape = RoundedCornerShape(size = 9.dp)
}

推荐答案

我已经设法解决了这个问题,删除了包装SwipeToDismiss可组合物的AnimatedVisibility(所以没有动画),而用Box可组合物与modifier = Modifier.animateItemPlacement()来包装LazyColumn项的itemContent.

所以我的代码现在看起来像这样:

(TaskCard.ktConstants.kt不变)

MainScreen.kt

package com.pochopsp.dailytasks.presentation.screen

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.pochopsp.dailytasks.R
import com.pochopsp.dailytasks.data.database.TaskEvent
import com.pochopsp.dailytasks.data.database.TaskState
import com.pochopsp.dailytasks.presentation.AddTaskDialog
import com.pochopsp.dailytasks.presentation.common.SwipeToDeleteContainer
import com.pochopsp.dailytasks.presentation.tasks.TaskCard

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
    state: TaskState,
    onEvent: (TaskEvent) -> Unit
) {

    Scaffold(
        modifier = Modifier,
        content = { paddingValues ->

            if(state.isAddingTask){
                AddTaskDialog(state = state, onEvent = onEvent)
            }

            Column(
                modifier = Modifier.padding(start = 30.dp, end = 30.dp,
                    bottom = paddingValues.calculateBottomPadding(),
                    top = paddingValues.calculateTopPadding())
            ) {
                Row (
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.weight(1f)
                ){
                    Text(
                        text = stringResource(R.string.done_tasks_count,
                            state.tasks.filter { t -> t.done }.size, state.tasks.size),
                        fontWeight = FontWeight.SemiBold,
                        modifier = Modifier.weight(2f)
                    )
                    Spacer(modifier = Modifier.weight(1.5f))
                    ElevatedButton(
                        elevation = ButtonDefaults.buttonElevation(
                            defaultElevation = 2.dp
                        ),
                        shape = RoundedCornerShape(8.dp),
                        contentPadding = PaddingValues(0.dp),
                        modifier = Modifier
                            .weight(0.9f)
                            .defaultMinSize(minWidth = 1.dp, minHeight = 30.dp),
                        onClick = { /*TODO*/ }
                    ) {
                        Icon(
                            Icons.Filled.FilterAlt,
                            contentDescription = "Localized description",
                        )
                    }
                    Spacer(modifier = Modifier.weight(0.5f))
                    ElevatedButton(
                        elevation = ButtonDefaults.buttonElevation(
                            defaultElevation = 2.dp
                        ),
                        shape = RoundedCornerShape(8.dp),
                        contentPadding = PaddingValues(0.dp),
                        modifier = Modifier
                            .weight(0.9f)
                            .defaultMinSize(minWidth = 1.dp, minHeight = 30.dp),
                        onClick = { /*TODO*/ }
                    ) {
                        Icon(
                            Icons.AutoMirrored.Filled.Sort,
                            contentDescription = "Localized description",
                        )
                    }
                }
                Spacer(modifier = Modifier.weight(0.2f))
                LazyColumn(
                    contentPadding = PaddingValues(bottom = 10.dp, top = 10.dp),
                    verticalArrangement = Arrangement.spacedBy(32.dp),
                    modifier = Modifier.weight(10f)
                ) {
                    items(
                        items = state.tasks,
                        key = { it.id }
                    ) { task ->
                        Box(modifier = Modifier.animateItemPlacement()){
                            SwipeToDeleteContainer(
                                item = task,
                                onDelete = {
                                    onEvent(TaskEvent.DeleteTask(task.id))
                                }
                            ) {
                                TaskCard(
                                    taskCardDto = task,
                                    onCheckedChange = { id, done -> onEvent(TaskEvent.SetDone(id, done)) }
                                )
                            }
                        }
                    }
                }
            }
        },
        bottomBar = {
            BottomAppBar(
                modifier = Modifier.graphicsLayer { shadowElevation = 80f },
                containerColor = Color(0xFFFFFFFF),
                contentColor = Color(0xFFA0A0A0),
                actions = {
                    IconButton(onClick = { /* do something */ }) {
                        Icon(
                            Icons.Outlined.Settings,
                            contentDescription = "Localized description"
                        )
                    }
                    IconButton(onClick = { /* do something */ }) {
                        Icon(
                            Icons.Outlined.Search,
                            contentDescription = "Localized description",
                        )
                    }
                },
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = {
                            onEvent(TaskEvent.ShowDialog)
                        },
                        containerColor = Color(0xFF2984BA),
                        contentColor = Color(0xFFFFFFFF),
                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
                    ) {
                        Icon(Icons.Filled.Add, "Localized description")
                    }
                }
            )
        }
    )
}

SwipeToDeleteContainer.kt

package com.pochopsp.dailytasks.presentation.common

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissState
import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.pochopsp.dailytasks.presentation.theme.Constants
import kotlinx.coroutines.delay

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> SwipeToDeleteContainer(
    item: T,
    onDelete: (T) -> Unit,
    animationDuration: Int = 500,
    content: @Composable (T) -> Unit
) {
    var isRemoved by remember {
        mutableStateOf(false)
    }
    val state = rememberDismissState(
        confirmValueChange = { value ->
            if (value == DismissValue.DismissedToStart) {
                isRemoved = true
                true
            } else {
                false
            }
        }
    )

    LaunchedEffect(key1 = isRemoved) {
        if(isRemoved) {
            onDelete(item)
        }
    }

    SwipeToDismiss(
        state = state,
        background = {
            DeleteBackground(swipeDismissState = state)
        },
        dismissContent = { content(item) },
        directions = setOf(DismissDirection.EndToStart)
    )
}



@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DeleteBackground(
    swipeDismissState: DismissState
) {
    val color = if (swipeDismissState.dismissDirection == DismissDirection.EndToStart) {
        Color.Red
    } else Color.Transparent

    Box(
        modifier = Modifier
            .clip(Constants.cardShape)
            .background(color)
            .padding(16.dp)
            .fillMaxSize(),
        contentAlignment = Alignment.CenterEnd
    ) {
        Icon(
            imageVector = Icons.Default.Delete,
            contentDescription = null,
            tint = Color.White
        )
    }
}

下面是GIF中的固定动画:

Android相关问答推荐

Android添加设置图标齿轮到应用程序信息

ENV变量在gradle进程中没有更新

Android可组合继承?

Kotlin DSL:为什么我可以从Play Store获取发布版本的日志(log)?

我如何剪裁一个可由另一个合成的

在Jetpack Compose中将导航绘图显示在顶部栏下方、底部栏上方

插入视图模型时,dagger 未命中绑定错误

我们可以使用KSP读取类中变量的值吗?

从片段导航回来

可组合函数无限地从视图模型获取值

retrofit2.HttpException: HTTP 401

当我想使用例如 material3 时,为什么我需要添加对 material 的依赖?底部导航?

如何在 Jetpack Compose 中对齐按钮底部中心?

设置背景图片组成Column

获取 ArithmeticException:除以零,但我没有在任何地方除以零

组成不重叠的元素

找不到(包名称).在以下位置搜索:

LazyColumn 单选中的状态提升. Jetpack compose

Jetpack Compose:SpanStyle 的 TextAlign(垂直居中)

如何将设备屏幕位置转换为发送事件位置?