Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)

like image 874
Jan Veselý Avatar asked Dec 20 '20 15:12

Jan Veselý


3 Answers

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)

like image 197
Hans Avatar answered Nov 16 '22 00:11

Hans


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

like image 7
Steffen Avatar answered Nov 16 '22 02:11

Steffen


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

                    }
                }

like image 6
developer Avatar answered Nov 16 '22 00:11

developer