Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

11. Animations
Written by Subhrajyoti Sen

Can you recall an app that was a pleasure to use? If so, it’s most likely because the app had great animations.

Animations are an excellent way to improve your app’s user experience. Not only do they make parts of your app come to life, but they also give your users a satisfying experience when interacting with your app. Animations make your app stand out.

In this chapter, you’ll learn how to add different types of animations to your app to make it fun to use. You will do so by:

  • Using Lottie to add complex loading animations without writing a single line of animation code yourself.
  • Using LottieFiles to find and play suitable frames in the animation.
  • Making an animated icon using Animated Vector Drawables.
  • Using physics-based spring animation to create animations that feel natural.
  • Using fling animation to let the user move a UI element with gestures.

You’ll start with an introduction to Lottie.

Lottie

Lottie is an animation library developed by the folks at Airbnb. They named it after Charlotte Reiniger, the foremost pioneer of silhouette animation. Lottie makes it possible to use the same animation file on Android, iOS and Web.

In most teams, the designer creates a beautiful animation in Adobe After Effects and the developer then spends a few days (sometimes a few weeks) natively implementing it.

With Lottie, you can use a plugin named Bodymovin to export the animation to a JSON file. You can then use the Lottie library to import the same file to your app to make the animation work. No extra animation code is needed.

Why use Lottie

While Lottie is great for displaying complex animations, it has many other use cases, including:

  • Walkthroughs: Apps generally use GIFs or videos to show feature walkthroughs. Lottie can do the same with a fraction of the file size.
  • Animated Icons: Lottie’s great for displaying animated icons based on user interactions. Although you can make animated icons with Animated Vector Drawables, Lottie supports a wider range of After Effect features. It can also control the animation progress based on user interactions, such as gestures.

Lottie has several advantages over other forms of animations as well:

  • Lottie animations scale well.
  • You can easily download the animation file over the network.
  • The same animation file works across all platforms.
  • It’s easy to loop between different frames of the animation.

Ready to dive in? Find out how to use Lottie next.

Setting up Lottie

Open the build.gradle for the app module and add the following dependency for Lottie:

implementation "com.airbnb.android:lottie:5.0.1"

Sync Gradle by clicking the Sync Now button.

Open the raw directory under the app resources. You’ll see two files named happy_dog.json and lazy_cat.json. These are your Lottie animations.

Figure 11.1 — Lottie Animation Files
Figure 11.1 — Lottie Animation Files

Now, from the layout directory, open fragment_details.xml and replace the ProgressBar view with the following code:

<com.airbnb.lottie.LottieAnimationView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/loader"
    app:lottie_loop="true"
    />

LottieAnimationView is responsible for loading the animation and applying various properties. When lottie_loop is enabled, it continuously loops the animation.

Now, you need to start playing the animation while the pet’s details load. Open AnimalDetailsFragment.kt in details.presentation and replace startAnimation with the following code:

private fun startAnimation(@RawRes animationRes: Int) {
    binding.loader.apply {
      isVisible = true
      setAnimation(animationRes) // 1
      playAnimation() // 2
    }
}

In this code you use:

  1. setAnimation to set the JSON file resource that you want Lottie to display
  2. playAnimation() to start playing the animation.

Next, update displayLoading by replacing displayLoading with the new startAnimation, like this:

private fun displayLoading() {
  startAnimation(R.raw.happy_dog) // HERE
  binding.group.isVisible = false
}

Also replace startAnimation() inside displayError, as in the following code:

private fun displayError() {
  startAnimation(R.raw.lazy_cat) // HERE
  binding.group.isVisible = false
  Snackbar.make(requireView(),
      R.string.an_error_occurred,
      Snackbar.LENGTH_SHORT).show()
}

Don’t forget that you need to add functionality to cancel the and hide the animations as well. Replace the current implementation of stopAnimation with the following:

private fun stopAnimation() {
  binding.loader.apply {
    cancelAnimation() // HERE
    isVisible = false
  }
}

cancelAnimation stops the Lottie animation.

Build and run. Click on any pet’s image to go to its details page. While the data loads, you’ll now see a happy dog animation.

Figure 11.2 - Lottie Loading Screen
Figure 11.2 - Lottie Loading Screen

Customizing the Animation

Lottie allows you to customize various properties of the animation like:

  1. Animation speed
  2. Fill color
  3. Start and end frames
  4. Repeat count and repeat mode

Consider a case where you want to use only a certain portion of the animation instead of the entire thing. With Lottie, you don’t need to go back to your designer and request changes. Instead, you simply specify the start and end frames of the animation.

For this app, you want to display only the part of the loading animation where the dog’s eyes are open.

Open this URL in a browser: https://lottiefiles.com/preview then upload the Lottie file named happy_dog.json to it.

Pause the animation and use the SeekBar to find the frame number where the dog’s eyes open.

Figure 11.3 — Lottie Animation Preview
Figure 11.3 — Lottie Animation Preview
In this case, the starting frame is around 50.

By dragging the SeekBar a bit more, you’ll find that the dog closes its eyes at frame number 113.

Figure 11.4 — Lottie Animation Preview At a Different Frame
Figure 11.4 — Lottie Animation Preview At a Different Frame

With that information, you know that you want to set the minimum frame to 50 and the maximum frame to 112. Also, you’ll set the animation speed to 1.2x because the default speed feels a bit slow

To do this, modify startAnimation, like this:

private fun startAnimation(@RawRes animationRes: Int) {
  binding.loader.apply {
    isVisible = true
    setMinFrame(50) // 1
    setMaxFrame(112) // 2
    speed = 1.5f // 3
    setAnimation(animationRes)
    playAnimation()
  }
}

In this code, you:

  1. Set the initial frame with setMinFrame.
  2. Set the final frame with setMaxFrame.
  3. Change the speed with the speed property.

Build and run. You’ll notice that only the selected part of the animation plays and that it plays at 1.5x the previous speed.

Customizing Other Animation Properties

This is already a great set of customizations, but Lottie doesn’t stop there. It lets you customize a wide range of properties of the animation. For example, you can modify the color of a single path in the animation. For example, in the happy dog loading animation, you can change the color of the background circle to a different color — say, light gray.

Try this out by opening happy_dog.json and searching for the icon_circle layer. This represents the background circle in the animation. For other animation files, you can ask the designer on your team to help you find the layer you need.

Change startAnimation adding the following code:

Add the following code to the end of startAnimation:

  private fun startAnimation(@RawRes animationRes: Int) {
    binding.loader.apply {
      // ...
    }
    binding.loader.addValueCallback( // 1
        KeyPath("icon_circle", "**"), // 2
        LottieProperty.COLOR_FILTER, // 3
        {
          PorterDuffColorFilter(Color.LTGRAY, PorterDuff.Mode.SRC_ATOP) // 4
        }
    )
  }

Here’s what’s going on in the code:

  1. You use addValueCallback to add a callback to the Lottie animation that returns a custom color filter for the layer you want to modify.
  2. To do this, you need to pass the layer as the first parameter using a KeyPath. You create a KeyPath, passing its name as the first parameter and a regular expression that filters layers with the same name. In this case, you use a wildcard, **
  3. The second parameter for addValueCallback is the property of the layer you want to change. In this case, you want to change its color using LottieProperty.COLOR_FILTER.
  4. Finally, you set the new value that, in this case, is a ColorFilter using a lambda.

Build and run. Go to the details screen and notice that the color of the animation background has changed from light yellow to light gray, as in Figure 11.5.

Figure 11.5 — Change the color of a layer
Figure 11.5 — Change the color of a layer

You’ve now successfully added a Lottie animation to your app and even customized it without having to go to your designer for help.

Animated Vector Drawables

Android uses Vector Drawables to display scalable images in your app. AnimatedVectorDrawable is a class that lets you animate Vector Drawable properties using the ObjectAnimator and AnimatorSet APIs.

Traditionally, AnimatedVectorDrawable runs on the UI thread. Starting from API level 25 (Android 7.1), however, it runs on the RenderThread. This has the advantage that, even if there’s jank in the UI because of long-running work taking place in the UI thread, AnimatedVectorDrawable will continue to run smoothly.

It’s also interesting to note that Lottie animations run on the UI thread. That means that in some cases, it’s beneficial to use AnimatedVectorDrawable over Lottie animations.

There are two ways to define the animations:

  1. Define VectorDrawable, AnimatedVectorDrawable and ObjectAnimator in three separate XML files.
  2. Define everything in a single XML file.

The first approach is preferable because it makes it easy to reuse animations and Vector Drawables across multiple views. In this book, you’ll use the first approach.

Consider an example of a gesture that lets the user “like” a pet. When the user double-taps the pet’s image, an outline of a heart fades in and starts filling up. Once the heart is full, it fades away.

Creating the Vector

You’ll start by drawing the heart shape. Create a file named ic_heart_unfilled.xml in the drawable directory and add:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:name="heart"
    android:width="24dp"
    android:height="24dp"
    android:alpha="0"
    tools:alpha="1"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <group
      android:pivotY="12"
      android:pivotX="12">
    <path
        android:fillColor="#ff1744"
        android:pathData="M 16.5 3 C 14.76 3 13.09 3.81 12 5.09 C 10.91 3.81 9.24 3 7.5 3 C 4.42 3 2 5.42 2 8.5 C 2 12.28 5.4 15.36 10.55 20.04 L 12 21.35 L 13.45 20.03 C 18.6 15.36 22 12.28 22 8.5 C 22 5.42 19.58 3 16.5 3 Z M 12.1 18.55 L 12 18.65 L 11.9 18.55 C 7.14 14.24 4 11.39 4 8.5 C 4 6.5 5.5 5 7.5 5 C 9.04 5 10.54 5.99 11.07 7.36 L 12.94 7.36 C 13.46 5.99 14.96 5 16.5 5 C 18.5 5 20 6.5 20 8.5 C 20 11.39 16.86 14.24 12.1 18.55 Z"
        android:strokeWidth="1" />
    <clip-path
        android:name="heart_mask"
        android:pathData="M 12 21.35 L 10.55 20.03 C 5.4 15.36 2 12.28 2 8.5 C 2 5.42 4.42 3 7.5 3 C 9.24 3 10.91 3.81 12 5.09 C 13.09 3.81 14.76 3 16.5 3 C 19.58 3 22 5.42 22 8.5 C 22 12.28 18.6 15.36 13.45 20.04 L 12 21.35 Z" />
    <group
        android:name="circle"
        android:translateY="17">
      <path
          android:fillColor="#ff1744"
          android:pathData="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 14.651 22 17.196 20.946 19.071 19.071 C 20.946 17.196 22 14.651 22 12 C 22 9.349 20.946 6.804 19.071 4.929 C 17.196 3.054 14.651 2 12 2 Z"
          android:strokeWidth="1" />
    </group>
  </group>
</vector>

The above vector draws an unfilled heart, which will be the starting state of the animation. It has an opacity of 0 because the icon will initially be invisible, then fade in. tools:alpha="1" lets you see the icon in Android Studio’s preview. You can use the Design view in Android Studio for a preview of the image, as Figure 11.6 shows:

Figure 11.6 — The Heart Vector Drawable
Figure 11.6 — The Heart Vector Drawable

The vector also has a circle that’s initially placed below the heart so it’s not visible. The aim of the animation is to gradually move this circle up so it gives the illusion of the heart filling up. The part of the circle outside the heart isn’t visible to the user because of the clip-path defined in the vector.

Creating the Animations

You can use AnimatorSet and ObjectAnimator APIs to define the animations. For this animation, you’ll use both. You’ll create the fading animation first.

Create an animator resource directory under the res folder. Then create a file called animator_alpha in the animator directory and add:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">

  <objectAnimator
    android:duration="400"
    android:interpolator="@android:interpolator/linear"
    android:propertyName="alpha"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType" />

  <objectAnimator
    android:duration="200"
    android:interpolator="@android:interpolator/linear"
    android:propertyName="alpha"
    android:startOffset="100"
    android:valueFrom="1"
    android:valueTo="0"
    android:valueType="floatType" />
</set>

The above XML defines two animations that run sequentially. The ordering attribute specifies whether the animations execute in parallel or in sequence. The first animation animates the value of the alpha property from 0 to 1 over a duration of 400 milliseconds. It uses linear interpolation, which means the rate at which the property value changes is constant for the entire duration.

The second animation also changes the alpha property, but from 1 to 0 and it takes 200 milliseconds. startOffset defines the time after which the animation starts. So this animation set will take 400 + 100 + 200 = 700 milliseconds in total to complete.

Now, you have to create the animation for the part where the circle moves up the heart. Create a file called animator_heart_fillup.xml in the animator directory and add the following XML:

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:interpolator="@android:interpolator/accelerate_cubic"
    android:propertyName="translateY"
    android:startOffset="100"
    android:valueFrom="17"
    android:valueTo="0"
    android:valueType="floatType" />

This animates translateY from a value of 17 to 0 over a duration of 250 milliseconds. It uses an accelerate_cubic interpolator, which means that it will use a cubic function to accelerate the rate of change of the values.

Defining the Animated Vector

Create a file called heart_fill_animation.xml inside the drawable directory and add:

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_heart_unfilled">
  <target
    android:name="circle"
    android:animation="@animator/animator_heart_fillup" />

  <target
    android:animation="@animator/animator_alpha"
    android:name="heart"/>
</animated-vector>

This file does two important things: First, it specifies the Animated Vector Drawable using the drawable attribute. It then specifies that the animator_alpha animation has to be applied to the vector constituent named heart and the animator_heart_fillup animation to the constituent named circle. It will throw an exception if it can’t find the strings mentioned in the name attribute in the specified vector.

Playing the Animation

Open fragment_details.xml and add the following attribute to the ImageView with the ID heart_image:

app:srcCompat="@drawable/heart_fill_animation"

This is a reference to the AnimatedVectorDrawable you just created.

Open AnimalDetailsFragment.kt and add the following to the onDoubleTap callback inside doubleTapGestureListener:

(binding.heartImage.drawable as Animatable?)?.start()

Here, you’re getting a reference to the Drawable you just assigned to the ImageView. You know it’s an AnimatedVectorDrawable that implements the Animatable interface that abstracts everything that’s possible to animate. You then cast the AnimatedVectorDrawable to Animatable and invoke start on it to start the animation.

And that’s it. Your AnimatedVectorDrawable is good to go. Build and run, then go to any pet’s details page and double-tap the image. You’ll now see a nice heart animation, showing your love for the cute pet.

Figure 11.7 shows an intermediate state of the animation:

Figure 11.7 — One Frame of the Heart Animation
Figure 11.7 — One Frame of the Heart Animation

Physics-based Animations

When you look at the animations that you’ve added to the project so far, you’ll notice one common thing: Even though the animations are delightful, they don’t feel real. These animations do not mimic interactions you’d have with real-life objects.

One way to significantly improve the user experience is to add physics-based animations. These animations follow the laws of physics, which makes them seem more natural and relatable to the user. Physics-based animations help you do this without having to worry about a lot of math.

In this chapter, you’ll implement two kinds of animations:

  1. Fling animation
  2. Spring animation

You’ll use the Jetpack DynamicAnimation library to create these animations. Open the build.gradle for the app module and add the following library declaration:

implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"

This gives you access to the library to use in the following animations.

Spring Animation

Spring animations give a bouncy feel to objects. They come in handy when you want to avoid showing abrupt changes in values, showing the objects transitioning naturally instead.

Consider a bouncing basketball. With each bounce, the height that the ball reaches reduces until the ball eventually comes to a halt. Springs work in a similar way. You initially stretch them to a certain length, then release them. They repeatedly expand and contract but the expansion keeps reducing until it stops in the contracted state.

When you open any pet’s details screen, one of the main actions you want the user to do is to call the organization about the pet. To draw attention to that action, you’ll add a bouncy animation to increase the size of the Call button, which the arrow in Figure 11.8 points to:

Figure 11.8 — The Call Button
Figure 11.8 — The Call Button

Before you start writing the spring animation, you need to learn about SpringForce. Every spring animation uses the concept of a virtual spring. Such a spring has two properties:

  1. Damping Ratio: Determines how quickly the values change over time.
  2. Stiffness: Sets the force with which the objects — or views — move.

Now, you’re ready to start. Open AnimalDetailsFragment.kt in the details.presentation package and add the following code before onCreate.

private val springForce: SpringForce by lazy {
  SpringForce().apply { // 1
    dampingRatio = DAMPING_RATIO_HIGH_BOUNCY // 2
    stiffness = STIFFNESS_VERY_LOW // 3
  }
}

In this code, you:

  1. Create an instance of SpringForce that encapsulates the property of the spring animation you want to apply.
  2. Set dampingRatio, which describes how oscillations in a system decay after a disturbance. In this case, you use DAMPING_RATIO_HIGH_BOUNCY, an existing constant for a damping ratio that makes a very bouncy spring.
  3. Set the stiffness, assigning the existing value STIFFNESS_VERY_LOW. The stiffer a spring is, the more force it applies to the attached object when the spring is not at the final position.

To increase the button size, you increase the button’s scaleX and scaleY properties.

In the same AnimalDetailsFragment.kt file, add the following code before onCreate:

private val callScaleXSpringAnimation: SpringAnimation by lazy {
  SpringAnimation(binding.call, DynamicAnimation.SCALE_X).apply {
    spring = springForce
  }
}

private val callScaleYSpringAnimation: SpringAnimation by lazy {
  SpringAnimation(binding.call, DynamicAnimation.SCALE_Y).apply {
    spring = springForce
  }
}

This creates two SpringAnimation instances for the scaleX and scaleY properties, respectively, and sets springForce as their spring.

In fragment_details.xml, look at the attributes of the FloatingActionButton named call. Notice that its scaleX and scaleY attributes are set to 0.6. The animation will work by increasing the values of these attributes from 0.6 to 1.0.

Go back to AnimalDetailsFragment.kt and add the following code at the end of displayPetDetails:

callScaleXSpringAnimation.animateToFinalPosition(FLING_SCALE)
callScaleYSpringAnimation.animateToFinalPosition(FLING_SCALE)

The code calls animateToFinalPosition with the FLING_SCALE to start both the spring animations. You set the value of FLING_SCALE to 1.0 at the beginning of the class.

Build and run. Click on any pet’s image to go to the details screen. You’ll notice that, right after the details of the pet become visible, the Call button bounces and increases in size.

Well done. You’ve successfully added a realistic animation to your app.

Fling Animation

Consider an example of a user flicking a coin. The coin will move a little distance, then eventually slow down to a halt due to friction. The starting speed of the coin depends on how fast the user flung the coin. Fling animations help mimic this effect.

Wouldn’t it be fun if there was an Easter egg somewhere in the app? How about showing a cute doggy picture if the user flings the Call button and the button stops on the pet’s image?

Similar to the spring animation, you’ll need two separate fling animations to accomplish this: one to change the x position of your view and another to change the y position.

Open AnimalDetailsFragment.kt in the details.presentation package, and add the following code before onCreate.

private val FLING_FRICTION = 2f

private val callFlingXAnimation: FlingAnimation by lazy {
  FlingAnimation(binding.call, DynamicAnimation.X).apply { // 1
    friction = FLING_FRICTION // 2
    setMinValue(0f) // 3
    setMaxValue(binding.root.width.toFloat() - binding.call.width.toFloat()) // 4
  }
}

private val callFlingYAnimation: FlingAnimation by lazy {
  FlingAnimation(binding.call, DynamicAnimation.Y).apply { // 1
    friction = FLING_FRICTION // 2
    setMinValue(0f) // 3
    setMaxValue(binding.root.height.toFloat() - binding.call.width.toFloat()) // 4
  }
}

In this code, you:

  1. Use the FlingAnimation constructor, passing references to which View you want to animate and which of its properties to animate.
  2. Set the FlingAnimation’s friction. The greater the friction is, the sooner the animation will slow down. In both cases, you use the existing FLING_FRICTION, which has the value 2.0. That means that it takes a bit of effort to fling the button onto the image.
  3. Use setMinValue to set the initial value to 0.
  4. Set the end value using setMaxValue.

You’ve defined two FlingAnimation — now, you can use them.

Detecting a Fling

Now that you have your animations ready, you need a way to detect the fling gesture so you can start the animations. You’ll use a GestureListener to detect fling gestures.

In AnimalDetailsFragment.kt, in the details.presentation package, add the following code inside displayPetDetails:

val flingGestureListener = object: GestureDetector.SimpleOnGestureListener() { // 1
  override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, // 2
    velocityY: Float): Boolean {
    return true
  }

  override fun onDown(e: MotionEvent) = true // 2
}
val flingGestureDetector = GestureDetector(requireContext(), flingGestureListener) // 3

binding.call.setOnTouchListener { v, event ->
  flingGestureDetector.onTouchEvent(event)
}

In this code, you:

  1. Create GestureDetector.SimpleOnGestureListener. This is an interface GestureDetector provides to listen for specific events like double-taps or, in this case, flings.
  2. The GestureDetector.SimpleOnGestureListener requires you to implement onFling and onDown. The former is called when a fling gesture happens, the latter when a tap occurs. The return values tell if the events have been consumed or if they can propagate to other components.
  3. To recognize the specific gesture, you need a GestureDetector. Here, you create one using Context and flingGestureListener, which you just created.
  4. Finally, you bind the event on the Call button to the GestureDetector

You call onFling whenever the user performs a fling gesture. velocityX and velocityY represent the x and y velocities of the fling. You’ll need this information to start the animations.

Starting the Fling Animation

When a fling gesture happens, you have to start the animations. Add the following code inside onFling, which becomes:

// ...
val flingGestureListener = object: GestureDetector.SimpleOnGestureListener() {
  override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float,
                       velocityY: Float): Boolean {
    callFlingXAnimation.setStartVelocity(velocityX).start() // 1
    callFlingYAnimation.setStartVelocity(velocityY).start() // 2
    return true
  }

  override fun onDown(e: MotionEvent) = true
}
// ...

In this code, you:

  1. Use setStartVelocity to set the velocity that starts the fling animation. You get this value from the onFling gesture callback.
  2. Start the animation using start.

Build and run. Open the details screen for any pet and try flinging the Call button. You’ll notice that the button moves in the direction of the fling and then starts slowing down.

Listening for the Animation’s End

To show the secret image, you need to check if the Call button overlaps the image when it stops moving. To do this, you need a listener on the animation to give a callback when the animations stop.

In AnimalDetailsFragment.kt, add the following at the end of displayPetDetails:

callFlingYAnimation.addEndListener { _, _, _, _ ->
  if (areViewsOverlapping(binding.call, binding.image)) {
    val action = AnimalDetailsFragmentDirections.actionDetailsToSecret()
    findNavController().navigate(action)
  }
}

This adds an end listener to the y-fling animation. areViewsOverlapping is a helper method that checks if two views overlap. You use it to check if the Call button overlaps the image. If it does, start a new fragment to show the secret image.

Build and run the app. On the details page, if you fling the Call button hard enough that it stops on the image, you’ll see a cute doggy picture.

Figure 11.9 — Call Button Fling Animation
Figure 11.9 — Call Button Fling Animation

Congratulations! You’ve now seen how easy it is to add next-level animations to your app, giving the user a better overall experience.

Key Points

  • Animations make your app stand out and leave an impression on the user.
  • Lottie is great for complex animations and can be highly customized.
  • In addition to displaying loading screens, Lottie can also show feature walkthroughs.
  • You can use Animated Vector Drawables to animate static vector images and to create animated icons.
  • Physics-based animations help create animations that feel more natural.
  • Spring animations can create bouncing animations.
  • Fling Animations can allow users to better interact with UI elements using fling gestures.

Great! In this chapter, you learned a lot about Lottie and physics-based animations. In the next chapter, you’ll learn how to use MotionLayout and the new Motion Layout Editor.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.