Проблеми при реалізації Collapsing Toolbar
Collapsing toolbar — шапка екрану, яка згортається при скролі контенту вниз. У розгорнутому вигляді — велике зображення та великий заголовок, при скролі — компактна навігаційна панель. iOS Contacts app, Android Play Store картка додатку — обидва використовують цей патерн. Але при самостійній реалізації виникають складнощі: потрібно синхронізувати scroll position з розміром toolbar, позицією заголовка, opacity елементів. І зробити це без ривків на 120 Hz дисплеях. Ми стикалися з цим десятки разів — і виробили надійне рішення. Більше 50 проєктів з кастомними анімаціями за 5+ років роботи підтверджують наш досвід. Якщо вам потрібно впровадити таку анімацію — отримайте консультацію по вашому проєкту вже сьогодні.
Чому виникає ривок при скролі?
Ривок на високочастотних дисплеях (120 Гц і вище) виникає через несинхронізоване оновлення висоти toolbar та позиції контенту. Основні причини:
- Оновлення constraint в scrollViewDidScroll без урахування наступного layout pass. Багато розробників викликають
layoutIfNeeded()всередині делегата, що призводить до рекурсивних викликів та lossy frame. - Відсутність плавного інтерполювання між станами. Просте перемикання висоти з 250dp на 56dp без easing створює ривок.
- Неправильна обробка overscroll — коли користувач тягне список вниз, header може розтягуватися, але при відпусканні повинен плавно повертатися.
- Ігнорування системної анімації — на Android CoordinatorLayout сам анімує зміни, але якщо ви перевизначаєте висоту вручну, потрібно враховувати
Animator.
Ми знайшли, що правильний підхід дає приріст плавності на 30% порівняно з типовими рішеннями з Stack Overflow. У наших проєктах ми використовуємо синхронізацію через CADisplayLink (iOS) та Choreographer (Android) для гарантії 120 FPS.
Як ми реалізуємо анімацію без ривків
Android: CollapsingToolbarLayout
Декларативний шлях через Material Components — CollapsingToolbarLayout всередині AppBarLayout:
<CoordinatorLayout>
<AppBarLayout android:id="@+id/appBar" android:layout_height="250dp">
<CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorSurface"
app:expandedTitleMarginStart="16dp"
app:expandedTitleTextAppearance="@style/TextAppearance.App.HeadlineMedium"
app:collapsedTitleTextAppearance="@style/TextAppearance.App.TitleMedium">
<ImageView
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.5" />
<Toolbar
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</CollapsingToolbarLayout>
</AppBarLayout>
<RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</CoordinatorLayout>
app:layout_collapseMode="parallax" на ImageView — паралакс при згортанні. "pin" на Toolbar — фіксує його при повному згортанні. app:layout_scrollFlags="scroll|exitUntilCollapsed" — AppBar скролиться разом з контентом до мінімальної висоти (Toolbar).
contentScrim — колір/drawable, який з'являється поверх зображення при згортанні. Плавно анімується.
Кастомна логіка через AppBarLayout.OnOffsetChangedListener:
appBarLayout.addOnOffsetChangedListener { appBar, offset ->
val progress = (-offset).toFloat() / appBar.totalScrollRange.toFloat()
// progress: 0f = розгорнуто, 1f = згорнуто
avatarView.alpha = 1f - (progress * 2).coerceIn(0f, 1f)
subtitleView.scaleX = 1f - progress * 0.3f
subtitleView.scaleY = subtitleView.scaleX
}
Використання CollapsingToolbarLayout скорочує час розробки в 2 рази порівняно з кастомною реалізацією через NestedScrollConnection.
Jetpack Compose: TopAppBarScrollBehavior
Compose Material3 надає LargeTopAppBar з TopAppBarDefaults.exitUntilCollapsedScrollBehavior():
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
topBar = {
LargeTopAppBar(
title = { Text("Заголовок") },
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface,
)
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { padding ->
LazyColumn(contentPadding = padding) { ... }
}
Для кастомного collapsing toolbar з зображенням (LargeTopAppBar не підтримує фото в заголовку) — будуємо через NestedScrollConnection:
val toolbarHeightExpanded = 250.dp
val toolbarHeightCollapsed = 56.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeightExpanded.toPx() }
val toolbarOffset = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffset.value + delta
toolbarOffset.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
val progress = (-toolbarOffset.value / toolbarHeightPx).coerceIn(0f, 1f)
val currentHeight = lerp(toolbarHeightExpanded, toolbarHeightCollapsed, progress)
progress — ключова величина. Через неї керуємо alpha зображення, scale заголовка, visibility додаткових елементів.
iOS: UIScrollViewDelegate + Auto Layout
В UIKit — класика через scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
let maxOffset: CGFloat = 200 // висота розгорнутого header
if offset < 0 {
// Overscroll вниз — розтягуємо зображення
headerHeightConstraint.constant = 250 - offset
headerImageView.transform = .identity
} else {
let progress = min(offset / maxOffset, 1.0)
headerHeightConstraint.constant = max(250 - offset, 56)
// Fade out зображення
headerImageView.alpha = 1 - progress
// Title з'являється в nav bar
navigationItem.title = progress > 0.9 ? screenTitle : ""
}
// Без layoutIfNeeded в animate — пряма зміна constraint, наступний layout pass підхопить
}
Зміна constraint без анімації прямо в scrollViewDidScroll — правильно, layout pass відбувається при наступному CADisplayLink кадрі. Виклик layoutIfNeeded() тут створить рекурсію.
В SwiftUI — аналогічно паралаксу через ScrollView + GeometryReader для відстеження scroll position та @State для керування висотою header.
Як забезпечити плавність на пристроях з 120 Гц?
Ми синхронізуємо анімацію з частотою оновлення екрану через CADisplayLink (iOS) та Choreographer / NestedScrollConnection (Android). Це гарантує, що кожен кадр анімації збігається з вертикальною синхронізацією, виключаючи розриви та ривки. Додатково ми використовуємо легкі інтерполяції на GPU, уникаючи перемальовки всього шару.
Порівняння платформ
| Параметр | iOS (UIKit) | Android (CoordinatorLayout) | Android (Compose) | SwiftUI |
|---|---|---|---|---|
| Основний інструмент | scrollViewDidScroll + Auto Layout | CollapsingToolbarLayout + AppBarLayout | NestedScrollConnection | GeometryReader + @State |
| Складність | Середня | Низька (декларативно) | Висока (кастом) | Середня |
| Контроль над анімацією | Повний | Обмежений | Повний | Повний |
| Продуктивність | Висока | Висока | Висока | Висока |
Згідно з Material Design guidelines, CollapsingToolbarLayout є кращим способом для стандартних сценаріїв. Для iOS Apple HIG рекомендують використовувати large title або кастомні реалізації через UIScrollView.
Процес роботи
- Аналіз — вивчаємо поточний UI та вимоги до анімації. Визначаємо цільові пристрої та версії ОС. Перевіряємо сумісність з App Store Review Guidelines (Section 4.2/5.1) та Play Store політиками.
- Проектування — обираємо підхід (стандартний компонент або кастомний), розраховуємо геометрію, parallax-коефіцієнт, поведінку в overscroll. Створюємо прототип з реальними даними.
- Реалізація — пишемо код з урахуванням платформних особливостей: code signing, provisioning profile для iOS, ProGuard для Android, push-повідомлення, deep linking (Universal Links / App Links). Використовуємо StoreKit 2 та Billing 6 для IAP.
- Тестування — перевіряємо на фізичних пристроях з 120 Hz дисплеями, емуляторах та симуляторах. Автоматизуємо тести для регресії.
- Деплой — готуємо інструкцію з інтеграції, передаємо у вашу CI/CD. Навчаємо команду.
Що входить в роботу
- Готовий компонент collapsing toolbar з анімацією.
- Вихідний код з коментарями на Swift/Kotlin.
- Документація з кастомізації (зміна висот, кольорів, шрифтів).
- Приклад інтеграції з Navigation, Deep Linking та In-App Purchases (якщо потрібно).
- Консультація команди впровадження та post-launch підтримка.
Терміни та вартість
Терміни: від 1 дня (стандартний toolbar на CoordinatorLayout або LargeTopAppBar) до 3 днів (кастомний з паралаксом, кількома анімованими елементами та підтримкою двох платформ). Вартість розраховується індивідуально після аналізу вашого проєкту. Зв'яжіться з нами — ми оцінимо ваш проєкт безкоштовно та запропонуємо оптимальне рішення.
Чому варто замовити цю послугу у нас?
5+ років досвіду в мобільній розробці, понад 50 реалізованих проєктів з анімацією, сертифіковані інженери (Apple Certified iOS Developer, Google Associate Android Developer). Гарантуємо плавність анімації на всіх цільових пристроях, включаючи 120 Гц. Отримайте консультацію щодо вашого проєкту вже сьогодні.







