我为惰性列构建了一个滑动解除框,其中包含一个确认对话框。 这是为了确保我们不会意外地从列表中删除项目,因此用户需要确认。
快乐的情况正在工作(用户滑动/用户确认),但是当我按下取消时,我正在重置状态(使用
SwipeToDismissBoxState.reset()
)。
除了我无法再次滑动该项目之外,一切似乎都已重置(甚至该项目正在动画返回)。 当项目移出视线(例如由于滚动)并重新进入屏幕时,它似乎根本没有重置。
我正在重置
SwipeToDismissBoxState.reset()
,我不知道我做错了什么......
Compose-多平台版本:1.6.11
这里是代码片段,一旦它起作用了,请随意重复使用它!
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> SwipeToDismissContainer(
item: T,
itemName: String,
animationTime: Int = 300,
confirmationDialog: Boolean = true,
onDismiss: (T) -> Unit,
content: @Composable (T) -> Unit
) {
var potentialDelete by remember { mutableStateOf(false) }
var deleteItem by remember { mutableStateOf(false) }
var resetItem by remember { mutableStateOf(false) }
val state = rememberSwipeToDismissBoxState(
confirmValueChange = { dismissValue ->
when (dismissValue) {
SwipeToDismissBoxValue.EndToStart,
SwipeToDismissBoxValue.StartToEnd -> {
if (confirmationDialog) {
potentialDelete = true
true
} else {
potentialDelete = false
deleteItem = true
true
}
}
else -> false
}
}
)
LaunchedEffect(resetItem) {
if (resetItem) {
state.reset()
resetItem = false
}
}
LaunchedEffect(deleteItem) {
if (deleteItem) {
delay(animationTime.toLong())
onDismiss(item)
}
}
AnimatedVisibility(
visible = !deleteItem,
exit = shrinkVertically(
animationSpec = tween(durationMillis = animationTime),
shrinkTowards = Alignment.Top
) + fadeOut()
) {
SwipeToDismissBox(
state = state,
backgroundContent = {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.errorContainer)
) {
Icon(
modifier = Modifier.minimumInteractiveComponentSize(),
imageVector = Icons.Outlined.Delete, contentDescription = null
)
}
},
content = {
content(item)
}
)
}
if (confirmationDialog && potentialDelete) {
SwipeDismissConfirmationDialog(
itemName = itemName,
onCancel = {
potentialDelete = false
resetItem = true
}
) {
potentialDelete = false
deleteItem = true
}
}
}
@Composable
private fun SwipeDismissConfirmationDialog(
itemName: String,
onCancel: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
icon = { Icon(Icons.Default.Delete, contentDescription = null) },
title = { Text(text = stringResource(Res.string.item_deletion_dialog_title)) },
text = {
Text(
text = stringResource(
Res.string.item_deletion_dialog_dialog_description,
itemName
)
)
},
onDismissRequest = {
onCancel()
},
confirmButton = {
TextButton(
onClick = {
onConfirm()
}
) {
Text(text = stringResource(Res.string.item_deletion_dialog_dialog_confirm))
}
},
dismissButton = {
TextButton(
onClick = {
onCancel()
}
) {
Text(text = stringResource(Res.string.item_deletion_dialog_dialog_cancel))
}
}
)
}
据我所知,这要么是有意为之,要么是 M3 库中的错误。并不理想,但这是我正在使用的一个不错的解决方法。我还没有在 Multiplat 上测试过它,但它适用于 Android 原生。
基本思想是永远不确认值更改,然后保持滑动状态,就好像您已确认更改一样。我为那些可能想根据自己的需要使用代码的人添加了预览。
@Composable
fun <T> SwipeToDismissContainer(
item: T,
itemName: String,
animationTime: Int = 300,
confirmationDialog: Boolean = true,
onDismiss: (T, onError: () -> Unit) -> Unit,
content: @Composable (T) -> Unit
) {
var potentialDelete by remember { mutableStateOf(false) }
var deleteItem by remember { mutableStateOf(false) }
var stateToMaintain by remember { mutableStateOf<SwipeToDismissBoxValue?>(null) }
val state = rememberSwipeToDismissBoxState(
confirmValueChange = { dismissValue ->
when (dismissValue) {
SwipeToDismissBoxValue.EndToStart,
SwipeToDismissBoxValue.StartToEnd -> {
if (confirmationDialog) {
potentialDelete = true
} else {
potentialDelete = false
deleteItem = true
}
stateToMaintain = dismissValue
}
else -> {}
}
false //Immediately resets the state so we can swipe it again if confirmation is canceled or if deletion fails
}
)
//Maintains the row's swiped state while it waits for confirmation and for AnimatedVisibility to hide the item
LaunchedEffect(stateToMaintain) {
stateToMaintain?.let {
state.snapTo(it)
stateToMaintain = null
}
}
LaunchedEffect(deleteItem) {
if (deleteItem) {
delay(animationTime.toLong())
onDismiss(item) {
deleteItem = false
}
} else {
//In our app, the onDismiss function also takes in an onError: () -> Unit,
//which allows us to bring the item back if deletion fails
state.reset()
}
}
AnimatedVisibility(
visible = !deleteItem,
exit = shrinkVertically(
animationSpec = tween(durationMillis = animationTime),
shrinkTowards = Alignment.Top
) + fadeOut()
) {
SwipeToDismissBox(
state = state,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.errorContainer)
)
},
content = {
content(item)
}
)
}
val scope = rememberCoroutineScope()
if (confirmationDialog && potentialDelete) {
SwipeDismissConfirmationDialog(
itemName = itemName,
onCancel = {
potentialDelete = false
scope.launch { state.reset() } //reset() seems to only reset the visual state, not the full state object
}
) {
potentialDelete = false
deleteItem = true
}
}
}
@Composable
private fun SwipeDismissConfirmationDialog(
itemName: String,
onCancel: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
title = { Text(text = "Delete Confirmation") },
text = { Text("Delete $itemName?") },
onDismissRequest = onCancel,
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = "Delete")
}
},
dismissButton = {
TextButton(onClick = onCancel) {
Text(text = "Cancel")
}
}
)
}
@Preview(showBackground = true)
@Composable
private fun Test() {
val list = (1..100).toList()
val scope = rememberCoroutineScope()
Column {
list.forEach {
SwipeToDismissContainer(
it,
it.toString(),
onDismiss = { _, onError ->
scope.launch {
delay(1000)
onError()
}
}
) { item ->
Text(
text = item.toString(),
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
)
}
}
}
}