7.
Basic List Animations
Written by Filip Babić
It’s hard to show everything your app offers on one static screen. Instead, most developers use a dynamic list of data to display items on demand — making these dynamic lists the most common UI type in mobile apps. Because they’re so common, it’s important to know how to animate dynamic lists.
Animating the items on the list is an opportunity to give your users useful information about what’s happening with that data. For example, you can add animations when items initially appear on the screen or when they’re added, moved or removed. This concept, where you use motion on the screen to give more meaning to your users’ actions, is called meaningful motion. Whenever you add animations to your app, their purpose should always be to give the app more meaningful motion.
In this chapter, you’ll learn about the animations you can apply to your list of movies. More precisely, you’ll learn:
- How to write simple XML animations, then apply them to list items as layout animations to animate those items when they appear onscreen.
- What the
ItemAnimatorAPI is and how to use it to create, add, remove and move the list items’ animations. - How to use the
DiffUtilandListAdapterAPIs to emit smart data set changes.
Now, you’ll dive right in by learning about layout animations in lists!
Getting started
To begin, use Android Studio Arctic Fox or newer to open the starter project folder within 07-basic-list-animations in the aat-materials repository. Once the project syncs, build the app. You’ll see the login screen.
Layout animations
Layout animations are basic animations that run whenever you have an XML layout on the screen.
These animations only run the first time the ViewGroup appears on the screen, which makes them useful not just for list items, but also for static elements. However, because you usually show many list items, their effect is more visible in dynamic UI elements.
You’ll use the same approach as you did in Chapter 1, Value & Property Animations”, where you created small XML files to animate the UI.
Building layout scale animations
For your first step, you’ll build a scale-up animation for your list items.
Create a new file in the anim package by right-clicking the res folder in the project structure and choosing New ▸ Android Resource File. For the resource type, choose Animation and name the file scale_item_animation.
Now, add the following code to the file inside the set tags:
<scale
android:duration="500"
android:fromXScale="0"
android:fromYScale="0"
android:toXScale="1"
android:toYScale="1" />
In this small snippet, you’ve defined how the animation will behave. It will scale items up from 0 scale to 1, making them look as if they are growing until they fill the screen. You also define the duration as 500 milliseconds, so the animation is easier to notice.
Next, create a new file in the anim folder called item_animation, using the same approach as before. Replace the emtpy set tags with:
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/scale_item_animation"
android:animationOrder="normal" />
In this small snippet, you’ve just defined a new layoutAnimation. You’ll use the same file for all your layout animations, but you’ll change the animation attribute to point to different animations.
animationOrder defines how to play the animations, in case you’ve defined multiple property animations in animation.
Now that you’ve defined not only the animation, but also how it will behave as a layout animation, you’ll apply the result to your list items.
Open fragment_popular.xml and add the following line of code to RecyclerView underneath android:id="@+id/popularMoviesList":
android:layoutAnimation="@anim/item_animation"
Using layoutAnimation, you define which animation will run while the UI is laid out.
Items will appear like this:
The items will be invisible at first because their scale is 0. After that, they’ll slowly scale up until they reach the full scale in the UI.
Build and run. Tap Sign In and then tap Login to navigate to the popular movies. > Note: If using an emulator, you may have to switch to the Favorites tab and then back to Popular.
You’ll now see an awesome animation when your list items appear!
This animation not only looks nice, but it also shows users that the data they’re seeing is new.
Next, you’ll build a similar animation, except you’ll use translation instead of scale.
Building translation animations
To build a translation animation, create a new file in the anim folder named vertical_translation_item_animation. Add the following code inside the set tags:
<translate
android:duration="500"
android:fromYDelta="-100%"
android:toYDelta="0%" />
Similar to the previous animation, you define the animation’s from and to values, as well as its duration.
The delta values represent the position of the item relative to the top-right corner of the screen, like so:
The original item position is neutral, or 0 delta. Negative delta represents the space before or above the item, whereas positive delta represents the space after or under the item.
The translation makes it seem like items are falling in from the top when you open the screen.
Now, in item_animation, replace android:animation with:
android:animation="@anim/vertical_translation_item_animation"
The code above changes the item animation to use your new translation. The animation will play out like so:
Build and run. You’ll see the animation on PopularMoviesFragment.
Your items act as if they’re dropping into the UI from the top, which looks really cool!
Combining multiple animations
So far, with just a few lines of code, you’ve built two lovely animations for your list items. It’s that easy! But it’s also great to know you can apply multiple animations in your animation set and apply them to each list item. You’ll do that next.
Create a new file in the anim folder called combined_item_animation. Add the following code inside the set tags:
<translate
android:duration="500"
android:fromXDelta="-100%"
android:toXDelta="0%" />
<alpha
android:duration="500"
android:fromAlpha="0"
android:toAlpha="1" />
Adding both of these animations to the set means you’ll run multiple transformations on each list item. To understand how the deltaX animation works, look at the following diagram:
In this case, the delta position defines how far to the left (negative delta) or the right (positive delta) the item is. Neutral, or 0, delta is the starting point on the screen, within the UI bounds.
Now, replace the android:animation in item_animation with the following:
android:animation="@anim/combined_item_animation"
You’re now pointing to a different set of animations, which will apply to each of your items.
The animation will play out like this:
Here, the items don’t just translate in horizontally with a slide-in animation; they also update their alpha values from fully transparent items to fully visible by using a fade-in animation.
Build and run. You’ll see your awesome combined animation at work!
Using these basic animation types, you can achieve beautiful layout animations without much code. But these animations only occur when the items first appear.
List items are usually dynamic, which means you can add them, remove them or move them around. You’ll tackle how to animate those changes next!
Using data set changes to animate list items
To animate list items when the data set changes, you’ll use convenient functions in your RecyclerViews. Open MoviesRecyclerAdapter and take note of notifyDataSetChanged() when you call setItems():
fun setItems(newItems: List<Movie>) {
this.items.clear()
this.items.addAll(newItems)
notifyDataSetChanged() // here
}
This function tells the adapter that all the items in your data set changed, so it should update them all. Obviously, this isn’t something you want. You rarely want to re-render all the items on your screen.
As you see here, calling notifyDataSetChanged() invalidates all the UI elements. When that happens, RecyclerView.Adapter knows that it should render new data, so it promptly calls onBindViewHolder() for each item in the list.
This forces it to redraw all the elements, even those that didn’t change — which is an expensive operation. It’s better to notify the adapter when you add or remove new items rather than invalidating everything.
Next, you’ll see how to update items when you add or remove them from the list.
Removing items from the list
Open MoviesRecyclerAdapter.kt. You’ll use this adapter to render popular movies in the PopularMoviesFragment. Once it’s open, navigate to onBindViewHolder().
Currently, you send a simple hard-coded long-tap listener within onBindViewHolder() to MoviesViewHolder:
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
holder.bind(items[position]) { movie ->
this.items.remove(movie) // 1
notifyDataSetChanged() // 2
}
}
You’ll use this hard-coded listener to add or remove selected items from the list, to imitate data operations. It will work like this:
- You remove the item from
items, which is your adapter’s data set. - You need to change the
notifyDataSetChanged()call to tell the user you removed a single item.
Change the callback you pass to bind() to the following:
holder.bind(items[position]) { movie ->
val itemIndex = items.indexOf(movie) // 1
this.items.remove(movie) // 2
notifyItemRemoved(itemIndex) // 3
}
The snippet now does the following:
- Fetches the index of the item you want to remove.
- Removes the item from the list.
- Notifies the adapter that you removed only one item at a given
itemIndex.
This way, instead of invalidating the entire data set, you call notifyItemRemoved(), which tells the adapter that only one index changed and the item at the index was removed.
The animation will play out like this:
When you long-tap an item, any items below it will collapse and move up the list. This is a nice, simple animation that helps inform the users of what’s happening.
Build and run. Long-tap an item in PopularMoviesFragment. When you remove an item, you’ll now see your cool animation.
This is a good example of meaningful motion.
Next, you’ll learn how to notify the adapter that you’ve added more items to the list.
Adding items to the list
As with notifyItemRemoved(), you can use notifyItemInserted() to tell the adapter you added new items to the list.
To do that, change the callback signature that you pass to bind() to:
holder.bind(items[position]) { movie ->
val newIndex = (0..items.size).random() // 1
this.items.add(newIndex, movie) // 2
notifyItemInserted(newIndex) // 3
}
Instead of removing the item, you:
- Generate a random index within the bounds of the data set.
- Add the same item as a duplicate at the given
newIndex. - Call
notifyItemInserted()and let the adapter know that you’ve inserted a new item.
This time, you’ll randomly add more items, which will cause other items to shift.
As you insert an item at a specific position, the adapter will push items underneath it to make space for the new item. This is another simple, cool animation that gives users more information about what’s happening on the screen.
Build and run. You’ll see the following behavior:
So how does the list know how to animate these items? Is there is an automatic way to notify the adapter of changes?
Well, the answer lies in a very simple API: ItemAnimator.
Using ItemAnimators
Whenever you post any data set changes to the RecyclerView, you trigger its ItemAnimator. Each RecyclerView has an ItemAnimator that you can change programmatically.
By default, the list uses DefaultItemAnimator, which animates items like you’ve seen so far — by either shifting them to the bottom or collapsing them to the top.
The ItemAnimator API receives a notification of any data set changes. It exposes four main functions to animate each type of change:
-
animateDisappearance: Animates an item that’s being removed.
-
animateAppearance: Animates an item as it’s added.
-
animatePersistance: Runs when an item is present in the data set before and after, but the item hasn’t been invalidated.
-
animateChange: Animates an item when its position changes.
In each of these functions, the magic happens when you change something in the data set.
To override DefaultItemAnimator, pass in a custom instance to your RecyclerView, like so:
myRecyclerView.itemAnimator = object: RecyclerView.ItemAnimator {
// ... Implement your animations
}
DefaultItemAnimator is great for most applications because it supplies users with well-known behavior. However, it’s worth learning how to implement a custom ItemAnimator so you can handle the cases where it doesn’t do everything you need.
That’s what you’ll do next.
Creating a custom ItemAnimator
Creating custom ItemAnimators isn’t hard. You’ll learn how to do it by building a nice scale-up animation when you add a new item to the list.
Create a new class called MyItemAnimator in the popular package, and replace the code with the following:
class MyItemAnimator : DefaultItemAnimator() { // 1
override fun animateAdd(holder: RecyclerView.ViewHolder?): Boolean {
if (holder != null) {
// 2
holder.itemView.scaleX = 0f
holder.itemView.scaleY = 0f
// 3
holder.itemView.animate()
.scaleX(1f)
.scaleY(1f)
.setDuration(1000)
.start()
return true // 4
}
return super.animateAdd(holder)
}
}
Here’s what’s going on:
- First, instead of extending from
RecyclerView.ItemAnimator, you extendDefaultItemAnimator(). That way, you don’t have to provide all the animations for all types of data set changes. It saves you a lot of work and gives you the ability to override only the animations you want to customize. - Next, since you’ll be scaling items up, you set the root
View‘sscaleXandscaleYproperties to0f, so the items don’t appear on the screen. - You then start the animation, scaling back up to
1f, or full item scale, for1000milliseconds, or one second. - Finally, you return
trueto tell the animator it needs to apply these animations.
It’s as simple as that! Now, to apply this animator, go back to PopularMoviesFragment and change the following part of onViewCreated():
binding.popularMoviesList.apply {
adapter = popularAdapter
itemAnimator = MyItemAnimator() // apply your animator
}
Finally, to make the animation more visible, change the code in MoviesRecyclerAdapter’s onBindViewHolder() to the following:
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
holder.bind(items[position]) { movie ->
val newIndex = position + 1 // here
this.items.add(newIndex, movie)
notifyItemInserted(newIndex)
}
}
Instead of using a random index to add the item, you’ll just add it to the next index. That way, the animation will be easier to observe.
Build and run. You’ll see the following animation:
The animation looks really nice — and it was simple to implement. Remember, it’s best to override ItemAnimator only if you can’t describe your item changes with a simple shift of items. Otherwise, it might not match the behavior that Android users expect.
These animations are all simple and cool, but you have to send the updates to the adapter manually. Or do you? You’ll try another way next.
DiffUtil & ListAdapter
RecyclerView has two APIs that help you update the items in the list automatically: DiffUtil and ListAdapter.
The DiffUtil API stands for difference utility, a class that helps you tell the difference between each item in the list when you change your data set through its DiffUtil.ItemCallback.
An example of DiffUtil implementation is MoviesDiffCallback. Open that file within the package util to see the following code:
class MoviesDiffCallback : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
}
As you can see, the API exposes two functions:
- areItemsTheSame: Calculates if the two list items are the same. If you have abstract or complex hierarchies, you can compare types here; for simple items, you can compare IDs.
- areContentsTheSame: Calculates if the contents of the items are the same. This is useful when you have items that have many properties. That way, the item can stay the same, but its contents will change. You don’t need to shift any positions, but you can update the item and reload its UI based on the new state.
In the case above, you compare the item IDs because changes in the item’s ID most likely indicate that the data changed, too. And if the ID doesn’t change, there won’t be any new data from the API.
Pairing DiffUtil with ListAdapter
DiffUtil isn’t very helpful on its own; it’s often paired up with ListAdapter, another API that automates the way the adapter sends data set changes to the list.
ListAdapter is just an advanced RecyclerView.Adapter that computes data set change differences, then updates the UI efficiently because DiffUtil‘s computations are optimized. Open MoviesAdapter.kt and you’ll see the following signature:
class MoviesAdapter : ListAdapter<Movie, MoviesAdapter.MoviesViewHolder>(MoviesDiffCallback()) { }
This adapter is already built for you; you’ll use it to simplify the way you load and change the data set. Notice how you’re passing MoviesDiffCallback to the constructor so it knows how to compare each item in the list.
Open PopularMoviesFragment.kt and near the top change the adapter to the following:
private val popularAdapter: MoviesAdapter by inject() // injecting from DI
Instead of using the manual adapter, you use the MoviesAdapter. It’s automated and does everything for you!
Now, also change the way you load the data at the top of attachObservers():
private fun attachObservers() {
viewModel.movies.observe(viewLifecycleOwner, { movies ->
popularAdapter.submitList(movies.shuffled()) // 1
// 2
GlobalScope.launch {
repeat(3) {
delay(1000)
popularAdapter.submitList(viewModel.movies.value?.shuffled() ?: emptyList())
}
}
})
...
}
Here’s what’s happening in this snippet of code:
- You set up the base use case for
movies, callingsubmitList()whenever something changes in the database to let the adapter know about new items. Because you don’t control the data in the adapter, you leave the responsibility of computing the changes to the adapter. - Then, you set up a fake operation within a coroutine. It delays for a second, then submits a
shuffled()list of movies to the adapter three times. Similar to what you did when you removed or added movies to the list, this imitates data change operations.
If any of the items in your database change, they will be resubmitted, your adapter will calculate the difference using the MoviesDiffCallback and you’ll receive an optimized, animated update.
Build and run one final time. You’ll see all of your data shuffling about with a nice, animated display! Because the shuffle operations are random, it’s hard to provide screenshots that showcase the behavior, so make sure to run the app!
Note: This shuffle logic is great for showcasing multiple item changes, but it doesn’t serve much purpose beyond that. The final project doesn’t have this last change, so make sure to use the next chapter’s starter project as you proceed through the book!
DiffUtil internals
Whenever you submit a new list to the adapter, DiffUtil compares all the items in the list based on the conditions in MovieDiffCallback.
When any positions change, DiffUtil lets the adapter know how to react to it depending on whether it’s the same item, but it needs a data update, or if it’s a completely different item needing a full update.
Internally, it triggers ItemAnimator, which applies whatever animations it defines for those types of changes.
It seems daunting at first, but once you realize it’s only a few puzzle pieces tied together, it’s simple.
Challenge: Add rotation animations
To practice using layout animations, try building a rotation animation using XML. Once you finish, apply it to your RecyclerView and watch your items spin around when they initially appear in the UI.
To achieve this, use the <rotate> animation tag and the fromDegrees and toDegrees properties to define the spin. You can also use the pivotX and pivotY properties to define the center of the rotation.
You could also combine the rotation animation with a fade-in animation to make it look even cooler.
Once you’re done, be sure to check out the challenge project within the 07-basic-list-animations folder, in the aat-materials repository, to find the solution. Compare how your animation works with the provided example.
Good luck!
Key points
- Using layout animations, you can apply basic animations whenever an item first appears in the list.
- Layout animations can be translations, scaling, alpha changes and rotation animations.
- You can combine multiple simple animations within the animation set to define the order they play in.
- Using data set changes, you can tell your adapters when items are removed, moved or added to the list, or which items changed their contents.
- There are various data set change functions, so be sure to use the one that best describes your change!
- To animate data changes,
RecyclerViewusesItemAnimator, which exposes many functions to animate different types of changes. - If unchanged,
RecyclerViewusesDefaultItemAnimator, which offers simple, predefined animations. - You can create a custom
ItemAnimatorby extending fromDefaultItemAnimatorand overriding only the data set functions you want to change. - If you don’t want to calculate changes yourself, use the
DiffUtilandListAdapterAPIs to implement automatic data changes to your list. - When you call
submitList()to the adapter,DiffUtillets it know how to animate items. -
DiffUtilandListAdapteruse the list’sItemAnimatorto perform required animations. -
DiffUtil’s computations are optimized, with most only taking 10–30 milliseconds.
Where to go from here?
In the next chapter, you’ll add more options to your list items by adding swipe gestures that let you delete items or mark them as favorites. Once you add features to update the database items, ListAdapter and MovieDiffCallback will ensure you automatically receive updates and more animations in your list.
RecyclerView.Adapter API gives you more options to animate data set changes. You can use the following functions to do so:
- notifyItemChanged(): When the item’s contents change, but not its position.
- notifyItemMoved(): If the item’s position changes, but not its content.
- notifyItemRangeChanged(): If multiple items in a range change.
- notifyItemRangeInserted(): When you insert more than one item.
- notifyItemRangeRemoved(): When you remove more than one item.
Try out these functions and see how the list behaves!
Alternatively, if you want to learn more about ItemAnimators, you can implement a custom animator that transforms each item in a custom and complex way. You can apply any animations you can think of, so go crazy!