Parallax scroll effect implementation in mobile app

NOVASOLUTIONS.TECHNOLOGY is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.
Development and support of all types of mobile applications:
Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1 servicesAll 1735 services
Parallax scroll effect implementation in mobile app
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    445

Parallax Scroll Effect Implementation in Mobile Apps

A parallax scroll effect occurs when background images move slower than content, creating an illusion of depth. This technique is commonly applied to product cards, hero sections, and user profiles. The difference between poor and excellent implementation isn't in the visual concept but rather in whether the animation runs on the main thread or not.

iOS: Correct Parallax Without Main Thread Blocking

A naive implementation using UIScrollViewDelegate.scrollViewDidScroll with frame or transform updates works but appears stuttery. Each frame triggers a delegate call on the main thread, calculates offset, and updates layout. On iPhone SE 2nd gen with fast scrolling, this can drop to 45 FPS.

The correct UIKit approach uses CAScrollLayer or separate CALayer transforms. The most elegant solution is UICollectionViewCompositionalLayout with orthogonalScrollingBehavior and supplementaryContentInsetsReference. For simple parallax in cells:

override func layoutSubviews() {
    super.layoutSubviews()
    // Called on bounds changes, including collection view scroll
    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 executes during every layout pass, including scroll—this runs on main thread but without unnecessary delegates or DispatchQueue calls. The coefficient 0.3 means 30% of the scroll offset. The image should extend above the cell by approximately cellHeight * parallaxRatio on each side.

In SwiftUI, parallax is achieved through ScrollView and 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) // visible area smaller than image
        }
    }
}

GeometryReader reads position in global coordinates—this recalculates with each scroll. On iOS 17+, use .scrollTargetBehavior and ScrollView with onScrollGeometryChange for better performance.

Android: Parallax Without Jank

A naive iOS implementation using RecyclerView.OnScrollListener with view.translationY = offset * factor causes similar issues: scroll listener on main thread and unnecessary measure/layout passes.

The best approach is MotionLayout with scroll-triggered OnSwipe through 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 manages animation based on scroll progress—the image shifts as content scrolls without main thread callbacks.

For RecyclerView with per-item parallax, use RecyclerView.ItemDecoration with 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 executes during each draw pass—this is performant because it's tied to rendering rather than scroll events.

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 ->
                // Called on positioning—use carefully
            }
        )

        // Use ScrollState through LazyListState
        // Implementation through rememberLazyListState() + derivedStateOf
    }
}

Correct parallax in Compose uses LazyListState and 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 applies transformation on GPU without layout invalidation—the most performant way in Compose.

Timeline

Parallax for a single-screen hero image: half a day. Parallax in a list with many elements (RecyclerView / LazyColumn / LazyVStack): 1 day. Cost is calculated individually.