Jetpack Compose Animations in Real Time

Halil Ozercan
ProAndroidDev
Published in
5 min readJun 11, 2021

--

https://nbasic.net/apps/particles.html

A while ago I saw this animation on /r/internetisbeautiful. I highly recommend you go check it out for yourself on your browser. It sure looks better in 60 fps and also way smoother in 120+ fps. Although there is nothing fancy going on, small details in the animation makes it quiet nice to watch. Naturally, I’ve decided to implement it using Jetpack Compose. I thought there would be fun challenges behind a simple animation like this when I dive into the implementation details. Initially my assumption was

  • Number of dots would complicate things performance wise
  • Line width should be inversely proportional to the distance between dots
  • High number of dots might cause a frame to lag above frame seconds limit.

The first concern turned out to be not a serious one thanks to realistically low(less than a million) number of dots and the ability of modern phones to run O(n²) algorithms like a kid’s toy.

The last concern is the biggest one in any heavy loaded animation. Games would never feel natural on a weak machine if they drew every frame no matter how long it takes to actually draw a frame. When a frame is late to the party, it should be dropped and game should resync with the wall clock. You can learn more about this at Wikipedia.

Let’s assume we have the perfect device that can calculate and draw every frame in a nanosecond. We can sketch our design and finally address this non-existent perfect machine at the end while drawing everything.

State

One of the foundational blocks of Jetpack Compose is the state. State describes a picture in time for the whole environment without any sense of continuity. Compose has other building blocks (Animation Library, coroutines) that help us give life to a state in a continuous timeline.

Given what kind of animation we are trying to achieve, it’s not that difficult to figure out what kind of information do we need to draw a single frame.

  • Dots (Position, Speed)

We will have more information in state as part of implementation detail and additional configuration options but we don’t need anything else for the basic mental version. All we need are the DOTS!!

A very basic structure of a Dot can be as follows

data class Dot(
val position: Offset,
val vector: Offset
)

I used Offset as vector because it offers a distance function that spares us from writing a simple square root operation…

Drawing the state on screen is going to require us to define a certain threshold where dots start a connection. This threshold does not have to be part of the internal state of animation because it will not interest us while calculating the next iteration of state. The distance threshold will only be useful while drawing.

Drawing the lines between dots

I’m omitting lots of code here that you can find in the original repository. See the bottom of this page.

Put the dots in the state

Essentially, we leave the state empty with no dots because we have no idea how big or small the drawing area is going to be. I’d rather have number of dots that is proportional to container size. Also, initial position of all these dots are going to be random. How can we randomize the position without knowing the boundaries first? Of course we can use ratios to a percentage but it would hinder our ability to calculate boundary collisions in the next steps. In this case, a very useful modifier reaches to our help:

onSizeChanged { newSize ->
dotsAndLinesState = dotsAndLinesState.sizeChanged(
size = newSize
)
}

You can and should use BoxWithConstraints for this task. However, original implementation is actually a background modifier written with composed modifier builder. I’ll use chained modifiers that consume a mutable state.

sizeChanged function that we define as an extension on DotsAndLinesState is designed to work continuously even when size changes unexpectedly, not just for the first size update.

findPopulation is left as an implementation detail. Original code binds it to another configurable option to easily control the population through a slider.

Next Frame

An immutable state that has information about dots helped us to draw a single frame. Now, it’s time for Compose to do its magic. If we wrap this state with remember { mutableStateOf { .. } } , every update to this state should notify consumers (a Canvas) to re-compose themselves.

var dotsAndLinesState by remember {
mutableStateOf(
DotsAndLinesState()
)
}

The most crucial part of making this animation right would be the calculation of next state. So, when will we have the next step? Whenever we can. We are not going to force our animation to some constant FPS value. Even if we did that, an older device might have trouble rendering 30 FPS with 100 dots animating at the same time. The actual answer to “when next step” question is exactly putting that when into the calculation as a parameter.

next function should mainly take care of 2 things;

  • Collision detection with borders
  • Find where the dot will land according to its vector

Note: This implementation completely ignores the case where speed is so high that the dot should actually bounce from both sides at the same time. Traveling at such speeds will never be possible in our system ;)

Introduction of durationMillis is going to be the key for smooth animation in any device with either low or high computation power. We simply follow newton’s physics in this iteration function.

Respecting the frame rate

Finally, we arrive at the main point of this article. Compose offers multiple Animation APIs that serve different needs from very simple position animations to complicated state transitions. You can learn more about these APIs from this awesome doc page.

I want to highlight an API that is even more fundamental that concerns individual frames. Without further ado

We start a LaunchedEffect from our composed modifier that runs as long as this modifier is part of the composition tree. Later, while(isActive) makes sure that next iteration runs indefinitely.

/**
* Awaits the next animation frame and returns frame time in nanoseconds.
*/
public suspend fun awaitFrame(): Long

awaitFrame waits the next frame and gives us the exact(nano) time that frame was drawn so that we can keep track of the wall clock and calculate our state iterations accordingly. We hold onto the result of awaitFrame at each iteration of outer loop because we are actually interested in the time between frames. We update our mutable state at the end of white loop block according to how many milliseconds have passed.

This gives us a very smooth and flexible animation that can handle 120 FPS devices as well as very old devices that cannot even produce 20 FPS for larger states (state iteration is O(n²)).

Implementation

Here you can find the original implementation that I repeatedly mentioned in the article

https://github.com/halilozercan/madewithcompose/tree/main/dotsandlines/src/main/java/com/halilibo/dotsandlines

Also, an older video that showcases the first implementation with multiple configuration options.

--

--