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

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

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

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

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

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

Стандартний 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 дні. Вартість розраховується індивідуально після оцінки обсягу. Зв'яжіться з нами для консультації — допоможемо вибрати оптимальний підхід під ваш бюджет та терміни. Отримайте оцінку проекту.

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

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

  • Розробка мобільного додатка для компанії FEEDME
    Розробка мобільного додатка для компанії FEEDME
    1010
  • Розробка мобільного додатку для компанії XOOMER
    Розробка мобільного додатку для компанії XOOMER
    903
  • Розробка мобільного додатку для компанії RHL
    Розробка мобільного додатку для компанії RHL
    1332
  • Розробка мобільного додатку для компанії ZIPPY
    Розробка мобільного додатку для компанії ZIPPY
    1180
  • Розробка мобільного додатку для компанії Affhome
    Розробка мобільного додатку для компанії Affhome
    1107
  • Розробка мобільного додатку для компанії FLAVORS
    Розробка мобільного додатку для компанії FLAVORS
    714