Проведите пальцем, чтобы закрыть или обновить

Компонент SwipeToDismissBox позволяет пользователю закрыть или обновить элемент, проведя по нему пальцем влево или вправо.

API поверхность

Используйте SwipeToDismissBox composable для реализации действий, которые запускаются жестами смахивания. Ключевые параметры включают:

  • state : Состояние SwipeToDismissBoxState , созданное для хранения значения, полученного в результате вычислений для элемента смахивания, которое запускает события при создании.
  • backgroundContent : настраиваемый компонуемый элемент, отображаемый позади содержимого элемента, который становится виден при пролистывании содержимого.

Простой пример: обновление или удаление при смахивании

Фрагменты в этом примере демонстрируют реализацию смахивания, которая либо обновляет элемент при смахивании от начала до конца, либо закрывает элемент при смахивании от конца до начала.

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

Ключевые моменты кодекса

  • swipeToDismissBoxState управляет состоянием компонента. Он запускает обратный вызов confirmValueChange после завершения взаимодействия с элементом. Тело обратного вызова обрабатывает различные возможные действия. Обратный вызов возвращает логическое значение, которое сообщает компоненту, следует ли отображать анимацию отклонения. В этом случае:
    • Если элемент пролистывается от начала до конца, он вызывает лямбду onToggleDone , передавая текущий todoItem . Это соответствует обновлению элемента todo.
    • Если элемент смахивается с конца в начало, вызывается лямбда-функция onRemove , передающая текущий todoItem . Это соответствует удалению элемента todo.
    • it != StartToEnd : Эта строка возвращает true если направление смахивания не StartToEnd , и false в противном случае. Возвращение false предотвращает немедленное исчезновение SwipeToDismissBox после смахивания «переключение выполнено», позволяя визуальное подтверждение или анимацию.
  • SwipeToDismissBox позволяет горизонтальное взаимодействие смахивания для каждого элемента. В состоянии покоя он показывает внутреннее содержимое компонента, но когда пользователь начинает смахивание, содержимое перемещается и появляется backgroundContent . И обычное содержимое, и backgroundContent получают полные ограничения родительского контейнера для рендеринга. content рисуется поверх backgroundContent . В этом случае:
    • backgroundContent реализован как Icon с фоновым цветом на основе SwipeToDismissBoxValue :
    • Blue при смахивании StartToEnd — переключение между задачами.
    • Red при смахивании EndToStart — удаление элемента списка дел.
    • Для Settled ничего не отображается на заднем плане — когда элемент не перемещается, на заднем плане ничего не отображается.
    • Аналогично, отображаемый Icon адаптируется к направлению проведения пальцем:
    • StartToEnd отображает значок CheckBox , когда задача выполнена, и значок CheckBoxOutlineBlank , когда задача не выполнена.
    • EndToStart отображает значок 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()
            )
        }
    }
}

Ключевые моменты кодекса

  • mutableStateListOf(...) создает наблюдаемый список, который может содержать объекты TodoItem . Когда элемент добавляется или удаляется из этого списка, Compose перекомпоновывает части пользовательского интерфейса, которые зависят от него.
    • Внутри mutableStateListOf() инициализируются четыре объекта TodoItem с соответствующими описаниями: «Оплатить счета», «Купить продукты», «Сходить в спортзал» и «Приготовить ужин».
  • LazyColumn отображает вертикально прокручиваемый список todoItems .
  • onToggleDone = { todoItem -> ... } — это функция обратного вызова, вызываемая из TodoListItem , когда пользователь отмечает объект как выполненный. Она обновляет свойство isItemDone для todoItem . Поскольку todoItems — это mutableStateListOf , это изменение запускает перекомпозицию, обновляя пользовательский интерфейс.
  • onRemove = { todoItem -> ... } — это функция обратного вызова, которая активируется, когда пользователь удаляет элемент. Она удаляет конкретный todoItem из списка todoItems . Это также вызывает перекомпозицию, и элемент будет удален из отображаемого списка.
  • Модификатор animateItem применяется к каждому TodoListItem , так что placementSpec модификатора вызывается, когда элемент был отклонен. Это анимирует удаление элемента, а также переупорядочивание других элементов в списке.

Результат

В следующем видео демонстрируется базовая функциональность смахивания для закрытия из предыдущих фрагментов:

Рисунок 1. Базовая реализация функции смахивания для закрытия, которая может как пометить элемент как завершенный, так и показать анимацию закрытия для элемента в списке.

Полный пример кода смотрите в исходном файле GitHub .

Расширенный пример: анимация цвета фона при смахивании

В следующих фрагментах показано, как использовать позиционный порог для анимации цвета фона элемента при проведении пальцем.

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

Ключевые моменты кодекса

  • drawBehind рисует непосредственно на холсте за содержимым компонуемого Icon .
    • drawRect() рисует прямоугольник на холсте и заполняет все границы области рисования указанным Color .
  • При проведении пальцем цвет фона элемента плавно меняется с помощью lerp .
    • При проведении пальцем от StartToEnd цвет фона постепенно меняется со светло-серого на синий.
    • При проведении пальцем от EndToStart цвет фона постепенно меняется со светло-серого на красный.
    • Величина перехода от одного цвета к другому определяется параметром swipeToDismissBoxState.progress .
  • OutlinedCard добавляет тонкое визуальное разделение между элементами списка.

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

Ключевые моменты кодекса

  • Основные положения этого кода см. в разделе «Основные положения» предыдущего раздела, в котором описывается идентичный фрагмент кода.

Результат

В следующем видео показаны расширенные функциональные возможности с анимированным фоновым цветом:

Рисунок 2. Реализация смахивания для отображения или удаления с анимированными цветами фона и более длительным порогом регистрации действия.

Полный пример кода смотрите в исходном файле GitHub .

Дополнительные ресурсы