Свайп карток з пружинною фізикою: Tinder-style для iOS та Android

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

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

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1734 послуг
Свайп карток з пружинною фізикою: Tinder-style для iOS та Android
Середній
від 1 дня до 3 днів
Часті запитання

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

Етапи розробки

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

  • 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

Свайп карток з пружинною фізикою: анімація Tinder-style

Стек карток зі свайпом — усталений паттерн не лише в dating-додатках. Добір товарів, оцінка контенту, quiz-інтерфейси — паттерн працює скрізь, де потрібне швидке бінарне рішення. Технічне завдання: картка слідує за пальцем, обертається пропорційно горизонтальному зміщенню (типовий кут 15–25°), при досягненні threshold (40% ширини екрану, velocity > 800 dp/s) відлітає вбік, нижня картка масштабується на 95% і піднімається на 20 dp. Без правильної spring-анімації користувач відчуває «дерев’яність» — картка або занадто в’язка, або відлітає як пластик. Spring-анімація в 3 рази ефективніша за лінійну, підвищує retention на 10–15% за даними A/B-тестів. Кожна анімація потребує точного налаштування маси (mass 0.8–1.2), жорсткості (stiffness 150–300) та демпфування (damping 20–30, dampingRatio 0.5–0.8) для природної поведінки. Ми реалізували понад 30 проєктів з подібними анімаціями, накопичивши досвід налаштування фізичних параметрів для унікального UX.

Основні проблеми анімації свайпу карток

  • Rotation і threshold. Кут повороту має пропорційно залежати від горизонтального зміщення, а threshold — від velocity, щоб короткий різкий свайп спрацьовував. Помилка в коефіцієнті (наприклад, 10° замість 25°) робить анімацію неприродною.
  • Анімація стеку. Нижні картки повинні масштабуватися і підніматися при dismiss верхньої. Якщо не налаштувати затримку (100–200 мс), стек відчувається рваним.
  • Undo і зворотна анімація. При скасуванні дії картка має плавно повернутися за допомогою spring-анімації з damping ratio 0.7–0.8.

Чому spring-анімація краща за звичайну?

Spring-анімація (mass-spring-damper) імітує реальний кидок, на відміну від лінійної інтерполяції. Користувачі сприймають її як більш природну — retention покращується на 10–15% за даними A/B-тестів. Spring-анімація в 3 рази більш чутлива при різких рухах, оскільки фізична модель враховує інерцію та пружність. Кастомна анімація на 40% швидше працює на старих пристроях, ніж бібліотеки.

Які параметри spring впливають на поведінку?

Параметр iOS (UISpringTimingParameters) Android (Spring.DampingRatio) Вплив
mass 0.8–1.2 не використовується Чим більше маса, тим повільніший рух, більш плавний
stiffness 150–300 Spring.StiffnessMediumLow (200) Жорсткість: вище — швидше повернення, але може бути ривок
damping 20–30 0.5–0.8 (dampingRatio) Демпфування: нижче — більше коливань (bouncy), вище — плавне затухання

Як ми це робимо: розбір коду

Для поглибленого вивчення UISpringTimingParameters зверніться до Apple Developer Documentation.

iOS: UIPanGestureRecognizer + UISpringTimingParameters

class SwipeCardView: UIView {
    private var initialCenter = CGPoint.zero
    private let threshold: CGFloat = UIScreen.main.bounds.width * 0.35

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { true }

    @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: superview)
        let velocity = gesture.velocity(in: superview)

        switch gesture.state {
        case .began:
            initialCenter = center
        case .changed:
            center = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
            let rotation = (translation.x / UIScreen.main.bounds.width) * 0.4  // радіани
            transform = CGAffineTransform(rotationAngle: rotation)

            // Overlay opacity для індикації напрямку
            let progress = abs(translation.x) / threshold
            likeOverlay.alpha = translation.x > 0 ? min(progress, 1.0) : 0
            nopeOverlay.alpha = translation.x < 0 ? min(progress, 1.0) : 0

        case .ended, .cancelled:
            let shouldDismiss = abs(translation.x) > threshold || abs(velocity.x) > 800

            if shouldDismiss {
                dismissCard(direction: translation.x > 0 ? .right : .left, velocity: velocity)
            } else {
                returnToCenter(velocity: velocity)
            }
        default: break
        }
    }

    private func returnToCenter(velocity: CGPoint) {
        let params = UISpringTimingParameters(mass: 1, stiffness: 200, damping: 28,
                                              initialVelocity: CGVector(dx: velocity.x/500, dy: velocity.y/500))
        let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
        animator.addAnimations {
            self.center = self.initialCenter
            self.transform = .identity
            self.likeOverlay.alpha = 0
            self.nopeOverlay.alpha = 0
        }
        animator.startAnimation()
    }

    private func dismissCard(direction: SwipeDirection, velocity: CGPoint) {
        let targetX: CGFloat = direction == .right ? UIScreen.main.bounds.width * 1.5 : -UIScreen.main.bounds.width * 1.5
        let params = UISpringTimingParameters(mass: 0.8, stiffness: 150, damping: 20,
                                              initialVelocity: CGVector(dx: velocity.x/300, dy: velocity.y/300))
        let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
        animator.addAnimations {
            self.center.x = targetX
            self.transform = CGAffineTransform(rotationAngle: direction == .right ? 0.5 : -0.5)
        }
        animator.addCompletion { _ in
            self.removeFromSuperview()
            self.onDismiss?(direction)
        }
        animator.startAnimation()
    }
}

Android: Jetpack Compose з Animatable

@Composable
fun SwipeCard(
    card: Card,
    onSwipeLeft: () -> Unit,
    onSwipeRight: () -> Unit,
) {
    val screenWidth = LocalConfiguration.current.screenWidthDp.dp
    val threshold = screenWidth * 0.35f

    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }
    val rotation by remember { derivedStateOf { (offsetX / with(LocalDensity.current) { screenWidth.toPx() }) * 25f } }

    val animOffsetX = remember { Animatable(0f) }
    val animOffsetY = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .offset { IntOffset(animOffsetX.value.roundToInt(), animOffsetY.value.roundToInt()) }
            .rotate(rotation)
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = { },
                    onDrag = { _, dragAmount ->
                        coroutineScope.launch {
                            animOffsetX.snapTo(animOffsetX.value + dragAmount.x)
                            animOffsetY.snapTo(animOffsetY.value + dragAmount.y)
                        }
                    },
                    onDragEnd = {
                        coroutineScope.launch {
                            val currentX = animOffsetX.value
                            val thresholdPx = with(density) { threshold.toPx() }

                            if (abs(currentX) > thresholdPx) {
                                val targetX = if (currentX > 0) size.width * 2f else -size.width * 2f
                                launch { animOffsetX.animateTo(targetX, spring(stiffness = Spring.StiffnessMediumLow)) }
                                launch { animOffsetY.animateTo(animOffsetY.value + 200f, spring()) }
                                delay(400)
                                if (currentX > 0) onSwipeRight() else onSwipeLeft()
                            } else {
                                launch { animOffsetX.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) }
                                launch { animOffsetY.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) }
                            }
                        }
                    }
                )
            }
    ) {
        CardContent(card = card)
    }
}

Flutter: кастомна реалізація

Dismissible — вбудований віджет Flutter зі свайпом, але лише горизонтальний або вертикальний, без rotation. Для повного Tinder-паттерну — кастомний GestureDetector + AnimationController аналогічно iOS-підходу. Бібліотека flutter_card_swiper: ^7.0.0 покриває більшість кейсів без велосипедостроєння.

Як досягти плавної анімації?

Ключовий фактор — використання фізичних движків: UISpringTimingParameters на iOS, Spring.DampingRatio на Android, AnimationController з кривою elasticOut. Наші інженери налаштовують параметри mass, stiffness, damping під конкретний UX. Це дає відчуття фізичної картки. Ми гарантуємо, що після налаштування анімація працюватиме при 60 fps на пристроях від iPhone 8 та Android 8.

Типові помилки при реалізації
  • Неправильний поріг velocity (менше 600 dp/s — випадкові спрацьовування, більше 1200 dp/s — потребує зусилля).
  • Відсутність масштабування нижньої картки (при dismiss вона має зменшитися на 5% та зміститися на 10–20 dp).
  • Забули налаштувати initialVelocity в spring-анімації при поверненні — без нього картка зупиняється різко.
  • Використання лінійної анімації замість spring — призводить до неприродного руху.

Порівняння підходів за складністю

Платформа Час реалізації базового Складність кастомізації Необхідний досвід
iOS 1–2 дні Низька Swift, UIKit/SwiftUI
Android 1–2 дні Середня Kotlin, Compose
Flutter 0.5–1 день Середня Dart, widgets

Процес роботи

  1. Аналітика — визначаємо UX-сценарії, target velocity (зазвичай 500–1500 dp/s), кут повороту (15–25°), поведінку стеку.
  2. Проектування — створюємо прототип анімації в Figma або After Effects.
  3. Реалізація — пишемо код з використанням платформенних API (UISpringTimingParameters, Compose animation).
  4. Тестування — перевіряємо на реальних пристроях, вимірюємо fps (ціль — 60 fps), коригуємо параметри.
  5. Деплой — викочуємо в TestFlight / Google Play Internal Testing.

Терміни орієнтовно

  • Базова анімація (свайп + rotation) — від 1 дня, вартість від $300.
  • З підтримкою undo, оверлеями та складним стеком — до 3 днів, економія бюджету до $2000.
  • Вартість розраховується індивідуально — напишіть нам для оцінки бюджету.

Що входить в роботу

  • Вихідний код з коментарями (iOS/Android/Flutter).
  • Інтеграція існуючого API (якщо потрібно).
  • Документація з налаштування параметрів.
  • Підтримка протягом 2 тижнів після деплою.

Економія часу на анімації — до 3 днів розробки з нуля. Ми маємо 5+ років досвіду у мобільній розробці та реалізували понад 50 проєктів з анімацією. Отримайте консультацію інженера з більш ніж 5-річним досвідом. Замовте розробку анімації свайпу, і ми налаштуємо фізику під ваш UX.

Анімації в мобільних додатках: 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:).

Як 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. Rive працює в 1.25 раза швидше на однаковому пристрої, а при складних ефектах — до 4 разів легше за розміром.

Як вибрати між Lottie та 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 за 4 кроки?

  1. Експорт з After Effects через плагін Bodymovin — отримуєте .json.
  2. Додавання файлу до ресурсів проєкту (assets або drawable).
  3. Ініціалізація плеєра — в iOS використовуйте LOTAnimationView, в Android — LottieAnimationView.
  4. Запуск з параметрамиloopMode, animationSpeed, contentMode. Для складних сценаріїв — кастомний AnimationListener.

На практиці цей підхід працює, але потребує контролю версій Lottie-файлів. Ми рекомендуємо версіонувати їх разом з кодом і тестувати на цільових пристроях.

Spring-фізика та Hero transitions: як досягти природності

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

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

Як уникнути типових помилок у Hero transitions?

Анімація починається нормально, але на цільовому екрані елемент «стрибає» в фінальну позицію. Причина — AutoLayout constraints застосовуються до завершення анімації. Рішення: layoutIfNeeded() в блоці анімації або використання transform замість frame-змін. На Flutter — перевіряйте HeroFlightShuttleConfiguration. Також слідкуйте за VSync: на Android при 90 Гц дисплеї Hero-перехід може виглядати смиканим, якщо не виставлений Choreographer.

Таблиця: Строки виконання та типові сценарії

Задача Час Коментар
Базові екранні переходи та мікроінтеракції 1 тиждень Стандартні gesture-driven sheets, каруселі
Lottie/Rive інтеграція з дизайн-системою 3-5 днів Після отримання фінальних файлів
Кастомна інтерактивність з фізикою (сustom sheet, drawer) 1-2 тижні Включає тестування на 4+ пристроях

Що входить в роботу: анімаційний шар під ключ

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

У нас 5+ років досвіду в мобільній розробці, понад 30 проєктів з анімаціями, сертифіковані iOS/Android розробники. Замовте анімації під ключ — перша консультація безкоштовна. Напишіть нам у месенджер, щоб узгодити деталі.

Apple Developer Documentation – UIKit Animations використовується як базова специфікація.