Parallax effect made it simple with Jetpack Compose

Image for post
Image for post

I was recently playing with Jetpack Compose, learning the future of the UI on Android. In a sample app, I tried to build a parallax effect I implemented in previous applications. In a screen you have an image in the background that scrolls less than the content you can scroll. Let’s see how we can implement this animation coupled with a fade out of the image using Jetpack Compose.

Disclaimer: the code samples are based on Compose 1.0.0-alpha11. The API methods might change in a near future.

The Parallax Effect

The idea here was to add a parallax effect to the image while scrolling the content. The image will translate up as the content is scrolling up as well but the image velocity will be slower than the content. In addition, the image will fade out progressively while scrolling.

Image for post
Image for post
Parallax and alpha effect on the image while scrolling

With the current UI system of Android, based on the View component, this animation is possible by setting a OnScrollListener on a RecyclerView, or a OnScrollChangeListener on a ScrollView to get the current vertical scroll offset.

Getting the Y-axis scroll position for a RecyclerView

Then, you can compute the new alpha and the position on the Y axis in order to animate the view with the translationY and alpha attributes.

Now let’s see how this effect can be achieved with Jetpack Compose.

First attempts: the non optimized way

In a world of Functional Programming and states, paradigms are different than imperative and object-oriented languages. Jetpack Compose uses declarative programming paradigms to build UI components called Composable. To have a clear overview, have a look at the documentation “Thinking in Compose” for more details or see this post by Leland Richardson:

To achieve this parallax animation, a simple Composable screen is created with a ConstraintLayout, as our main container, an Image (here we are using CoilImage to display a remote image) and a Column (which holds the scrollable content).

To make the Column scrollable, you have to set the vertcalScroll(state) Modifier and pass a ScrollState in order to get the Y offset while scrolling. Then, in the CoilImage Modifier, you can access the alpha() (which takes a Float between 0 and 1) and the absoluteOffset() where you can set the X and Y offsets in DP.

With this implementation, when scrolling the content, a lot of frames were dropped and the scroll felt very laggy. It was not smooth at all as you can see below!

Image for post
Image for post
Choppy parallax animation

Afterwards, I looked at the Alignment attribute of CoilImage in order to play with the vertical bias of the image. But again, the scroll was super laggy.

alignment = BiasAlignment(0f, max(-1f, -scrollState.value / 300f))

The main reason there is a choppy animation is that every time a Modifier attribute is changed, it triggers the recomposition and relayout of the Composable — a very expensive operation. This means that every time we are receiving the scroll position from ScrollState the component is being redrawn.

So what is the best way to animate the component in real time with runtime State? Let’s have a look at the Modifier.graphicsLayer {} to optimize this effect.

Smooth parallax with graphicsLayer Modifier

If you take a look at the documentation of the Modifier interface in Compose, you have access to the Modifier.graphicsLayer {} of a Composable.

According the documentation, this modifier makes content draw into a draw layer and can be invalidated independently from the parent. So it prevents the whole recomposition and relayout of the component in order to have better performances.

You should use this lambda when you modify the layer properties (alpha, elevation, shape…) with runtime state values such as ScrollState or LazyListState for example.

And now the parallax effect is working like a charm! The animation is smooth without any dropped frames or laggy scroll. Here is a sneak peek of the final animation🤘🙂

You can achieve the same animation with a LazyColumn. You will need to set a LazyListState with rememberLazyListState(). Thanks to this state, you will be able to access the scroll offset of the first visible item (similar to the listener of a RecyclerView).

Jetpack Compose is a complete new way of thinking and implementing UI components after 10 years into the Android development. But it is a lot of fun at the same time, as it uses awesome functional programming paradigms. If you are animating the Composable content such as opacity, scaling or offset make sure to use Modifier.graphicsLayer {} especially if you are using runtime state values.

Thanks to Adam Powell for guiding me through the smooth and optimized way to animate Composables and understand some Jetpack Compose concepts. And big thanks to Wajahat Karim for reviewing this article 👏.

Enjoy the Compose life!

Senior Android Engineer @ Aircall (Paris) - Startup way of life, beer lover and world traveler.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store