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.
Bar
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.
In 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 itemPositions
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.
Shape
Let’s talk about the construction of the shape itself. First, we create a shape by implementing the Shape
interface:
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.
Animations
We have created interfaces that help to add different kinds of animations.
For indent position animations we created the IndentAnimation
interface:
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 fun
:
With 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 BallAnimation
interface.
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, ballSize
, fraction.value:
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:
Icons
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.
Wiggle 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.
ColorButtons
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)
Animating fraction:
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 degreesRotationInterpolation
:
To rotate in the required anchor (from the top in the middle) we made an extension modifier:
Conclusion
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!