Проблеми при реалізації 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 Гц. Отримайте консультацію щодо вашого проєкту вже сьогодні.







