简体   繁体   中英

LazyColumn with SwipeToDismiss

What is the correct way to use SwipeToDismiss and LazyColumn in android compose alpha09 ?

My approach:

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

There a is problem when the LazyColumn is recomposed the item on the deleted position is Dismissed - not visible. I hacked it with dismissState.snapTo(DismissValue.Default) . But for split of a second you can see the old item visible. If I do not use remember but DismissState I get: java.lang.IllegalArgumentException: Cannot round NaN value. caused by androidx.compose.material.SwipeToDismissKt$SwipeToDismiss$2$1$1$1.invoke-nOcc-ac(SwipeToDismiss.kt:244)

Here you can find an example of how to use LazyColumn with 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

modified from 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)

The main problem in the original is that the dismiss-state is remembered by the item's position. When the list changes (which is quite obvious when deleting an item), the remembered dismissState will then apply to the next item (which is wrong of course). To remedy this use items(items, {listItem: MyRoutesViewModel.ListItem -> listItem.id} ) instead of just items(items)

Try to pass the key inside lazy column. Then rememberDismissState will work according to the item id instead of the list position.

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

                    }
                }

Previous answers have mentioned passing a key factory to a LazyColumn in order to tie the state of a list item to a unique identifier rather than its position in the list. If for whatever reason you can't use a LazyColumn , you can still use the key utility like this:

for (item in items) {
  key(item.id) {
    ... // use item
  }
}

Solution from: https://stackoverflow.com/a/70191854/8124931

There is this awesome official Android doc about SwipeToDismiss Here

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM