Przesuń palcem, aby zamknąć lub zaktualizować

Komponent SwipeToDismissBox pozwala użytkownikowi odrzucić lub zaktualizować element, przesuwając go w lewo lub w prawo.

Interfejs API

Aby zaimplementować działania wywoływane przez gesty przesuwania, użyj komponentu SwipeToDismissBox. Kluczowe parametry:

  • state: stan SwipeToDismissBoxState utworzony w celu przechowywania wartości wygenerowanej przez obliczenia dotyczące elementu przesuwania, który uruchamia zdarzenia po wygenerowaniu.
  • backgroundContent: konfigurowalny element kompozycyjny wyświetlany za treścią, który ujawnia się po przesunięciu treści.

Przykład podstawowy: aktualizacja lub zamknięcie po przesunięciu

W tym przykładzie fragmenty kodu pokazują implementację przesuwania, która aktualizuje element, gdy przesuniesz go od początku do końca, lub zamyka go, gdy przesuniesz go od końca do początku.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItem(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Blue)
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Red)
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        ListItem(
            headlineContent = { Text(todoItem.itemDescription) },
            supportingContent = { Text("swipe me to update or remove.") }
        )
    }
}

Najważniejsze informacje o kodzie

  • swipeToDismissBoxState zarządza stanem komponentu. Po zakończeniu interakcji z elementem wywołuje on funkcję confirmValueChange. Treść wywołania zwrotnego obsługuje różne możliwe działania. Wywołanie zwrotne zwraca wartość logiczną, która informuje komponent, czy ma wyświetlić animację zamykania. W tym przypadku:
    • Jeśli element jest przesuwany od początku do końca, wywołuje funkcję onToggleDone lambda, przekazując bieżącą wartość todoItem. Odpowiada to aktualizowaniu elementu listy zadań.
    • Jeśli element jest przesuwany od końca do początku, wywołuje funkcję onRemove lambda, przekazując bieżącą wartość todoItem. Odpowiada to usunięciu zadania.
    • it != StartToEnd: ta linijka zwraca wartość true, jeśli kierunek przesunięcia nie jest StartToEnd, a w przeciwnym razie zwraca wartość false. Zwracanie false zapobiega natychmiastowemu zniknięciu SwipeToDismissBox po przesunięciu palcem, co umożliwia wizualne potwierdzenie lub animację.
  • SwipeToDismissBox umożliwia przesuwanie elementów poziomo. W przypadku Pinteresta pokazuje ona wewnętrzną zawartość komponentu, ale gdy użytkownik zacznie przesuwać palcem, treść zostanie usunięta i pojawi się backgroundContent. Zarówno normalna zawartość, jak i backgroundContent mają pełne ograniczenia kontenera nadrzędnego, w którym są renderowane. content jest narysowana na wierzchu backgroundContent. W tym przypadku:
    • backgroundContent jest implementowany jako Icon z kolorem tła opartym na SwipeToDismissBoxValue:
    • Blue podczas przesuwania StartToEnd – włączanie i wyłączanie elementu listy zadań.
    • Red podczas przesuwania EndToStart – usuwanie zadania.
    • W przypadku Settled tło nie jest wyświetlane – gdy element nie jest przesuwany, tło nie jest wyświetlane.
    • Podobnie wyświetlana Icon dostosowuje się do kierunku przesuwania:
    • StartToEnd wyświetla ikonę CheckBox, gdy zadanie jest wykonane, i ikonę CheckBoxOutlineBlank, gdy nie jest wykonane.
    • EndToStart wyświetla ikonę Delete.

@Composable
private fun SwipeItemExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItem(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

Najważniejsze informacje o kodzie

  • mutableStateListOf(...) tworzy obserwowalną listę, która może zawierać obiekty TodoItem. Gdy element zostanie dodany do tej listy lub z niej usunięty, Compose ponownie utworzy te części interfejsu, które na nim polegają.
    • Wewnątrz mutableStateListOf() inicjowane są 4 obiekty TodoItem z odpowiednimi opisami: „Opłacanie rachunków”, „Zakupy spożywcze”, „Chodzenie na siłownię” i „Kolacja”.
  • LazyColumn wyświetla listę todoItems przewijaną w pionie.
  • onToggleDone = { todoItem -> ... } to funkcja wywołania zwrotnego wywoływana z poziomu TodoListItem, gdy użytkownik zaznaczy obiekt jako gotowy. Zmień właściwość isItemDone w komponencie todoItem. Ponieważ todoItems jest mutableStateListOf, ta zmiana powoduje ponowne skompilowanie i zaktualizowanie interfejsu.
  • onRemove = { todoItem -> ... } to funkcja wywołania zwrotnego, która jest wywoływana, gdy użytkownik usunie element. Usuwa określony element todoItem z listy todoItems. Spowoduje to również ponowne złożenie listy, a element zostanie usunięty z wyświetlanej listy.
  • Modyfikator animateItem jest stosowany do każdego elementu TodoListItem, aby wywołać funkcję placementSpec modyfikatora po odrzuceniu elementu. W tym celu animujesz usunięcie elementu oraz zmianę kolejności innych elementów na liście.

Wynik

Ten film pokazuje podstawową funkcję przesuwania, aby zamknąć, z poprzednich fragmentów kodu:

Rysunek 1. Podstawowe wykonanie funkcji przesunięcia w bok, która może oznaczać ukończenie elementu i wyświetlić animację usunięcia elementu z listy.

Pełny przykładowy kod znajdziesz w pliku źródłowym na GitHubie.

Przykład zaawansowany: animacja koloru tła podczas przesuwania

Poniższe fragmenty kodu pokazują, jak zastosować próg pozycji, aby animować kolor tła elementu podczas przesuwania.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItemWithAnimation(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .drawBehind {
                                drawRect(lerp(Color.LightGray, Color.Blue, swipeToDismissBoxState.progress))
                            }
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(lerp(Color.LightGray, Color.Red, swipeToDismissBoxState.progress))
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        OutlinedCard(shape = RectangleShape) {
            ListItem(
                headlineContent = { Text(todoItem.itemDescription) },
                supportingContent = { Text("swipe me to update or remove.") }
            )
        }
    }
}

Najważniejsze informacje o kodzie

  • drawBehind rysuje bezpośrednio na płótnie za treścią elementu Icon.
    • drawRect() rysuje prostokąt na płótnie i wypełnia cały zakres rysunku określonym Color.
  • Podczas przesuwania kolor tła elementu zmienia się płynnie za pomocą funkcji lerp.
    • W przypadku przesunięcia z poziomu StartToEnd kolor tła stopniowo zmienia się z jasnego szarego na niebieski.
    • W przypadku przesunięcia z EndToStart kolor tła stopniowo zmienia się z jasnego szarego na czerwony.
    • Wielkość przejścia z jednego koloru na następny określa parametr swipeToDismissBoxState.progress.
  • OutlinedCard dodaje subtelne wizualne oddzielenie między elementami listy.

@Composable
private fun SwipeItemWithAnimationExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItemWithAnimation(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

Najważniejsze informacje o kodzie

Wynik

Ten film pokazuje zaawansowane funkcje z animowanym tłem:

Rysunek 2. Przesuwanie w celu wyświetlenia lub usunięcia elementu z animowanymi kolorami tła i dłuższym progiem rejestrowania działania.

Pełny przykładowy kod znajdziesz w pliku źródłowym na GitHubie.

Dodatkowe materiały