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

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

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

Используйте composable-функцию 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(
                        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. Это соответствует обновлению задачи.
    • Если провести пальцем по элементу от конца к началу, будет вызвана лямбда-функция 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
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 рисует непосредственно на холсте за контентом 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.

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