Beautiful LazyList(RecyclerView) Parallax Scrolling

Beautiful LazyList(RecyclerView) Parallax Scrolling

Jetpack compose LazyList (RecyclerView) scrolling with Parallax effect

Β·

4 min read

The parallax effect is nothing but moving 1 scene layer slower compared to the other faster-moving scene. When we see a fast-moving scene on top of a slow-moving scene it creates a beautiful parallax effect. In real life, we can find the parallax effect everywhere e.g. when you see the mountain from within a car, the mountain seems to move slowly and that is a parallax effect. We can leverage this idea and create such experiences in android for the list that scrolls.

We'll build this with jetpack compose LazyList but you can also do it for a normal scrollable list. Let's start

First, let's have a LazyList with some mock items

@Composable
fun ParallaxScrollingContent(lazyListState: LazyListState) {
    LazyColumn(state = lazyListState) {
        items(
            items = List(50) { index -> "Item number ${index + 1}" },
            key = { item -> item }
        ) { item ->
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    // adding background is important
                    .background(MaterialTheme.colors.surface)
                    .padding(horizontal = 16.dp)
            ) {
                Text(
                    text = item,
                    modifier = Modifier.padding(vertical = 8.dp),
                    style = MaterialTheme.typography.body1
                )
                Divider()
            }
        }
    }
}

Now we have got a list that has enough content and is ready for good scrolling. The next step is to add an image as the first item of LazyList for which we want to have a parallax effect.

@Composable
fun ParallaxScrollingContent(lazyListState: LazyListState) {
    LazyColumn(state = lazyListState) {
        item {
            Image(
                // add your image from drawable
                painter = painterResource(R.drawable.team_discussing),
                contentDescription = "first image",
                modifier = Modifier
                    .aspectRatio(16 / 9f),
                contentScale = ContentScale.Crop
            )
        }
        items(
            items = List(50) { index -> "Item number ${index + 1}" },
            key = { item -> item }
        ) { item ->
            // ....
        }
    }

We have added the first item as an image that scrolls with all the content below it at the same speed right, and that is what we have to change. We want to slow down the image's scroll speed so that it'll look different from other content.
For that, we'll have to have our implementation for its Y translation.

val firstItemTranslationY by remember {
        derivedStateOf {
            if (lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && lazyListState.firstVisibleItemIndex == 0) {
                lazyListState.firstVisibleItemScrollOffset * 0.6f
            } else {
                0f
            }
        }
    }

Since we only want to change the scrolling speed of the first item, Here we're checking if it's the first item and if the list has enough visible items using lazyListState.firstVisibleItemIndex and lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() respectively.

Then we take the item's scroll offset and multiply it by 0.6f.

Why multiply by 0.6f?

1 means no scroll, and 0 means the same scroll speed as others. It's up to you how much slower you want the item to scroll.

And then we'll plug this custom calculation into the first item's translationY using its Modifier#graphicsLayer , as shown below.

    item {    
            Image(
                // add your image from drawable
                painter = painterResource(R.drawable.team_discussing),
                contentDescription = "first image",
                modifier = Modifier
                    .aspectRatio(16 / 9f)
                    .graphicsLayer {
                        translationY = firstItemTranslationY
                    },
                contentScale = ContentScale.Crop
            )
        }

See it looks cool, right? 🀩

Okay so let's play more, and add fading effect too as it scrolls so that it looks more natural to the eyes. For that, we'll have to calculate the alpha for the first item based on its scroll and size.

val firstItemVisibility by remember {
        derivedStateOf {
            if (lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && lazyListState.firstVisibleItemIndex == 0) {
                1 - lazyListState.firstVisibleItemScrollOffset.toFloat() / lazyListState.layoutInfo.visibleItemsInfo[0].size
            } else {
                1f
            }
        }
    }

Again we'll be checking if it's the first item and if the list has enough visible items, then we'll divide the item's scroll offset by its size and subtract it by 1. Why because otherwise say if an offset is 0(initially it'll) then the item itself will be invisible πŸ™†β€β™‚οΈ

Now we'll plug this custom alpha calculation into the first item's alpha using its Modifier#graphicsLayer along with translationY

item {    
            Image(
                // add your image from drawable
                painter = painterResource(R.drawable.team_discussing),
                contentDescription = "first image",
                modifier = Modifier
                    .aspectRatio(16 / 9f)
                    .graphicsLayer {
                        alpha = firstItemVisibility
                        translationY = firstItemTranslationY
                    },
                contentScale = ContentScale.Crop
            )
        }

See now, how it looks πŸ₯Ή

That is it, it looks fantastic. Use it in your projects as it gives life to the lists.
The full code can be found on this gist: here

Please comment if you find something incorrect, inappropriate, or didn't understand. I'll try to address

Alright, thanks for reading. Spread if you liked 🌾

Β