Перетаскивание по опорным точкам

Последнее обновление: 19.04.2024

Кроме обычного возможности обычного перетаскивания Jetpack Compose также позволяет перетаскивать компоненты вдоль некоторой фиксированной линии, которая основана на двух или более опорных точках (anchor points). Каждая опорная точка имеет фиксированные положения на экране, соответственно вся ось перетаскивания также фиксирована.

Точка между двумя опорными точками называется порогом (threshold). Перетаскиваемый компонент вернется к начальной опорной точке, если перетаскивание закончится до достижения порогового значения. Если, с другой стороны, перетаскивание заканчивается после прохождения точки перехода, компонент продолжит движение, пока не достигнет следующей опорной точки.

Для использования перетаскивания по опорным точкам нужно добавить в проект библиотеку foundation. Для этого откроем файл libs.versions.toml и внесем в нем изменения в две секции:

[versions]
foundation = "1.6.4"

.........................

[libraries]
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }

Затем в файл build.gradle.kts(Module: app) добавим зависимость библиотеки foundation:

...............
dependencies {
    implementation(libs.androidx.foundation)
..........................

И для применения изменений нажмем на кнопку Sync Now

Основные элементы перетаскивания вдоль фиксированной оси

Для обработчки перетаскивания вдоль фиксированной оси к компоненту, для которого надо обработать перетаскивание, применяется модификатор anchoredDraggable():

@ExperimentalFoundationApi
fun <T : Any?> Modifier.anchoredDraggable(
    state: AnchoredDraggableState<T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = state.isAnimationRunning
): Modifier

Его параметры:

  • state: состояние типа AnchoredDraggableState<T>, которое хранит информацию о перемещении

  • orientation: направление перетаскивания

  • enabled: доступно ли перетаскивание

  • reverseDirection: в каком направлении идет перетаскивание

  • interactionSource: объект MutableInteractionSource, который передается во внутренний модификатор Modifier.draggable

  • startDragImmediately: если установлено значение true, то перетаскивание начнется немедленно. Предназначено для того, чтобы конечные пользователи могли "поймать" анимируемый компонент, нажав на него.

Перетаскиваемые опорные точки объявляются с помощью фабрики DraggableAnchors. Координаты этих точек определяются в пикселях в виде значений Float. Например, следующий код создает объект DraggableAnchors, который состоит из трех опорных точек, расположенных в начале, центре и конце пути перетаскивания:

enum class Anchors {
    Left,
    Center,
    Right
}

val anchors = DraggableAnchors {
    Anchors.Left at 0f
    Anchors.Center at widthPx / 2
    Anchors.Right at widthPx
}

Пороги объявляются как лямбда-выражения, которые возвращают пороговую позицию. При вызове лямбда-выражения в него передается значение, которое представляет расстояние между исходной и конечной опорными точками. Это расстояние можно использовать для расчета пороговой точки на пути перетаскивания. Например, следующий код объявляет порог в точке, составляющей 70% расстояния между двумя опорными точками:

{ distance: Float -> distance * 0.7f }

После объявления опорных точек и порога они используются для создания объекта AnchoredDraggableState, синтаксис которого следующий:

val state = remember {
    AnchoredDraggableState(
        initialValue = [позиция начальной опорной точки],
        anchors = DraggableAnchors {
            [определение всех опорных точек]
        },
        positionalThreshold = [вычисление порога],
        velocityThreshold = [пороговая скорость],
        animationSpec = [анимация]
    )
}

Конструктор AnchoredDraggableState принимает ряд параметров:

  • intialValue: начальная опорная точка перетаскиваемого элемента, в которой перетаскиваемый элемент появится при первом отображении

  • anchors: объект DraggableAnchors, который содержит опорные точки

  • positionalThreshold: лямбда вычисления порога

  • velocityThreshold: дополнительная настройка, определяющая скорость в dp в секунду, которую должна превысить скорость перетаскивания, чтобы перейти в следующее состояние

  • animationSpec: применяет эффекты анимации к операции перетаскивания

При перетаскивании позиция перемещаемого компонента автоматически не обновляется. Нам ее надо обновлять вручную. Для этого мы можем использовать модификатор offset(), который устанавливает координаты компонента. Для установки позиции в этот модификатор можно передать текущее смещения, которое можно получить из состояния AnchoredDraggableState с помощью вызова метода requireOffset(). Результатом будет текущая позиция по оси X или Y, в зависимости от того, является ли направоление перетаскивания горизонтальным или вертикальным. Например, при горизонтальном перетаскивании установка позиции могла бы выглядеть так:

Box(Modifier.offset { IntOffset( x = state .requireOffset().roundToInt(), y = 0) })

Пример перетаскивания

В качестве простейшего примера перетаскивания по опорным точкам рассмотрим следующее приложение:

package com.example.helloapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.unit.IntOffset
import kotlin.math.roundToInt

enum class Anchors {
    Start,
    Center,
    End
}

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val density = LocalDensity.current
            val parentBoxWidth = 320.dp
            val boxSize = 50.dp
            val widthPx = with(density) {(parentBoxWidth - boxSize).toPx() }
            val state = remember {
                AnchoredDraggableState(
                    initialValue = Anchors.Start,
                    anchors = DraggableAnchors {
                        Anchors.Start at 0f
                        Anchors.Center at widthPx / 2
                        Anchors.End at widthPx
                    },

                    positionalThreshold = { distance: Float -> distance * 0.5f },
                    velocityThreshold = { with(density) { 100.dp.toPx() } },
                    animationSpec = tween()
                )
            }
            Box(Modifier.padding(20.dp).width(parentBoxWidth)){
                Box(Modifier.width(parentBoxWidth).height(5.dp).background(Color.DarkGray).align(Alignment.CenterStart))
                Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.CenterStart))
                Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.Center))
                Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.CenterEnd))
                Box(
                    Modifier
                        .offset {
                            IntOffset(
                                x = state
                                    .requireOffset()
                                    .roundToInt(),
                                y = 0,
                            )
                        }
                        .anchoredDraggable(
                            state,
                            Orientation.Horizontal
                        )
                        .size(boxSize)
                        .background(Color.LightGray)
                )
            }
        }
    }
}

В итоге у нас получится ось с тремя точками, по которым мы сможем перемещать светло-серый компонент Box:

Перетаскивание по опорным точкам и модификатор anchoredDraggable в Jetpack Compose Kotlin Android

Итак, здесь мы определяем три опорных точки:

enum class Anchors {
    Start,
    Center,
    End
}

Сначала определяем несколько базовых переменных, которые будут применяться при рассчетах:

val density = LocalDensity.current
val parentBoxWidth = 320.dp
val boxSize = 50.dp
val widthPx = with(density) {(parentBoxWidth - boxSize).toPx() }

Переменная density представляет плотность текущего экрана и необходима для перевода из единиц dp в стандартные пиксели. Переменная parentBoxWidth хранит ширину контейнера, а также длину оси, по которой будет перемещаться компонент. Переменная boxSize хранит ширину и высоту компонента (ширина равна высоте). Наконец, ширина перетаскиваемой области в пикселях - переменная widthPx рассчитывается путем вычитания ширины перетаскиваемого компонента из ширины родительского контейнера.

Ширина перетаскиваемого компонента вычитается, чтобы учесть тот факт, что перетаскиваемый компонент будет центрирован по опорным точкам, оставляя отступ в половину ширины перетаскиваемого компонента на первой и последней опорных точках (эти две половины объединяются, и получается полная ширина перетаскиваемого компонента).

Далее с помошью вызова AnchoredDraggableState определяется состояния перетасиквания:

val state = remember {
    AnchoredDraggableState(
        initialValue = Anchors.Start,
        anchors = DraggableAnchors {
            Anchors.Start at 0f
            Anchors.Center at widthPx / 2
            Anchors.End at widthPx
        },
        positionalThreshold = { distance: Float -> distance * 0.5f },
        velocityThreshold = { with(density) { 100.dp.toPx() } },
        animationSpec = tween()
    )
}

В конструктор класса AnchoredDraggableState передается позиция начальной опорной точки, а также объект DraggableAnchors с тремя опорными точками. Через параметр positionalThreshold задаем порог, который расположен по середине между соседними опорными точками.

Стоит отметить, что на момент написания статьи класс AnchoredDraggableState представляет экспериментальную функциональность, поэтому к методу onCreate() в MainActivity применяется аннотация @ExperimentalFoundationApi:

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {

Компонент верхнего уровня, который содержит интерфейс, представляет компонент Box:

Box(Modifier.padding(20.dp).width(parentBoxWidth)){
    Box(Modifier.width(parentBoxWidth).height(5.dp).background(Color.DarkGray).align(Alignment.CenterStart))
    Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.CenterStart))
    Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.Center))
    Box(Modifier.size(10.dp).background(Color.DarkGray, CircleShape).align(Alignment.CenterEnd))

Этот Box имеет ширину, равную parentBoxWidth. Внутри этого компонента с помощью дополнительных компонентов Box отрисована ось перемещения с тремя точками.

Затем определяется компонент Box, который собственно будет перемещаться вдоль оси:

Box(
    Modifier
        .offset {
            IntOffset(
                x = state
                    .requireOffset()
                    .roundToInt(),
                y = 0,
            )
        }
        .anchoredDraggable(
            state,
            Orientation.Horizontal,
        )
        .size(boxSize)
        .background(Color.LightGray)
)

Сначала к этому компоненту применяется модификатор offset() для управления положением. Для получения координаты x применяется метод requireOffset() состояния. Затем смещение используется для позиционирования Box вдоль оси X.

Затем Box становится перетаскиваемым путем применения модификатора аnchoredDraggable(), в который передается состояние перетаскивания и направление (по горизонтали).

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850