Кастомная анимация Pull-to-Refresh в мобильном приложении

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

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

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

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    562

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

Почему стандартный 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 c consumePreScroll контролирует скорость обновления. В Flutter — CustomRefreshIndicator использует controller.value с интерполяцией, что даёт плавную 60 FPS-анимацию без лишних перерисовок.

Минимальная длительность анимации: если данные пришли за 200 мс, анимация выглядит как мигание. Мы выдерживаем минимум 800 мс для комфортного UX — на iOS через Task.sleep(for: .seconds(0.8)), на Android через delay(800L) в корутине, во Flutter через Future.delayed. Стоимость реализации кастомного pull-to-refresh — от 50 000 до 200 000 ₽ в зависимости от платформы и сложности анимации.

Сроки и стоимость

Базовая реализация с простой анимацией занимает 4–8 часов. Сложная анимация с Lottie, нестандартными жестами и поддержкой тёмной темы — 1–2 дня. Стоимость рассчитывается индивидуально после оценки объёма. Свяжитесь с нами для консультации — поможем выбрать оптимальный подход под ваш бюджет и сроки. Получите бесплатную оценку проекта.

Анимации в мобильных приложениях: Lottie, Rive, Spring и Reanimated

Мы сделали анимации для десятков проектов — от игровых интерфейсов до bank-grade приложений. Знаем, как заставить 120 fps работать даже на Android с ProGuard. Если анимация дёргается — проблема не в инструменте, а в выборе подхода. Ниже покажем, как мы выбираем между Lottie и Rive, почему Spring physics побеждает UIView.animate, и как Reanimated 3 выжимает 60 fps на старых устройствах. Получите консультацию по вашему проекту — оценим анимационный слой бесплатно.

Почему UIView.animate ломается на сложных сценариях

UIView.animate(withDuration:) и ObjectAnimator на Android — правильный выбор для простых переходов. Но как только анимация становится интерактивной (пользователь тянет элемент, скорость зависит от жеста), нужен другой подход.

На iOS для gesture-driven анимации правильный инструмент — UIViewPropertyAnimator. Он позволяет приостанавливать, обращать и модифицировать анимацию в процессе. Типичный кейс: bottom sheet, который следует за пальцем, продолжает движение с инерцией после отпускания и притягивается к ближайшей позиции. С UIView.animate это либо не работает совсем, либо требует ручной физики.

В SwiftUI withAnimation работает из коробки, но интерактивность ограничена — нет прямого аналога UIViewPropertyAnimator. Обходной путь: .gesture(DragGesture()) + @GestureState + явное вычисление позиции. Или уходим в SwiftUI Animations API с Animation.spring(duration:bounce:) из iOS 17.

Как React Native Reanimated обходит JS-мост

React Native Animated API выполняет анимации в JS thread — это источник джанка при загруженном bridge. Reanimated 3 решает проблему через worklets: функции, которые компилируются и выполняются прямо на UI thread без пересечения JS-моста.

Пример: parallax scroll header. На базовом Animated.Value при быстром скролле FPS падает до 40-45 на mid-range Android. На Reanimated с useAnimatedScrollHandler — стабильные 60 fps, потому что весь пересчёт позиции происходит на UI thread.

Reanimated 3 с useSharedValue, useAnimatedStyle и withSpring/withTiming — это текущий стандарт для анимаций в React Native. Gesture Handler v2 плотно интегрирован: useAnimatedGestureHandler заменяет PanResponder и тоже работает на UI thread.

Почему Lottie может снижать FPS на Android

Lottie экспортирует After Effects анимацию в JSON. На iOS с lottie-ios работает стабильно, но на Android при сложных эффектах (blur, частицы, градиенты) векторный рендеринг через Canvas приводит к просадкам до 30-40 fps. Решение — либо упрощать анимацию, либо использовать Rive с аппаратным рендерингом. Мы тестировали: Lottie-файл с размытием 5 МБ на Xiaomi Redmi Note 10 даёт 48 fps, аналогичная анимация в Rive (.riv 400 КБ) — 60 fps.

Lottie vs Rive: что выбрать для интерактивных интерфейсов

Оба инструмента решают задачу «дизайнер делает анимацию, разработчик добавляет файл». Но принципиально по-разному.

Подробное сравнение в таблице
Критерий Lottie Rive
Формат JSON векторная анимация Бинарный .riv
Интерактивность Нет (линейное воспроизведение) State Machine, реакции на ввод
Производительность Средняя (blur/частицы тяжелы) Аппаратный рендеринг Metal/OpenGL
Размер файла 2-5 МБ 200-500 КБ
Поддержка платформ iOS, Android, Web, Flutter, RN iOS, Android, Web, Flutter, RN

Выбор прост: статичная декоративная анимация (splash screen, onboarding иллюстрации) — Lottie. Интерактивные UI-элементы с состояниями — Rive. Например, кнопка с hover, pressed, loading, success — одна Rive-анимация с четырьмя состояниями против четырёх отдельных Lottie-файлов.

Дополнительно: Lottie — стандарт для размеченной векторной анимации, Rive — более гибкий инструмент с физикой и state machine.

Spring-физика и Hero transitions: как добиться естественности

Spring-анимация ощущается естественно потому что имитирует физику — массу, жёсткость и демпфирование. В SwiftUI: Animation.spring(response:dampingFraction:). В Android Compose: spring(dampingRatio = Spring.DampingRatioMediumBouncy).

Для Hero-переходов (элемент «перелетает» между экранами) на iOS используем UIViewControllerTransitioningDelegate + UIViewControllerAnimatedTransitioning. В SwiftUI с iOS 17 — matchedTransitionSource + navigationTransition(.zoom). На Flutter — Hero виджет, который работает из коробки.

Как избежать типичных ошибок в Hero transitions?

Анимация начинается нормально, но на целевом экране элемент «прыгает» в финальную позицию. Причина — AutoLayout constraints применяются до завершения анимации. Решение: layoutIfNeeded() в блоке анимации или использование transform вместо frame-изменений.

Что входит в работу: анимационный слой под ключ

  • Интеграция Lottie/Rive-файлов в дизайн-систему
  • Код gesture-driven переходов (bottom sheets, drawers, карусели)
  • Тестирование на реальных устройствах (iOS 15–17, Android 10–14)
  • Документация по анимациям (архитектура, ключи состояний)
  • Поддержка при обновлении дизайна (гарантия 30 дней)

У нас 5+ лет опыта в мобильной разработке, более 30 проектов с анимациями, сертифицированные iOS/Android разработчики.

Сроки выполнения

  • Базовые экранные переходы и микроинтеракции — 1 неделя.
  • Lottie/Rive интеграция с дизайн-системой — 3-5 дней после получения финальных файлов.
  • Кастомная gesture-driven интерактивность (sheet, drawer, карусель с физикой) — 1-2 недели.

Напишите нам — добавим анимации под ключ за 2 недели. Первая консультация бесплатно.