A parallax scrolling effect is always nice to look at and it catches the attention of the user. Using Jetpack Compose this effect can be implemented fairly easy.
Just another item
In the example we have an image on top of a list with text items. This could mean we have an Image followed by a LazyColumn, but when looking at the API LazyColumn we can not only call the items method and give it a list with data to create our items from.
val list = (0..1_000).map{ "Item $it" }.toList()
LazyColumn {
items(list) { item ->
Text(text = item)
}
}
We can also call multiple item and items methods in the scope of a LazyColumn. It is not limited to just having it call the items function.
For this means, we can use item to display the image, followed by items to create the list.
val list = (0..1_000).map{ "Item $it" }.toList()
LazyColumn {
item {
Image(
painter = resourcePainter(id = R.drawable.image),
contentDescription = "Top image"),
contentScale = ContentScale.Crop,
modifier = Modifier.fillParentMaxWidth()
}
items(list) { item ->
Text(text = item)
}
}
With that we already have a list with two types of items and of course right now they are scrolling in the same speed.
Not so fast
In order to achieve a parallax effect, we need to reduce the scrolling speed of the image, while keeping the scrolling speed of the other items untouched.
To influence the scrolling we need
- A way to get to scrolling information
- A way to manipulate the scrolling behaviour of the image.
For the scrolling information we can utilise LazyColumns parameter LazyListState, it contains the information we need. To get to it, instead of keeping the default, we create our own and pass it in.
val lazyListState = rememberLazyListState()
LazyColumn(state = lazyListState){
...
}
Derived from the LazyListState we can calculate a slower translation of our image along the y-axis, compared to the other items.
val firstItemTranslationY by remember {
derivedStateOf {
when {
lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && lazyListState.firstVisibleItemIndex == 0 -> lazyListState.firstVisibleItemScrollOffset * .6f
else -> 0f
}
}
}
We check if the image is visible on screen, if not we don't want to do anything and just say behave normally, by returning a translation of 0. As long as the image is visible we take the current scrolling offset and multiple it by a number between 1 and 0. The closer the number is to 1 the slow the image will move with the scrolling. For the parallax scroll effect we want to keep the image longer in the screen as it normally would be.
You can play around with that value to adjust the speed as you like. For this example let's keep it somewhere in the middle with 0.6.
Now we have the value to adjust our images scrolling. But where should be apply it? For that Compose offers a pretty handy Modifier that we can use. Modifier.graphicsLayer provides us with the possibility to manipulate scale, rotation, alpha, translation and more of a Composable it is applied used on.
The translation sounds like a good option. Let's apply Modifier.graphicsLayer to our image and set the vertical translation to the value we just calculated.
...
item {
Image(
painter = painterResource(id = R.drawable.image),
contentDescription = "Top image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillParentMaxWidth()
.graphicsLayer {
translationY = firstItemTranslationY
}
)
}
...
Just to play around a little more, let's also fade out the image slowly while it is scrolling.
val visibility by remember {
derivedStateOf {
when {
lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && lazyListState.firstVisibleItemIndex == 0 -> {
val imageSize = lazyListState.layoutInfo.visibleItemsInfo[0].size
val scrollOffset = lazyListState.firstVisibleItemScrollOffset
scrollOffset / imageSize.toFloat()
}
else -> 1f
}
}
}
The procedure is basically the same as before, the difference is instead of taking the offset of the first item we take its size and its scroll offset and calculate the alpha value.
When we apply it we subtract our calculated visibility from 1 and the image will be fully transparent when it leaves the screen.
.graphicsLayer {
alpha = 1f - visibility
translationY = firstItemTranslationY
}
Conclusion
With Jetpack Composes LazyColumn and Modifier.graphicsLayer it is really simple to create a parallax scrolling effect. Calculate the translation derived from the LazyListState and apply it to the Modifier.
The whole code for this can be found on GitHub.
I also added a fading TopBar to this example. I wanted to have the Back Arrow always visible and therefore ended up creating my own TopBar. If you have any ideas on how to make it work with the standard Compose TopBar, leave a comment or write me directly. I would like to hear about it :-).
Hope you enjoyed this small example on parallax scrolling.
See you in the next one 👋.
Top comments (0)