Реалізація ефекту паралаксу при скролі у мобільному застосунку

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

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

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Реалізація ефекту паралаксу при скролі у мобільному застосунку
Середній
від 1 дня до 3 днів
Часті запитання

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

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

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

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Реалізація паралакс-ефекту прокручування у мобільних додатках

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

iOS: правильний паралакс без блокування головного потоку

Наївна реалізація через UIScrollViewDelegate.scrollViewDidScroll з оновленням frame або transform працює, але виглядає стрибкувато. Кожен кадр активує делегат на головному потоці, розраховує зміщення і оновлює макет. На iPhone SE 2nd gen при швидкому прокручуванні це може впасти до 45 FPS.

Правильний підхід у UIKit використовує CAScrollLayer або окремі трансформації CALayer. Найелегантніший варіант — UICollectionViewCompositionalLayout з orthogonalScrollingBehavior та supplementaryContentInsetsReference. Для простого паралаксу в ячейках:

override func layoutSubviews() {
    super.layoutSubviews()
    // Викликається при зміні bounds, включаючи прокручування collection view
    guard let superview = superview else { return }
    let cellFrameInSuperview = convert(bounds, to: superview)
    let parallaxOffset = cellFrameInSuperview.minY * 0.3
    heroImageView.transform = CGAffineTransform(translationX: 0, y: -parallaxOffset)
}

layoutSubviews виконується під час кожного pass макета, включаючи прокручування — це працює на головному потоці, але без зайвих делегатів чи DispatchQueue викликів. Коефіцієнт 0.3 означає 30% від зміщення прокручування. Зображення повинно розширюватися над ячейкою приблизно на cellHeight * parallaxRatio з кожної сторони.

У SwiftUI паралакс досягається через ScrollView та GeometryReader:

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            GeometryReader { geo in
                let offset = geo.frame(in: .global).minY
                Image(item.imageName)
                    .resizable()
                    .scaledToFill()
                    .frame(height: 250)
                    .offset(y: offset * 0.3)
                    .clipped()
            }
            .frame(height: 200) // видима область менша за зображення
        }
    }
}

GeometryReader читає позицію в глобальних координатах — це перераховується при кожному прокручуванні. На iOS 17+ використовуйте .scrollTargetBehavior та ScrollView з onScrollGeometryChange для кращої продуктивності.

Android: паралакс без затримок

Наївна реалізація через RecyclerView.OnScrollListener з view.translationY = offset * factor викликає аналогічні проблеми: слухач прокручування на головному потоці та непотрібні pass-операції виміру/макета.

Найкращий підхід — MotionLayout з OnSwipe скролл-триггером через NestedScrollView:

<MotionScene>
    <Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@id/nestedScrollView"
            motion:touchAnchorSide="top"
            motion:dragDirection="dragUp"
            motion:moveWhenScrollAtTop="true" />
    </Transition>
    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/heroImage"
            android:translationY="0dp" ... />
    </ConstraintSet>
    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/heroImage"
            android:translationY="-60dp" ... />
    </ConstraintSet>
</MotionScene>

MotionLayout керує анімацією на основі прогресу прокручування — зображення змінює позицію в міру прокручування контенту без викликів на головному потоці.

Для RecyclerView з паралаксом для кожної ячейки використовуйте RecyclerView.ItemDecoration з onDrawOver:

class ParallaxDecoration(private val factor: Float = 0.3f) : RecyclerView.ItemDecoration() {
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            val imageView = child.findViewById<ImageView>(R.id.heroImage) ?: continue
            val centerOffset = (parent.height / 2f) - (child.top + child.height / 2f)
            imageView.translationY = centerOffset * factor
        }
    }
}

onDrawOver виконується під час кожного draw pass — це ефективно, оскільки привʼязане до рендерингу, а не до скролл-подій.

Jetpack Compose

@Composable
fun ParallaxCard(item: Item) {
    val density = LocalDensity.current

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
            .clip(RoundedCornerShape(12.dp))
    ) {
        var offsetY by remember { mutableStateOf(0f) }

        Box(modifier = Modifier
            .fillMaxSize()
            .onGloballyPositioned { coordinates ->
                // Викликається при позиціюванні — використовуйте обережно
            }
        )

        // Використовуйте ScrollState через LazyListState
        // Реалізація через rememberLazyListState() + derivedStateOf
    }
}

Правильний паралакс у Compose використовує LazyListState та derivedStateOf:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    itemsIndexed(items) { index, item ->
        val itemOffset by remember {
            derivedStateOf {
                val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
                itemInfo?.let { (listState.layoutInfo.viewportEndOffset / 2f) - (it.offset + it.size / 2f) } ?: 0f
            }
        }
        Box(modifier = Modifier.height(200.dp).fillMaxWidth()) {
            Image(
                painter = painterResource(item.imageRes),
                contentDescription = null,
                modifier = Modifier
                    .fillMaxSize()
                    .graphicsLayer { translationY = itemOffset * 0.3f },
                contentScale = ContentScale.Crop
            )
        }
    }
}

graphicsLayer застосовує трансформацію на GPU без інвалідації макета — найефективніший спосіб у Compose.

Терміни

Паралакс для hero-зображення на одному екрані: половина дня. Паралакс у списку з багатьма елементами (RecyclerView / LazyColumn / LazyVStack): 1 день. Вартість розраховується індивідуально.