Впровадження 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 presets: 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 пружинить або закривається залежно від швидкості та позиції. Логіка: якщо швидкість вниз > 1000 dp/s або sheet нижче 40% висоти → анімуйте вниз та закрийте. Інакше → пружинистьте назад до оригінальної позиції. Все через 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 апроксимацією spring physics. Трудомістко, але повний контроль.
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: швидкість з detectDragGestures вже в pixels/second, передавайте напряму.
Час розробки
Spring-анімації для 2–4 окремих UI-елементів через SpringAnimation займають 1 день. MotionLayout сцена з gesture-driven переходом (collapsing header, expandable card) займає 1–2 дні. Повний екран зі складною анімаційною системою займає 2–3 дні. Вартість розраховується індивідуально.







