Реализация Spring-анимаций в Android-приложении (MotionLayout)
Android-разработчики долго работали с ValueAnimator и ObjectAnimator + AccelerateDecelerateInterpolator. Spring physics появились официально с Jetpack's SpringAnimation в 2018 году, а MotionLayout в том же году дал декларативный способ описывать сложные анимационные переходы. Сегодня у нас два чётко разделённых инструмента под разные задачи.
SpringAnimation: физика в коде
androidx.dynamicanimation:dynamicanimation — библиотека для spring и fling анимаций отдельных свойств View.
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
val springAnim = SpringAnimation(cardView, DynamicAnimation.TRANSLATION_Y).apply {
spring = SpringForce(0f).apply { // целевая позиция — 0f (исходная)
stiffness = SpringForce.STIFFNESS_MEDIUM // 1500f
dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY // 0.75f
}
}
springAnim.start()
Предустановки Stiffness: STIFFNESS_HIGH (10000), STIFFNESS_MEDIUM (1500), STIFFNESS_LOW (200), STIFFNESS_VERY_LOW (50). DampingRatio: NO_BOUNCY (1.0), LOW_BOUNCY (0.75), MEDIUM_BOUNCY (0.5), HIGH_BOUNCY (0.2).
Для gesture-driven spring — передаём скорость из VelocityTracker:
val vt = VelocityTracker.obtain()
// в onTouchEvent добавляем vt.addMovement(event)
vt.computeCurrentVelocity(1000) // pixels per second
springAnim.setStartVelocity(vt.yVelocity)
springAnim.animateToFinalPosition(targetY)
animateToFinalPosition — удобнее start() при повторных вызовах: если анимация уже идёт, просто меняет целевую позицию без резкого старта заново.
Практический кейс: bottom sheet с drag. Пользователь тянет вниз, отпускает — sheet spring-ится или закрывается в зависимости от скорости и позиции. Логика: если скорость вниз > 1000 dp/s или sheet опущен ниже 40% высоты → анимировать к нижнему краю и dismiss. Иначе → spring back к исходной позиции. Всё через SpringAnimation с setStartVelocity из VelocityTracker.
MotionLayout: декларативные переходы
MotionLayout — подкласс ConstraintLayout, управляет анимацией через MotionScene XML. Идеален для структурных изменений layout (не просто translation/scale, а изменение constraint-ов).
<!-- res/xml/scene_collapsing.xml -->
<MotionScene>
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@+id/header"
android:layout_height="200dp"
... />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/header"
android:layout_height="56dp"
... />
</ConstraintSet>
<Transition
android:id="@+id/transition"
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="400">
<OnSwipe
motion:touchAnchorId="@+id/recyclerView"
motion:touchAnchorSide="top"
motion:dragDirection="dragUp" />
</Transition>
</MotionScene>
Spring в MotionLayout — через motion:transitionEasing:
<KeyAttribute
motion:framePosition="100"
motion:motionTarget="@+id/fab"
motion:transitionEasing="overshoot(2.5)">
<CustomAttribute
motion:attributeName="scaleX"
motion:customFloatValue="1.0" />
</KeyAttribute>
overshoot(tension), anticipate(tension), anticipateOvershoot(tension) — встроенные easing функции с пружинным эффектом. Не настоящая физическая модель, но для большинства UI-переходов достаточно.
Для настоящего spring в MotionLayout — KeyCycle с sine wave аппроксимацией пружины. Трудоёмко, зато полный контроль.
Compose: spring() спецификация
В Jetpack Compose spring-анимации — первоклассные граждане:
val offset by animateFloatAsState(
targetValue = if (isExpanded) 0f else -300f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Animatable для gesture-driven:
val animatable = remember { Animatable(0f) }
LaunchedEffect(dragEnd) {
animatable.animateTo(
targetValue = 0f,
animationSpec = spring(stiffness = Spring.StiffnessMedium),
initialVelocity = lastVelocity
)
}
initialVelocity в Compose — в единицах значения в секунду. При работе с offset в pixels: velocity из detectDragGestures уже в pixels/second, передаём напрямую.
Сроки
Spring-анимации для 2–4 отдельных UI-элементов через SpringAnimation: 1 день. MotionLayout сцена с gesture-driven переходом (collapsing header, expandable card): 1–2 дня. Полный экран с комплексной анимационной системой: 2–3 дня. Стоимость рассчитывается индивидуально.







