带有 SwipeToDismiss 的 LazyColumn

Jan*_*elý 8 android android-jetpack android-jetpack-compose

在 android compose 中使用SwipeToDismiss和使用的正确方法是什么?LazyColumnalpha09

我的做法:

LazyColumn(
    modifier = Modifier.padding(6.dp),
    verticalArrangement = Arrangement.spacedBy(6.dp),
) {
    items(items = items) {
        TrackedActivityRecord(it.activity, it.record, scaffoldState)
    }
}

Run Code Online (Sandbox Code Playgroud)
@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)
    }
}
Run Code Online (Sandbox Code Playgroud)

LazyColumn重新组合删除位置上的项目时会出现问题Dismissed- 不可见。我用dismissState.snapTo(DismissValue.Default). 但是一瞬间,您可以看到旧项目可见。如果我不使用 remember 但 DismissState 我得到:java.lang.IllegalArgumentException: Cannot round NaN value.androidx.compose.material.SwipeToDismissKt$SwipeToDismiss$2$1$1$1.invoke-nOcc-ac(SwipeToDismiss.kt:244)

Han*_*ans 16

修改自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)
Run Code Online (Sandbox Code Playgroud)

原版中的主要问题是解除状态是通过项目的位置来记住的。当列表发生变化时(这在删除项目时非常明显),记住的dismissState将应用于下一个项目(这当然是错误的)。要解决此问题,请使用 items(items, {listItem: MyRoutesViewModel.ListItem -> listItem.id} ) 而不是仅仅 items(items)


Ste*_*fen 9

您可以在此处找到如何将 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!") }
                    )
                }
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss


dev*_*per 7

尝试在惰性列中传递密钥。然后rememberDismissState将根据项目id而不是列表位置来工作。

 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())
                                            }
                                        }
                                    }
                                )
                            })

                    }
                }

Run Code Online (Sandbox Code Playgroud)