带有 SwipeToDismiss 的 LazyColumn
Posted
技术标签:
【中文标题】带有 SwipeToDismiss 的 LazyColumn【英文标题】:LazyColumn with SwipeToDismiss 【发布时间】:2021-03-30 13:19:54 【问题描述】:在android compose alpha09
中使用SwipeToDismiss
和LazyColumn
的正确方法是什么?
我的做法:
LazyColumn(
modifier = Modifier.padding(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
)
items(items = items)
TrackedActivityRecord(it.activity, it.record, scaffoldState)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TrackedActivityRecord(
activity: TrackedActivity,
record: TrackedActivityRecord,
scaffoldState: ScaffoldState,
vm: TimelineVM = viewModel()
)
val dismissState = rememberDismissState()
if (dismissState.value != DismissValue.Default)
LaunchedEffect(subject = activity)
val deleted = scaffoldState.snackbarHostState.showSnackbar("Awesome", "do it")
if (deleted == SnackbarResult.Dismissed)
vm.rep.deleteRecordById(activity.id, record.id)
dismissState.snapTo(DismissValue.Default)
SwipeToDismiss(
state = dismissState,
background =
Box(Modifier.size(20.dp). background(Color.Red))
,
)
Record(activity = activity, record = record)
LazyColumn
被重构时出现问题,删除位置上的项目是Dismissed
- 不可见。我用dismissState.snapTo(DismissValue.Default)
破解了它。但是在一秒钟内,您可以看到旧项目可见。如果我不使用记住但 DismissState 我得到:java.lang.IllegalArgumentException: Cannot round NaN value.
由androidx.compose.material.SwipeToDismissKt$SwipeToDismiss$2$1$1$1.invoke-nOcc-ac(SwipeToDismiss.kt:244)
引起
【问题讨论】:
我正在使用您想出的相同解决方法。我认为这是目前最好的解决方案,因为 compose 仍处于 alpha 阶段,无论如何这可能不会成为未来的问题。 太好了,我暂时保留它,谢谢。 遇到了同样的问题,感谢您的破解。您是否将此作为问题提交给 Google? 【参考方案1】:修改自https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss:
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.draw.scale
import androidx.compose.material.DismissValue.*
import androidx.compose.material.DismissDirection.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
// This is an example of a list of dismissible items, similar to what you would see in an
// email app. Swiping left reveals a 'delete' icon and swiping right reveals a 'done' icon.
// The background will start as grey, but once the dismiss threshold is reached, the colour
// will animate to red if you're swiping left or green if you're swiping right. When you let
// go, the item will animate out of the way if you're swiping left (like deleting an email) or
// back to its default position if you're swiping right (like marking an email as read/unread).
@ExperimentalMaterialApi
@Composable
fun MyContent(
items: List<ListItem>,
dismissed: (listItem: ListItem) -> Unit
)
val context = LocalContext.current
LazyColumn
items(items, listItem: ListItem -> listItem.id) item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(EndToStart))
dismissed(item)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 1.dp),
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = direction ->
FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
,
background =
val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue)
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
)
val alignment = when (direction)
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
val icon = when (direction)
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
)
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
,
dismissContent =
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
)
Text(item.text)
)
data class ListItem(val id:String, val text:String)
原来的主要问题是,dismiss-state 是由项目的位置记住的。当列表发生变化时(这在删除项目时非常明显),记住的 dismissState 将应用于下一个项目(这当然是错误的)。要解决此问题,请使用 items(items, listItem: MyRoutesViewModel.ListItem -> listItem.id ) 而不仅仅是 items(items)
【讨论】:
嗨,我遇到了和你描述的一样的问题,你说的完全有道理。我正在尝试做同样的事情,但我不知道如何做。我有一个列表: Listitems(list.size, index -> list[index].first ) index ->
。【参考方案2】:
尝试在惰性列中传递密钥。那么rememberDismissState会根据item id而不是list位置来工作。
LazyColumn(modifier = Modifier
.background(Background)
.padding(bottom = SpaceLarge + 20.dp),
state = bottomListScrollState
)
if (newsList.value.isNotEmpty())
items(
items = newsList.value,
// Apply the key like below
key = news -> news.url ,
itemContent = news ->
var isDeleted by remember mutableStateOf(false)
val dismissState = rememberDismissState(
confirmStateChange =
Timber.d("dismiss value $it.name")
if (it == DismissValue.DismissedToEnd) isDeleted =
!isDeleted
else if (it == DismissValue.DismissedToStart) isDeleted =
!isDeleted
it != DismissValue.DismissedToStart || it != DismissValue.DismissedToEnd
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 2.dp),
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds = direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
,
background =
val direction =
dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue)
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Red
DismissValue.DismissedToStart -> Color.Red
)
val alignment = when (direction)
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
val icon = when (direction)
DismissDirection.StartToEnd -> Icons.Default.Delete
DismissDirection.EndToStart -> Icons.Default.Delete
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
)
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
, dismissContent =
if (isDeleted)
viewModel.deleteNews(news)
Timber.d("Deleted $news.url")
snackbarController.getScope().launch
snackbarController.showSnackbar(
scaffoldState = scaffoldState,
message = "Article successfully Deleted",
actionLabel = "Undo"
)
viewModel.result = news
else
NewsColumnItem(news = news)
viewModel.result = news
actions.gotoNewsViewScreen(news.url.encode())
)
)
【讨论】:
这行得通,但是如果我不能使用 LazyList 怎么办?如果我有一个带有 for 循环的列来遍历项目怎么办?【参考方案3】:您可以在此处找到如何将 LazyColumn 与 SwipeToDismiss 一起使用的示例:
// This is an example of a list of dismissible items, similar to what you would see in an
// email app. Swiping left reveals a 'delete' icon and swiping right reveals a 'done' icon.
// The background will start as grey, but once the dismiss threshold is reached, the colour
// will animate to red if you're swiping left or green if you're swiping right. When you let
// go, the item will animate out of the way if you're swiping left (like deleting an email) or
// back to its default position if you're swiping right (like marking an email as read/unread).
LazyColumn
items(items) item ->
var unread by remember mutableStateOf(false)
val dismissState = rememberDismissState(
confirmStateChange =
if (it == DismissedToEnd) unread = !unread
it != DismissedToEnd
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = direction ->
FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
,
background =
val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue)
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
)
val alignment = when (direction)
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
val icon = when (direction)
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.75f else 1f
)
Box(
Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
contentAlignment = alignment
)
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
,
dismissContent =
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
)
ListItem(
text =
Text(item, fontWeight = if (unread) FontWeight.Bold else null)
,
secondaryText = Text("Swipe me left or right!")
)
)
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss
【讨论】:
我是唯一一个此示例仅适用于列表更改之前的人吗?我实现了这个解决方案,但是当我删除一个项目时,在它下面的所有其他项目上滑动就会停止工作。 @PrimožIvančič 我有一个类似(相同?)的问题。似乎整个可组合项与列表的位置相关,而不是与项目相关联。如果我关闭顶部的项目,下一个项目将取代它的位置,但仍处于已关闭状态。 是的@GustavKarlsson,这是同一个问题。仅供参考:我无法解决这个问题,所以我放弃了“滑动关闭”的想法并实现了一个按钮。【参考方案4】:以前的答案提到将密钥工厂传递给LazyColumn
,以便将列表项的状态与唯一标识符而非其在列表中的位置联系起来。如果由于某种原因您不能使用LazyColumn
,您仍然可以像这样使用key
实用程序:
for (item in items)
key(item.id)
... // use item
解决方案来自:https://***.com/a/70191854/8124931
【讨论】:
以上是关于带有 SwipeToDismiss 的 LazyColumn的主要内容,如果未能解决你的问题,请参考以下文章
带有多个链接的 NSAttributedString 的 UILabel,带有行限制,显示尾部截断,带有未见文本的 NSBackgroundColorAttributeName
使用带有 uuencode 的“sendmail”发送邮件,并带有主题
带有 RecyclerView 的 DialogFragment 比带有 Recyclerview 的 Fragment 慢