Android Jetpack Compose Cool Status Bar Collapsable Parallax image effect

Hello everyone. In this post I will show you how to make this cool effect BUT it’s not just for the effect itself, instead it’s for the thing you will learn while making it. We will learn couple of Jetpack Compose and Kotlin concepts.
Everything you need will be provided bellow and in gists.
In this article I will assume that you have basic Compose skills and you can set up Compose project by yourself.

First, let’s take a look at what our end result is:

What do we have here?

  1. Parallax effect on image.
  2. Status bar matching dominant color of the top most section of the image.
  3. Button to change the image.
  4. Large content that needs to be scrolled.

We will start by adding images to our project. This is the list I’ve used:

Gist

Put this in a separate file.

Now we will create our core layout:

Gist

For our initial layout am using Coil and Accompanist library in order to use Coil in Compose code but don’t bother adding it to your project because we will be removing it in next step.

As you can see scrolling is not working. Luckily for us Compose is great and scrolling is pretty simple to add using Modifier.verticalScroll(), L21, 27:

Gist

Now we will extract dominant color of the image top most section using Palette library and it’s KTX version. We will move that method to separate file:

Gist

As you can see it is a suspend fun.

  1. suspend fun allows us to use async functions as sync, “normal”, ones.Palette offers sync and async methods for getting palette from the given image. .generate { palette -> ... } is async one. We convert it to sync one using suspendCancellableCoroutine { ... }.
  2. We want to run this inside coroutine.

You will see why we return Pair<Color, Boolean> in a second.

Now is the time to remove Accompanist Coil library and start using Coil directly because Accompanist doesn’t allow us to access loaded image, it overrides Target and there’s nothing we can do about it.

Loading image with Coil has 3 steps to it:

  1. Defining Target. Target allows us to obtain loaded image and do something with it.
  2. Create request for loading the image. This is where we define what (url), how and where (Target) to load an image.
  3. Load image with the Loader.

Let’s define our Target :

Gist

This way we separate our code in Compose idiomatic way and we also make it more readable.

Now let’s define our Request:

Gist

allowHardware(false) is a must, otherwise app will crash.

We use remember {} with url as key because we want to get new request when url changes, new url = new image.

And it’s time to use all of this!!!

Let’s create separate @Composable and wire all of this there:

Gist

There is quite a bit going on here so let’s pause to explain.

  1. We are tracking image as MutableState so our code will recompose and update when we have new image.
  2. We are initializing Loader using LocalContext CompositionLocal to obtain context .
  3. We are changing image that needs to be shown inside target. When image is loaded we get it in our callback and we set new image to ourimage state variable. Image composable is displaying that image, L33, and it’s updated every time image changes.
  4. We trigger loading of the image inside LaunchedEffect block, L26. LaunchedEffect runs on composition OR when key/keys change. We pass url as our key which means that every time url changes LaunchedEffect will run and load new image.

We’ve come long way but there is still main things left, parallax effect and Status Bar color.

Let’s set Status Bar color first because that’s simpler and pretty easy one. For that we will use one more of the Accompaniest libraries -> System UI Controller.

You can add it to your project like this:

implementation "com.google.accompanist:accompanist-systemuicontroller:<version>"

Latest Accompaniest version as of time of writing this is 0.10.0.

Now we will use our computeDominantTopSectionColor() function inside our Target.

We will obtain System UI Controller with rememberSystemUiController() function. System UI Controller allows us to set system bars colors, Navigation Bar and Status Bar. Right now we need to change just Status Bar color.

Setting Status Bar color has two parts to it:

  1. Setting the color.
  2. Telling it do you need it to behave as dark or light. It being dark or light changes color of status bar icons. dark one sets icon color to white, light one is setting it to black. We don’t want neither dark Status Bar with black icons nor light Status Bar with white icons. In both cases user can’t see Status Bar content so we want to contrast them.

As I’ve already said it’s pretty easy and changes are minor:

Gist

Now what all have been waiting for. Parallax effect of the image that’s based on scrolling.

This is a bit harder than setting Status Bar color but nothing to be afraid of.

What do we need?

  1. Color that we’ve already computed in the last step, we just need to use it now.
  2. Scroll position in order to calculate overlay (parallax) opacity.
  3. Drawing of the overlay (parallax).

Drawing is expensive and taxing operation to do especially when combined with compose/recompose mechanism. Imagine drawing entire view with overlay on slightest scroll. That screams “UI junk!”. Luckily for us Compose gives us the way to draw overlay and modify it without recomposing entire composable, just the overlay. That’s huge performance benefit. We do that with Modifier.graphicsLayer {}.

Gist

Let’s see what changed.

  1. We have new parallaxColor state variable, L32to keep track of dominant top section color of the image and we use it as a background of the Image, L55.If we don’t do that default will be used and it’s black. graphicsLayer {} animates that background color. parallaxColor is assigned in Target callback L41.
  2. We have added CoroutineScope in order to run computeDominantTopSectionColor(), L39-40. Also, it wont work at all if you don’t run it inside coroutine.
  3. We have added graphichLayer {} Modifier to our Image. It gives us access to properties like shape, clip, scale, rotation etc. but we will use alpha for our use case. alpha is the alpha of the overlay (parallax). alpha can be in range of 0 to 1. We calculate the alpha as minimum of 1f which is the max value, fully opaque and we can’t see the image, and 1 — (scrollState.value / 400f) which gives us alpha value that follows scroll gesture. L56. This gives us fully opaque parallax when 2/3 of the image is scrolled of the screen and 1/3 of it is still visible. This looks better to me but feel free to experiment with this and find your sweet spot.

And that’s it! I hope you had fun and learned something new. This gives you nice base for creating something like CollapsableToolbar or moving our current implementation to LazyColumn. Am not going to do that in current post but if you have questions about that please leave a comment and I will be happy to help you! Also ask whatever you need help with and I will answer as soon as I can.

Full example will be available here.

Take care.

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