WWCode Talks Tech #20: Animating Your Way Into Jetpack Compose
Aida Issayeva, Software Engineer and Google Developer Expert for Android, shares her talk, “Animating Your Way Into Jetpack Compose.” She explains how animation improves the user experience and breaks down the different components of animation for Android.
Animations are essential in modern mobile apps to provide a smooth and delightful user experience. We need animations to grab the user's attention, send visual feedback, fill in wait time, and create a responsive, user-friendly interface. They're also fun, although too much animation is not so fun. In practice, animations are common solutions for masking slow responses from the backend. Animation helps build a great user onboarding experience and teaches users to be more patient with that. Developers can expect higher user conversion, better usability of the app, more patient users, and smooth, stable results implementing animation.
The benefits of having animation are obvious. It is hard to imagine mobile apps without them nowadays. Animations have come a long way in Android. Historically, introducing and maintaining them on Android has been challenging. In the traditional View UI system, running two different animation APIs simultaneously was not an easy task. For example, combining duration-based API, like object animator, and physics-based API, like spring animation, would be hard. It would require a lot of boilerplate code to get settled with it.
Defining animations in separate drawable resource files and calling them in the main code would cause a contact switch. Another challenge was requiring a boilerplate of code to start the animations. For example, introducing all the listeners, animation listeners, and the following functions that we needed to add to make sure our animation could start. A separate set of APIs in each component for the same animations also had to be considered. For example, transition animations from one screen to another.
In activities, we would override the pending transition function to set the animation between one activity to another. In fragment, we would use a set of custom animation functions to add that transition between one fragment to another. If we wanted to transition between elements in view groups like Recyclerview, linear layouts, or relative layouts, we would need to add XML Android attribute and animate layout changes. Because of these reasons and how the View UI system was set up, animations in traditional view-based systems were treated as a “nice to have feature” for many MVPs. It is still that way in a lot of Android apps today.
The Android ecosystem has always been in active development. In the last year, it introduced Jetpack Compose. Jetpack Compose is a modern Android UI toolkit. It focuses on building UIs declaratively. This distinction is important because the traditional XML-based style was imperative, and multiple generations of Android learned and passed on the imperative way of building UI. The main difference between declarative and imperative styles is that the imperative style focuses on how. It provides a step-by-step guide on how you would like to get your desired result. The declarative style focuses on what. You describe what result you would like to achieve, and the declarative approach removes a lot of boilerplate. Jetpack Compose also provides powerful and extensible APIs that make it easy to implement various animations. Many composed animation APIs are composable functions like the rest of the Jetpack Composed UI elements.
These APIs are divided into two groups, high-level APIs and low-level APIs. High-level APIs are easy to use, require minimum actions to start the animations, cover several common patterns, and align with the best practices of material design motion. It's important to note some of these APIs are still experimental. There's a possibility that in the future, they may get removed. Currently, there are seven high-level APIs. AnimatedVisibility, Crossfade, and AnimatedContent are containers. Inside those containers, we can pass content and additional composable functions like columns or rows. They hold the content inside. We also have a modifier extension function, AnimateContentSize, additional animateAsState, updateTransition, and transition functions.
All these composable functions are all you need to build animations in Android Jetpack Compose. Whether it's to use the default variations or customize that. If you want to animate the appearance and disappearance of the UI element, AnimatedVisibility is the API to use. Under the hood, it creates a custom layout for its content. It also comes with the default enter transition, a combination of the fadeIn and expandIn and the exit transition, shrinkOut and fadeOut. The compose function is a container. This composable function is a container and, by default, requires a content, composable and a parameter, visible that will define the visibility of the UI element. It's important to note, when this parameter's value, the visible parameter's value changes from true to false, then the content composable will be removed.
To use AnimatedVisibility, we would introduce AnimatedVisibility in our composable function, add the content, and pass the boolean type state. AnimatedVisibility is a composable container, where the content can have its own animations and transitions. The enter and exit transitions can be customized as well. In the original source code, we had the combination of fadeIn plus shrinkOut. If we want, we can decide to combine or pass our own custom transitions. If we have this custom enter and exit animations, animated visibility will wait until all the animation's completed and only then come to this idle completely finished state. Crossfade animation animates between two layouts. This composable requires content and target state parameters.
AnimationSpec stores specifications of the animation, including the data type that needs to be animated. AnimationSpec runs physics-based and duration-based animation simultaneously in Jetpack Compose. To use cross-fade animation, you call Crossfade anywhere inside the composition, add the content in and pass the state of any type. Just like AnimatedVisibility, Crossfade is a composable container that holds the content where each content can have its own animation. In Jetpack Compose v1.1.1, Crossfade is still experimental.
AnimatedContent is a container that automatically animates its content when target state changes. AnimatedContent is a more fluid, flexible and transformative version of the Crossfade. It's far more powerful. It requires two parameters, the content and targetState. The content for different targetStates is defined in mapping between the targetState and the actual content composable function. When targetState changes, content for both new and previous targetStates will be looked up through the content Lambda, which is a content. Then they will go through the contentTransform so that the new target content can be animated in while the initial content animates out. Once the contentTransform is finished, the outgoing content will be removed.
By default, the targetContent has delayed fadeIn animation and the outgoing content has delayed scaleIn and fadeOut animation. To use AnimatedContent, you would call AnimatedContent inside your composable functions. AnimatedContent is a high-level animation API. It's a composable container, not a function. Because it's a container, the content inside this container can have its custom animations as well. You can proceed and pass this transitionSpec parameter to define your completely customized animations. AnimatedContent is still experimental.
The modifier extension function animateContentSize animates its size when the child composable at the tail of the chain or the child modifier changes the size. By default, there is no required parameter to this function. We can add it to any modifier. For example, let's say we have a toggle list. Whenever a user clicks on one of the rows, the list gets expanded with more description. We can add animateContentSize to the modifier of our column. We can wrap the description to expand mutable state of type boolean that will tell us whether we need to expand or whether we need to display this description text or not. It changes to a true and false whenever the user clicks on the clickable modifier function. It will give us a smooth transition between expanding the list, toggling list, and then collapsing the toggle list. AnimatedContentSize is an expansion function, unlike other previous high-level APIs. It cannot contain the animations.
AnimateFloatAsState, animateColorAsAtate and others are under the umbrella of animateAsState function. This one is the simplest animation composable function for animating a single value. With animateAsState function there is no need to create an instance of any animation class or handle interactions, as it manages everything under the hood. AnimateAsState is a composable function that returns a state. It has support for nine data types. If we provide a two way converter it can support even custom data types. It also provides a finishedListener to observe the end of the animation. API is updateTransition composable function. It creates and remembers instances of the transition, and updates its state. Transition manages one or more animations and runs them simultaneously between multiple states. The required parameter is targetState. After initializing the transition, we can use one of those animate extension functions on the transition to define child animation in this transition. Like animateAsState, animate function supports different data types out of the box, a total of 10, with additional animateValue extension function.
To use updateTransition API, you would introduce a variable transition by calling updateTransition function and passing the targetState to it. We would create two other variables, background and alpha. We would call animateColor and animateFloat of transition on them. Inside those functions, we have specified the desired values of background and alpha, based on the result of the targetState. That's how we were able to use updateTransition animation API. UpdateTransition animation API is a function that returns transitions. It can create and manage multiple animations via animate extension function, where we can optionally define a custom transition specs. For more complex transitions involving multiple composable functions, we can use createChildTransition function and create a child transition.
The last high-level composable is rememberInfiniteTransition. This function creates and returns the instance of InfiniteTransition. InfiniteTransition can hold one or more animations. However, they start running as soon as they enter composition and won't stop until they're removed from the composition. InfiniteTransition, unlike updateTransition, can have only three animate functions, animateColor, animateFloat, and animateValue.
A lot of out of box animation APIs come with the default animations in them. However, we can customize them by passing our own arguments into animationSpec. Jetpack Compose provides different kinds of AnimationSpecs out of the box, like spring, tween, keyframes, snap, repeatable and infiniteRepeatable. Spring creates a physics-based animation between start and end values. It can handle interruptions more smoothly than duration-based animations. It guarantees this continuous velocity. It can take two parameters, dampingRatio and stiffness. Damping ratio defines how bouncy the spring should be. There are four levels of bounciness. The default one is DampingRatioNoBouncing. Stiffness defines how fast the spring should move toward the end value.
There are six predefined levels. The default one is StiffnessMedium. Easing allows an animating value to speed up and slow down rather than move constantly. In Android, easing is a function basically, it takes the value between zero and one and returns a float. The returned value can be outside the represented boundary, like the high bouncy. Compose provides several building easing functions that cover most use cases. On top of having custom easing variations available, we can specify our easing functions. Cubic-bezier.com is a great website to try and customize the values and compare your custom easing with the most popular easings available.
Keyframe animation animates based on the snapshot value, specifying the different timestamps in the duration of the animation. The animation value will be moved between two keyframe values at any given time. With keyframes, we can customize the speed of our animations at specified periods. AnimationSpec is a special animation. Some people might say it's not an animation, but it's an animation that immediately switches the value from start to end. We can also specify delay if we want to delay the start animation. Repeatable runs duration-based animation, just like tween and keyframes. It runs it repeatedly until the specified number of iterations.
You can repeat mode parameters to specify whether the animation should repeat by starting from the beginning, like restart, or from the end, like reverse mode. InfiniteRepeatable is like repeatable, but it repeats for an infinite amount of iterations. There is no iteration parameter required. We can also pass sort of an initial start offset that can be used either to delay the start of the animation or fast forward the animation at any given time. A way to customize animations is by passing an argument in the TransitionSpec parameter. TransitionSpec is responsible for creating a kind of animated transition between entering and exiting content, basically transforming the content. It can combine multiple AnimationSpecs in one transitionSpec.
Animatable depends on the AnimationState. The high-level APIs all extend animation class. Animation class should only be used to control the time of the animation manually. It is stateless. It does not have a concept of the life cycle. Jetpack Compose provides easy and flexible animations. They come in two levels, high-level and low-level. High levels are useful and practical in many scenarios and can be implemented with one and two lines of code. The low-level APIs give you further control and customization, although it also means that you have to manage the life cycle and interruptions. AnimationSpec will allow combined physics and duration-based animations to create a more natural experience. Many animation APIs come with default parameters for animation specifications and require a minimum number of parameters. With Jetpack Compose, adding animations to your Android app is fun, easy, and intuitive.