At Exyte we like to challenge ourselves and implement complex design animations when we come across something we really like. Sometimes it becomes an article in our replicating series, and sometimes it ends up as a library. This time we found a great design by Yeasin Arafat @dribbble, and decided to replicate it for both iOS and Android to compare the ease of implementation in SwiftUI and Jetpack Compose. As usual, here’s an accompanying article on how we did it, this one for Jetpack Compose.
The main idea is to create an animated size and position change of a ball and indent.
First, let's look at the bar itself. In material3 design, we have such APIs as:
Example of using the API:
To animate the position change of the ball and the indent and place them to their correct locations, we need to know where the items (buttons) are located. The positioning information can be obtained from outside using
onGloballyPositioned, but we don't want to force the library user to write extra boilerplate code, so we arrange the elements ourselves and get their locations by using a custom layout. For each layout, you need a measure policy that defines how to place the elements. In this
fun, we just make the policy remembered to avoid unnecessary calculations. The callback is needed to maintain the position of the elements when they are lined up.
barMeasurePolicy we first measure the elements using
max.Width, and then place them and fill the array with their horizontal coordinates.
We store items’ data in
Afterwards, we calculate the offset of the selected item:
Now we identify the animation points for the ball and the indent, for this we have to consider is the shape of the bar. In the layout of NavBar we use the measurePolicy which was created above.
animateIndentShapeAsState creates a shape and animates its changes, and we use
graphicsLayer to apply the shape.
Let’s talk about the construction of the shape itself. First, we create a shape by implementing the
Let's take a look at the
addRoundRectWithIndent function - it creates a path. First, we add
IndentPath, and then we add other lines and arcs to build a rounded rectangular shape within the indent.
xOffset tells us the coordinates where the indent should be located. The whole
addRoundRectWithIndent function code is available here.
Let's examine the indent. The bezier curve points were provided by our designer, but they were only available for a specific size of the design. So, we develop a class that adjusts dimensions based on a given area - in other words, it scales the indent to the desired size of a given rectangle.
We have created interfaces that help to add different kinds of animations.
For indent position animations we created the
Let's go over the animations based on this interface:
Straight Indent Animation
In the code below we do the following:
- Set the vertical offset, the ball is placed slightly above the nav bar layout as per design.
- Animate the offset.
- Return produceState when the offset changes.
- Implement a helper function that assists in calculating the ball's offset.
Depth Change Animation We have also included an animation of the indent depth. It's a little more complicated because we have two animation steps.
- The indent depth decreases when the user leaves the current state of the navbar by pressing another button.
- The indent depth then increases when the user presses another button and creates a new state of the navbar. First, we declare the fraction which will be animated, and we specify the start and end points of the animation.
After this, the logic of the animation begins. In this case, when a new destination point (
targetOffset) appears, we need to start a new animation or change the points. We animate up to 2f because each of the animations will take 1 float value (from 0-1 to exit and 1-2 to enter). Let’s explore this a little further:
Let's look at the three different cases we’ve used above.
The first is
isExitIndentAnimating. It runs when the user has changed the destination point and the exit ("from") point has not yet finished its animation.
In this case, we simply change the destination point. And the animation ends at the new point with no problem.
Now consider a more complex case:
isEnterIndentAnimating. When the animation is already happening at the to point and at that moment a new
targetOffset value arrives (the user chooses a new item of the navbar). In this case, it is necessary to start reducing the depth of the to point and then start the animation at the new destination point.
We need to start a reverse height reduction animation in the "to" point. So, from the full fraction of this animation, we subtract the red fraction shown on the picture above and start a new animation from this point.
The last case,
isNotRunning, is when there are no animations currently available and we just need to start a new one.
Eventually we produce IndentRectShape state.
produceState is called every time the fraction and shapeCornerRadius change.
Ball and Its Animation
Now let's talk about the ball and its animation. For this, we use
ballAnimInfoState, which will be described later. Let's see how we make the ball and use it in a composable
ballTransform modifier we change its position and scale:
Now let’s talk about the animation. As with the indent, we have several predefined animation options and a basic
BallAnimInfo contains offset and scale information.
Straight ball animation
Now let's look at how straight animation works. We simply animate the offset and pass the
BallAnimInfo parameter, and that’s all there is to it.
Teleporting Ball Animation
The principle of the ‘teleport’ animation is no different from the indent height animation, we again place the “from" and “to” points in the same way, only the animation is different - in this case, we change the scale.
Parabola Ball Animation
The most interesting case is the parabola animation. For a more flexible and smoother arc trajectory, we use bezier curves.
Let’s specify the starting and ending points:
Establish the path and the necessary variables to measure them
As we discussed earlier, it is necessary to consider cases when the user selects a new item during ongoing animation. This piece of code takes place inside the
LaunchedEffect function, presented a bit later on. Here we measure where the ball is at the present moment and set those coordinates for the starting point (from). For the “to”, we set the coordinates of the new destination.
When the offset is changed,
LaunchedEffect is started and does the following:
- Calculates the new height for the trajectory.
- Sets the starting points - either if the animation is over, or if it’s currently running.
- Creates the path with the trajectory.
- Starts the animation.
Creating a correct offset when changing position,
Let's look at the trajectory. We use quadratic Bezier curves to create it:
А coordinate measurement function that sends the necessary information to the position array:
We have pre-made animation effects for the icons, and will explore them here. Two of them are included in the library and the third one is added as an example so that you too can create your own animated icons.
To implement the desired animation the button needs to grow a bit, while its insides wiggle around. The icon is drawn on the canvas, resized and has alpha applied as needed for the animation. We also use
graphicsLayer, because only when a new layer is created will the color blending mode be applied. On the canvas, we first draw a background icon, then draw a “wiggle” circle, and apply a
SrcIn blend mode. After that, we draw the outline icon. To make it easier to understand, here is a schematic illustration.
We have two different animations - one for wiggle and the other for entering/exiting (alpha and scale).
First, enter and exit animation:
Secondly, the wiggle animation:
Parameters for animation are
We change the parameters depending on the animations:
There are interpolators in the code, but there is nothing noteworthy about them, as we simply apply exactly the interpolations that help achieve the desired animation curve. For example, scale and radius animation:
The droplet button uses the same idea as before, except it’s even simpler this time. You can see the droplet button code here: DropletButton.kt. Let’s consider
ColorButtons, which are a bit more complicated.
Let's take a closer look at the animation: in each icon we have a background (colored figures), which is scaled and translated to the desired point from the direction the ball came from. Moreover, each icon has its own kind of animation.
To calculate where the background needs to go, we need to understand the direction where the ball comes from and where it goes.
The first case is for the situation when the ball comes to the point, and the second is when it leaves it. We should animate two backgrounds: the leaving and the arriving one.
Since both backgrounds need to be animated together, there are also two conditions: one for the outgoing background (bottom part of the picture) and one for the incoming background (top part of the picture)
Let's calculate the required offset:
The offset sign depends on the direction, and the value depends on the fraction.
Here we are drawing the background with the required scale and offset.
Let’s look at the bell animation icon. We have our fraction to animate rotation.
Drawing the icon:
To make it look like a pendulum is spinning we use
To rotate in the required anchor (from the top in the middle) we made an extension modifier:
Custom navigation bar animations in Android Jetpack Compose are a great way to enhance the user experience of your app. By introducing your own animations, you can create a unique look and feel that reflects your brand or app theme. You can customise the timing, duration and type of animations to achieve the desired effect. With the knowledge gained from this article, you can create custom animations for the navigation bar in your Android apps, or already use the ready-made library we've created.
Of course, the main complexity of this implementation lies in math and not in Kotlin code. But hopefully it shows how easy implementing complex animation can be in Jetpack Compose, and you can reuse these approaches for your custom UI. As always, the final solution is available as a library, and we hope to see you again soon for more libraries and tutorials!