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