0Pricing
Android Academy · Урок

Анимации жестов и пружин

Естественное движение на основе физики

«Анимации жестов и пружин» — бесплатный урок Android Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.

Интерактивное движение с физикой

Самые приятные анимации реагируют на движение пальца пользователя и ощущаются как движение реальных объектов — с инерцией и упругостью. Для их создания нужно объединить жесты с анимацией, основанной на физике.

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

Animatable: низкоуровневое управление

В отличие от animate*AsState, реагирующего на состояние, Animatable предоставляет императивное управление. Вы храните его в remember и управляете им из корутины с помощью вызовов вроде animateTo или snapTo.

Это основа движения, управляемого жестами.

@Composable
fun BounceOnClick() {
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .graphicsLayer { scaleX = scale.value; scaleY = scale.value }
            .clickable {
                scope.launch {
                    scale.animateTo(1.3f, spring())
                    scale.animateTo(1f, spring())
                }
            }
    )
}

Пружины: dampingRatio и stiffness

Спецификация spring определяется двумя значениями:

  • dampingRatio — степень упругости. Чем ниже значение, тем сильнее отскок; при 1f (критическом затухании) перелёта за цель нет.
  • stiffness — скорость. Чем выше значение, тем быстрее достигается цель.

Compose предоставляет такие константы, как Spring.DampingRatioMediumBouncy и Spring.StiffnessLow, поэтому Вам редко приходится указывать необработанные числовые значения.

val spec = spring<Float>(
    dampingRatio = Spring.DampingRatioLowBouncy,
    stiffness = Spring.StiffnessMedium
)

// later, inside a coroutine:
// offset.animateTo(targetValue = 0f, animationSpec = spec)

Распознавание жестов перетаскивания

Чтобы сделать движение интерактивным, нужно считывать сенсорный ввод. Modifier.pointerInput вместе с detectDragGestures сообщает смещения при перетаскивании, пока пользователь перемещает палец.

Вы накапливаете эти смещения и применяете их к компонуемому элементу.

@Composable
fun Draggable() {
    var offsetX by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .size(80.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectDragGestures { change, drag ->
                    change.consume()
                    offsetX += drag.x
                }
            }
    )
}

Возврат с помощью пружины

Объедините перетаскивание с Animatable, чтобы получить эффект «возврата». Во время перетаскивания мгновенно устанавливайте положение пальца с помощью snapTo; после отпускания вызывайте animateTo(0f) с пружинной анимацией, чтобы элемент подпрыгнул и вернулся на место.

@Composable
fun SnapBack() {
    val offsetX = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .offset { IntOffset(offsetX.value.roundToInt(), 0) }
            .size(80.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { change, drag ->
                        change.consume()
                        scope.launch { offsetX.snapTo(offsetX.value + drag.x) }
                    },
                    onDragEnd = {
                        scope.launch { offsetX.animateTo(0f, spring()) }
                    }
                )
            }
    )
}

Инерционное движение со скоростью

При настоящем резком движении объект продолжает перемещаться после того, как палец отрывается от экрана. Отслеживайте скорость с помощью VelocityTracker, а затем передайте её в animateDecay, чтобы элемент естественно замедлился с использованием основанной на трении DecayAnimationSpec.

val decay = rememberSplineBasedDecay<Float>()

// on drag end, with measured velocity:
// scope.launch {
//     offsetX.animateDecay(
//         initialVelocity = velocity,
//         animationSpec = decay
//     )
// }

Смахивание для закрытия с якорями

Для жестов смахивания с фиксированными положениями покоя, например для закрытия смахиванием, anchoredDraggable перемещает элемент к заданным якорям. Вы объявляете состояния и их положения в пикселях, а Compose обрабатывает пружинный переход между ними.

enum class DragValue { Start, End }

val state = remember {
    AnchoredDraggableState(
        initialValue = DragValue.Start,
        anchors = DraggableAnchors {
            DragValue.Start at 0f
            DragValue.End at 300f
        }
    )
}

Бесконечные и повторяющиеся анимации

Для непрерывного движения, например пульсирующей точки или эффекта мерцания при загрузке, используйте rememberInfiniteTransition вместе с infiniteRepeatable. Установите для RepeatMode значение Reverse, чтобы движение шло туда и обратно, или Restart, чтобы запускать цикл заново.

@Composable
fun Pulse() {
    val transition = rememberInfiniteTransition(label = "pulse")
    val scale by transition.animateFloat(
        initialValue = 1f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
            animation = tween(700),
            repeatMode = RepeatMode.Reverse
        ),
        label = "scale"
    )
    Box(Modifier.graphicsLayer { scaleX = scale; scaleY = scale })
}

Объединение жеста и пружины

Классический интерактивный сценарий: перетащить, чтобы переместить, и отпустить, чтобы зафиксировать положение. Отслеживайте скорость во время перетаскивания, а затем либо запускайте инерционное движение с помощью animateDecay, либо перемещайте элемент к якорю с помощью spring — в зависимости от расстояния или скорости.

Именно поэтому нижние панели, карусели и карточки, которые можно закрыть, ощущаются настолько естественно.

// pattern:
// 1) onDrag    -> animatable.snapTo(current + delta)
// 2) onDragEnd -> if (fast) animatable.animateDecay(velocity, decay)
//                 else      animatable.animateTo(nearestAnchor, spring())

Пружинная фиксация в чистом Kotlin

Чтобы понять, как пружина возвращает объект в исходное положение, рассмотрим минимальный цикл на чистом Kotlin. На каждом шаге он прикладывает возвращающую силу к цели и уменьшает скорость — это та же идея, которую Compose выполняет на каждом кадре.

fun main() {
    var pos = 0f
    var vel = 0f
    val target = 200f
    val stiffness = 0.2f
    val damping = 0.75f
    repeat(8) { step ->
        vel = (vel + (target - pos) * stiffness) * damping
        pos += vel
        println("step $step pos=${"%.1f".format(pos)}")
    }
}

Нажатия, удержания и несколько жестов

Перетаскивание — не единственный жест. pointerInput также предоставляет detectTapGestures для одиночных и двойных нажатий, а также длительных нажатий, и detectTransformGestures для масштабирования двумя пальцами и поворота.

Здесь длительное нажатие запускает пружинное изменение масштаба, объединяя распознавание жеста с Animatable.

@Composable
fun LongPressScale() {
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()
    Box(
        Modifier
            .graphicsLayer { scaleX = scale.value; scaleY = scale.value }
            .pointerInput(Unit) {
                detectTapGestures(
                    onLongPress = {
                        scope.launch { scale.animateTo(1.4f, spring()) }
                    },
                    onPress = {
                        tryAwaitRelease()
                        scope.launch { scale.animateTo(1f, spring()) }
                    }
                )
            }
    )
}

Быстрая проверка

Вы создаёте перетаскиваемую карточку, которая после отпускания должна подпрыгнуть и вернуться в исходное положение. Какое сочетание считается идиоматичным выбором в Compose?

Итоги: жесты и пружины

Теперь вы умеете создавать естественное интерактивное движение:

  • Animatable предоставляет императивное управление из сопрограмм с помощью animateTo и snapTo.
  • Параметры spring настраиваются с помощью dampingRatio (отскок) и stiffness (скорость).
  • pointerInput и detectDragGestures превращают сенсорный ввод в движение, а animateDecay добавляет инерционный бросок.
  • anchoredDraggable перемещает элемент к фиксированным положениям при взаимодействии смахиванием.
  • rememberInfiniteTransition управляет зацикленными эффектами, такими как пульсация и мерцание.

На этом раздел об анимациях в Jetpack Compose завершён. Теперь вы сможете уверенно анимировать значения, видимость, содержимое и жесты.

Часто задаваемые вопросы

Урок «Анимации жестов и пружин» бесплатный?

Да — полный текст урока «Анимации жестов и пружин» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 4 уроков всего.

Чему я научусь в уроке «Анимации жестов и пружин»?

Естественное движение на основе физики Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Android Academy?

Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Анимации жестов и пружин»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Android Academy?

Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Зачем нужны анимации
  2. Основы animate*AsState
  3. AnimatedVisibility и переходы
  4. Анимации жестов и пружин
← Назад к Android Academy