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?
- Parallax effect on image.
- Status bar matching dominant color of the top most section of the image.
- Button to change the image.
- Large content that needs to be scrolled.
We will start by adding images to our project. This is the list I’ve used:
Put this in a separate file.
Now we will create our core layout:
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
:
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:
As you can see it is a suspend fun
.
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 usingsuspendCancellableCoroutine { ... }
.- 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:
- Defining
Target
.Target
allows us to obtain loaded image and do something with it. - Create request for loading the image. This is where we define what (url), how and where (Target) to load an image.
- Load image with the
Loader
.
Let’s define our Target
:
This way we separate our code in Compose idiomatic way and we also make it more readable.
Now let’s define our Request
:
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:
There is quite a bit going on here so let’s pause to explain.
- We are tracking image as
MutableState
so our code will recompose and update when we have new image. - We are initializing
Loader
usingLocalContext
CompositionLocal
to obtaincontext
. - 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 timeimage
changes. - We trigger loading of the image inside
LaunchedEffect
block,L26
.LaunchedEffect
runs on composition OR whenkey/keys
change. We passurl
as our key which means that every timeurl
changesLaunchedEffect
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:
- Setting the color.
- Telling it do you need it to behave as
dark
orlight
. It beingdark
orlight
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:
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?
- Color that we’ve already computed in the last step, we just need to use it now.
- Scroll position in order to calculate overlay (parallax) opacity.
- 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 {}
.
Let’s see what changed.
- We have new
parallaxColor
state variable,L32
to keep track of dominant top section color of the image and we use it as a background of theImage
,L55
.If we don’t do that default will be used and it’s black.graphicsLayer {}
animates thatbackground
color.parallaxColor
is assigned inTarget
callbackL41
. - We have added
CoroutineScope
in order to runcomputeDominantTopSectionColor()
,L39-40
. Also, it wont work at all if you don’t run it inside coroutine. - We have added
graphichLayer {}
Modifier
to ourImage
. It gives us access to properties likeshape
,clip
,scale
,rotation
etc. but we will usealpha
for our use case.alpha
is thealpha
of the overlay (parallax).alpha
can be in range of0
to1
. We calculate the alpha as minimum of1f
which is the max value, fully opaque and we can’t see the image, and1 — (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.