Przesuń palcem, aby zamknąć lub zaktualizować

Komponent SwipeToDismissBox umożliwia użytkownikowi odrzucenie lub zaktualizowanie elementu przez przesunięcie go w lewo lub w prawo.

Interfejs API

Użyj funkcji kompozycyjnej SwipeToDismissBox, aby zaimplementować działania wywoływane gestami przesuwania. Najważniejsze parametry:

  • state: stan SwipeToDismissBoxState utworzony do przechowywania wartości wygenerowanej przez obliczenia dotyczące elementu przesunięcia, który po wygenerowaniu wywołuje zdarzenia.
  • backgroundContent: dostosowywany element kompozycyjny wyświetlany za treścią elementu, który jest widoczny po przesunięciu treści.

Podstawowy przykład: aktualizowanie lub odrzucanie po przesunięciu palcem

Fragmenty kodu w tym przykładzie pokazują implementację przesunięcia, która aktualizuje element po przesunięciu od początku do końca lub odrzuca element po przesunięciu 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(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.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(
                        painter = rememberVectorPainter(AppIcons.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 wywołanie zwrotne confirmValueChange. Treść wywołania zwrotnego obsługuje różne możliwe działania. Wywołanie zwrotne zwraca wartość logiczną, która informuje komponent, czy powinien wyświetlić animację zamknięcia. W takim przypadku:
    • Jeśli element zostanie przesunięty od początku do końca, wywołuje funkcję lambda onToggleDone, przekazując bieżący element todoItem. Odpowiada to aktualizacji zadania.
    • Jeśli element zostanie przesunięty od końca do początku, wywołuje funkcję lambda onRemove, przekazując bieżący element todoItem. Odpowiada to usunięciu elementu listy zadań.
    • it != StartToEnd: ten wiersz zwraca wartość true, jeśli kierunek przesunięcia nie jest StartToEnd, a w przeciwnym razie zwraca wartość false. Zwrócenie wartości false zapobiega natychmiastowemu zniknięciu elementu SwipeToDismissBox po przesunięciu palcem w celu oznaczenia jako gotowe, co umożliwia wyświetlenie potwierdzenia wizualnego lub animacji.
  • SwipeToDismissBox umożliwia przesuwanie każdego elementu w poziomie. W stanie spoczynku pokazuje wewnętrzną zawartość komponentu, ale gdy użytkownik zacznie przesuwać palcem, zawartość zostanie odsunięta i pojawi się backgroundContent. Zarówno zwykłe treści, jak i backgroundContent są renderowane z uwzględnieniem wszystkich ograniczeń kontenera nadrzędnego. Element content jest rysowany nad elementem backgroundContent. W takim przypadku:
    • backgroundContent jest zaimplementowany jako Icon z kolorem tła opartym na SwipeToDismissBoxValue:
    • Blue podczas przesuwania StartToEnd – przełączanie zadania do wykonania.
    • Red podczas przesuwania EndToStart – usuwanie zadania.
    • W tle nie jest wyświetlana żadna treść dla Settled – gdy element nie jest przesuwany, w tle nie jest wyświetlana żadna treść.
    • Podobnie wyświetlany Icon dostosowuje się do kierunku przesunięcia:
    • StartToEnd wyświetla ikonę CheckBox, gdy zadanie jest wykonane, i ikonę CheckBoxOutlineBlank, gdy nie jest wykonane.
    • EndToStart wyświetla ikonę Delete.

@Composable
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 listę obserwowalną, która może zawierać obiekty TodoItem. Gdy element zostanie dodany do tej listy lub z niej usunięty, Compose ponownie skomponuje te części interfejsu, które od niej zależą.
    • W metodzie mutableStateListOf() inicjowane są 4 obiekty TodoItem z odpowiednimi opisami: „Pay bills”, „Buy groceries”, „Go to gym” i „Get dinner”.
  • LazyColumn wyświetla przewijaną w pionie listę todoItems.
  • onToggleDone = { todoItem -> ... } to funkcja wywołania zwrotnego wywoływana z TodoListItem, gdy użytkownik oznaczy obiekt jako gotowy. Aktualizuje właściwość isItemDone elementu todoItem. Ponieważ todoItems jest mutableStateListOf, ta zmiana powoduje rekompozycję, która aktualizuje interfejs.
  • onRemove = { todoItem -> ... } to funkcja wywołania zwrotnego, która jest aktywowana, gdy użytkownik usunie element. Usuwa określony element todoItem z listy todoItems. Spowoduje to też rekompozycję, a element zostanie usunięty z wyświetlanej listy.
  • Do każdego elementu TodoListItem stosowany jest modyfikator animateItem, dzięki czemu po odrzuceniu elementu wywoływana jest funkcja placementSpec modyfikatora. Spowoduje to animację usunięcia elementu oraz zmiany kolejności innych elementów na liście.

Wynik

Poniższy film pokazuje podstawową funkcję przesuwania w celu zamknięcia z poprzednich fragmentów kodu:

Rysunek 1. Podstawowa implementacja funkcji przesuwania w celu odrzucenia, która może zarówno oznaczyć element jako ukończony, jak i wyświetlić animację odrzucenia elementu na liście.

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

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

Poniższe fragmenty kodu pokazują, jak uwzględnić 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(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.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(
                        painter = rememberVectorPainter(AppIcons.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 obszarze roboczym za zawartością funkcji kompozycyjnej Icon.
    • drawRect() rysuje prostokąt na obszarze roboczym i wypełnia cały zakres rysowania określonym elementem Color.
  • Podczas przesuwania kolor tła elementu płynnie się zmienia, używając lerp.
    • W przypadku przesunięcia od StartToEnd kolor tła stopniowo zmienia się z jasnoszarego na niebieski.
    • W przypadku przesunięcia od EndToStart kolor tła stopniowo zmienia się z jasnoszarego na czerwony.
    • Stopień przejścia z jednego koloru do drugiego jest określany przez swipeToDismissBoxState.progress.
  • OutlinedCard dodaje subtelne wizualne rozdzielenie elementów 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

  • Najważniejsze informacje o tym kodzie znajdziesz w sekcji Najważniejsze informacje w poprzednim rozdziale, w którym opisano identyczny fragment kodu.

Wynik

Ten film przedstawia zaawansowane funkcje z animowanym kolorem tła:

Rysunek 2. Implementacja przesuwania w celu wyświetlenia lub usunięcia z animowanymi kolorami tła i dłuższym progiem przed zarejestrowaniem działania.

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

Dodatkowe materiały