Анимации жестов и пружин
Естественное движение на основе физики
«Анимации жестов и пружин» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Зачем нужны анимации
- Основы animate*AsState
- AnimatedVisibility и переходы
- Анимации жестов и пружин