我正在使用 Jetpack Compose 实现一个简单的 TODO 应用程序。我有以下问题: 当我尝试通过从右向左滑动来从 LazyColumn 中删除元素(卡片)时,动画看起来不太好。当它结束时,几乎看起来被删除的卡片下面的卡片跳到了被删除的卡片留下的空白处,看GIF更好地理解:
我希望动画能够“顺利”结束,而删除的卡片下面的卡片不会跳起来。 我怀疑这个问题可能与我在 LazyColumn 上设置的
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")
}
}
)
}
)
}
任务卡.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
)
}
}
常量.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
在
Spacer
中手动添加间距:
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)
)
Spacer(modifier = Modifier.height(32.dp))
}
不要忘记从
verticalArrangement
中删除 LazyColumn
属性。
请报告是否有效。