Реализация параллакс-эффекта при скролле в мобильном приложении

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-секциях, профилях пользователей. Разница между плохой и хорошей реализацией — не в визуальной идее, а в том, происходит ли это на main thread или нет.

iOS: правильный параллакс без main thread

Наивная реализация через UIScrollViewDelegate.scrollViewDidScroll + обновление frame или transform изображения работает, но дёргается. Каждый кадр — вызов делегата на main thread, расчёт offset, обновление layout. На iPhone SE 2nd gen при быстром скролле — просадка до 45 FPS.

Правильный подход в UIKit — через CAScrollLayer или UIScrollView с отдельным layer, привязанным через CALayer transform. Но самый элегантный вариант — 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 в ячейке вызывается при каждом layout pass, включая скролл — это работает на main thread, но без лишних делегатов и 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 читает позицию в глобальной системе координат — это recalculates при каждом скролле. На iOS 17+ лучше использовать .scrollTargetBehavior и ScrollView с onScrollGeometryChange — более performant API.

Android: параллакс без janк

На Android наивная реализация через RecyclerView.OnScrollListener + view.translationY = offset * factor даёт аналогичные проблемы: scroll listener на main thread, лишние measure/layout passes.

Лучший путь — 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 управляет анимацией на основе scroll progress — изображение смещается по мере скролла без callback на main thread.

Для RecyclerView с параллаксом в каждом item — RecyclerView.ItemDecoration c override 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 — это performant, так как привязано к рендерингу, а не к scroll event.

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 без invalidate layout — самый performant способ в Compose.

Сроки

Параллакс для hero-изображения на одном экране: полдня. Параллакс в списке с множеством элементов (RecyclerView / LazyColumn / LazyVStack): 1 день. Стоимость рассчитывается индивидуально.