Как закрыть или обновить уведомление

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

API surface

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

  • state – состояние SwipeToDismissBoxState, созданное для хранения значения, полученного в результате вычислений для элемента, который можно смахнуть. При получении значения запускаются события.
  • backgroundContent – настраиваемая composable-функция, которая показывается за контентом элемента, когда пользователь проводит по нему пальцем.

Простой пример: обновление или закрытие по свайпу

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

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

Важные сведения о коде

  • swipeToDismissBoxState управляет состоянием компонента. После завершения взаимодействия с объектом запускается обратный вызов confirmValueChange. Тело обратного вызова обрабатывает различные возможные действия. Функция обратного вызова возвращает логическое значение, которое сообщает компоненту, нужно ли показывать анимацию закрытия. В этом случае:
    • Если элемент смахнуть от начала до конца, вызывается лямбда-выражение onToggleDone, передающее текущее значение todoItem. Это соответствует обновлению элемента списка дел.
    • Если элемент смахнуть справа налево, вызывается лямбда-выражение onRemove, которому передается текущий элемент todoItem. Это соответствует удалению задачи.
    • 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
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(
                        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.") }
            )
        }
    }
}

Важные сведения о коде

  • drawBehind рисует непосредственно на холсте за контентом composable-функции 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.

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