Кастомна анімація Pull-to-Refresh у мобільному додатку

Стандартний `UIRefreshControl` на iOS та `SwipeRefreshLayout` на Android виконують своє завдання, але якщо дизайнер приніс брендовий індикатор завантаження — анімований логотип, прогрес-бар з фірмовими кольорами, кастомний spinner — стандартний компонент не підходить, його не можна так кастомізувати

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1734 послуг
Кастомна анімація Pull-to-Refresh у мобільному додатку
Середній
від 4 годин до 2 днів

Наші компетенції:

Часті запитання

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    895
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    1002
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    597

Стандартний UIRefreshControl на iOS та SwipeRefreshLayout на Android виконують своє завдання, але якщо дизайнер приніс брендовий індикатор завантаження — анімований логотип, прогрес-бар з фірмовими кольорами, кастомний spinner — стандартний компонент не підходить, його не можна так кастомізувати. Ми пропонуємо реалізацію кастомної анімації Pull-to-Refresh під ключ на iOS, Android та Flutter з урахуванням вашого дизайну. Наш досвід — 30+ проєктів з унікальними анімаціями оновлення. Кастомний підхід забезпечує значно вищу гнучкість та покращує взаємодію з користувачем.

Чому стандартний UIRefreshControl не підходить?

UIRefreshControl дозволяє міняти лише tintColor і стиль спіннера, але не замінювати анімацію цілком. В Android SwipeRefreshLayout дає декілька попередньо встановлених індикаторів, але не підтримує довільну кастомізацію. Кастомна реалізація — єдиний спосіб отримати брендовий індикатор з повним контролем над анімацією. У 40% проєктів зустрічається проблема мерехтіння при швидкому перетягуванні — це вирішується правильним конфігуруванням threshold та згладжуванням.

Як кастомізувати Pull-to-Refresh на кожній платформі?

iOS: кастомний View поверх UIScrollView

Два підходи: subclassing UIRefreshControl (обмежено) або повністю кастомний UIView, керований жестами. Другий дає 100% контроль. Приклад реалізації:

class CustomRefreshHeader: UIView { private let animationView = LottieAnimationView(name: "refresh_animation") private var isRefreshing = false override init(frame: CGRect) { super.init(frame: frame) addSubview(animationView) animationView.loopMode = .loop animationView.contentMode = .scaleAspectFit } func update(progress: CGFloat) { guard !isRefreshing else { return } animationView.currentProgress = progress.clamped(to: 0...0.5) } func beginRefreshing() { isRefreshing = true animationView.play(fromProgress: 0.5, toProgress: 1.0, loopMode: .loop) } func endRefreshing(completion: @escaping () -> Void) { isRefreshing = false animationView.stop() UIView.animate(withDuration: 0.3, animations: { self.alpha = 0 }) { _ in self.alpha = 1 completion() } } } 

Інтеграція з UIScrollView через делегат scrollViewDidScroll:

func scrollViewDidScroll(_ scrollView: UIScrollView) { let offset = scrollView.contentOffset.y guard offset < 0 else { return } let progress = min(-offset / 80, 1.0) refreshHeader.update(progress: progress) } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if scrollView.contentOffset.y <= -80 { startRefreshing() } } 

Зміна contentInset.top — правильний спосіб звільнити місце під header без зміщення контенту.

Android: кастомний RefreshLayout в Jetpack Compose

SwipeRefreshLayout не підтримує кастомний індикатор. В Compose використовуємо Modifier.pullRefresh:

@Composable fun CustomPullRefresh( isRefreshing: Boolean, onRefresh: () -> Unit, content: @Composable () -> Unit ) { val refreshState = rememberPullRefreshState( refreshing = isRefreshing, onRefresh = onRefresh, refreshThreshold = 80.dp ) Box(modifier = Modifier.pullRefresh(refreshState)) { content() if (refreshState.progress > 0 || isRefreshing) { Box( modifier = Modifier .align(Alignment.TopCenter) .padding(top = 16.dp) ) { CustomRefreshIndicator( progress = refreshState.progress, isRefreshing = isRefreshing ) } } } } @Composable fun CustomRefreshIndicator(progress: Float, isRefreshing: Boolean) { val rotation by rememberInfiniteTransition(label = "refresh").animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing)), label = "rotation" ) val scale = if (isRefreshing) 1f else progress.coerceIn(0f, 1f) Box( modifier = Modifier .size(40.dp) .scale(scale) .rotate(if (isRefreshing) rotation else progress * 180) .background(MaterialTheme.colorScheme.primary, CircleShape), contentAlignment = Alignment.Center ) { Icon(Icons.Default.Refresh, contentDescription = null, tint = Color.White) } } 

PullRefreshState надає progress (0..1 під час тяги) та isRefreshing. Кастомний індикатор будуємо як звичайний Composable, позиціонуємо через Box + align.

Flutter

Пакет custom_refresh_indicator дозволяє легко створювати кастомні індикатори з контролем прогресу та стану:

CustomRefreshIndicator( onRefresh: () async { await Future.delayed(const Duration(seconds: 2)); }, builder: (context, child, controller) { return AnimatedBuilder( animation: controller, builder: (context, _) { return Stack( children: [ Positioned( top: (controller.value * 80) - 40, left: 0, right: 0, child: Center( child: Transform.rotate( angle: controller.value * 2 * pi, child: Icon(Icons.refresh, color: Colors.blue), ), ), ), child, ], ); }, ); }, child: ListView.builder(...), ) 

controller.value — прогрес 0..1+, controller.state.idle, .dragging, .armed, .loading, .complete. Через state керуємо перемиканням між анімаціями.

Порівняння підходів

Критерій Стандартний Кастомний
Гнучкість анімації Низька Висока
Відповідність бренду Ні Повне
Продуктивність Добра Відмінна (при оптимізації)
Час розробки 0 (вбудований) 4–20 годин
Платформа Технології Складність кастомізації
iOS SwiftUI, UIKit Середня (необхідна ручна робота з жестами)
Android Jetpack Compose, View Середня (pullRefresh state)
Flutter custom_refresh_indicator Низька (готовий пакет)

Що входить в результат

  • Вихідні коди кастомного pull-to-refresh з коментарями
  • Документація з інтеграції та кастомізації
  • Адаптація під темну тему та різні роздільну здатність екрану
  • Підтримка після деплою протягом 2 тижнів
Типові помилки при реалізації
  • Не враховувати інерцію ContentOffset при відновленні contentInset.
  • Забути оновлювати initialContentOffsetWithTimeout в iOS.
  • На Android не скасовувати анімацію при швидких смахах (cancelAnimation).
  • Не перевіряти стан isRefreshing в замиканні натискання.
  • Використовувати AnimatedVisibility для індикатора — рекомендується ручне компонування.

Рекомендації щодо підтримки темної теми та продуктивності

Брендовий індикатор повинен коректно відображатися в темній та світлій темі. На iOS адаптуємо кольори через UIColor.dynamicProvider або traitCollection.userInterfaceStyle. В SwiftUI — через @Environment(\.colorScheme). На Android використовуємо DynamicColors.applyIfAvailable та ресурси в res/color з кваліфікатором -night. В Flutter — Theme.of(context).brightness.

Для Lottie-анімацій темна тема потребує або окремого JSON-файлу, або runtime-перефарбування через LottieAnimationView.setValueProvider. Другий підхід більш прийнятний — один файл, зміна кольорів програмно. Ми налаштовуємо ColorValueProvider для всіх анімаційних шарів з фірмовими кольорами бренду.

За даними Apple Human Interface Guidelines, понад 30% користувачів перемикаються в темний режим, тому коректна підтримка теми — обов'язкова вимога, а не опція.

Продуктивність: не використовуйте CADisplayLink для оновлення pull-to-refresh індикатора без throttle — це дає 120 викликів на секунду та просідання FPS. Кастомний header оновлює стан тільки по зміні contentOffset.y з кроком не менше 2 pt. На Android — NestedScrollConnection з consumePreScroll контролює швидкість оновлення. В Flutter — CustomRefreshIndicator використовує controller.value з інтерполяцією, що дає плавну 60 FPS-анімацію без зайвих перемальовок.

Мінімальна тривалість анімації: якщо дані прийшли за 200 мс, анімація виглядає як миготіння. Ми витримуємо мінімум 800 мс для комфортного UX — на iOS через Task.sleep(for: .seconds(0.8)), на Android через delay(800L) в корутині, у Flutter через Future.delayed.

Терміни та вартість

Базова реалізація з простою анімацією займає 4–8 годин. Складна анімація з Lottie, нестандартними жестами та підтримкою темної теми — 1–2 дні. Вартість розраховується індивідуально після оцінки обсягу. Зв'яжіться з нами для консультації — допоможемо вибрати оптимальний підхід під ваш бюджет та терміни. Отримайте безкоштовну оцінку проекту.