We live in times where apps have to be not only useful but also smooth and delightful. Times slightly different than a couple years ago where all we had to do was just
notifyDataSetChanged() on ListView adapter. Screen just blinked, new data appeared and that’s all.
Today, in a times of RenderThread, MaterialDesign animations and transitions app should show exactly what’s happening. User should see when data from his collection just changed or when something new appeared or/and was removed.
A few weeks ago we could see (live or streamed) great Android conference – Android Dev Summit. During those 2 days of deep technical sessions we could see Android engineering team presenting new things – Android Studio 2.0, new Gradle plugin, Instant run feature, new official emulator announcement and much more.
By the way if you missed it, I highly recommend to watch whole playlist – there are a lot of great sessions about Android development, tools and solutions.
One of those videos – RecyclerView Animations and Behind the Scenes is the reason of this post. Chet Haase and Yigit Boyar walked through animating items in RecyclerView and showed how to do it right. This can be good starting point how to make our
RecyclerView more interactive and delightful.
While those are only basics, presented solutions can help you with cleaning up your huge
RecyclerView.Adapter and solve most common issues.
InstaMaterial meets RecyclerView usage guidelines
Today we’ll take a look at RecyclerView animations from practical point of view (soon I’ll try to formalize those things and delve into whole RecyclerView bit deeper).
InstaMaterial source code which we want to cleanup is available under this commit (most recent head version is already updated with changes described below).
Also what is important – from a user perspective we’ll change nothing in current app. But from the code we’ll try do things right (or at least as clean as it’s possible in example app).
Code which we want to rebuild is responsible for two similar actions:
- Like feed item by clicking on its main image
- Like feed item by clicking on like button
Those animations should be triggered in our RecyclerView
- Appearance animation – feed item should slide from the bottom when objects was added for the first time
- Big like animation – heart with circular background should be animated when user clicks on main image
- Like button animation – heart should rotate and be filled when users clicks on like button or when user clicks on main image (and 2. animation is played)
Here are described animations (recorded from recent app):
Big like animation
Like button animation
Previously our animations were implemented directly in FeedAdapter, the
RecyclerView.Adapter subclass. Everything worked, so what was wrong with this approach?
- Animations are not what
RecyclerView.Adapteris designed for. According to documentation:
Adapters provide a binding from an app-specific data set to views that are displayed within a RecyclerView.
Our adapter will have enough amount of line of binding code the have it twice more with animations stuff.
- By doing animations in adapter we have to care about cancelling them, handling view recycling in proper way, making sure that they are played in a right moment in time and much more. Everything on our own.
- While single item animations are something doable, objects interactions (moving/swapping items, updating items positions while new object appeared/disappeared) is something more complicated.
- RecyclerView creators gave us their official solution:
This class defines the animations that take place on items as changes are made to the adapter.
It works close to
RecyclerView, handling all mentioned above cases for us. And because of that we can care more about animations quality, than about the logic which handles them properly in scrolling lifecycle.
Let’s take a look at our FeedAdapter one more time.
Those lines of codes shouldn’t be here:
We need to have control on when we should animate items and not (items should be animated on first appearance, but not when view is restored in activity restoring process).
We should keep our animations somewhere, in case we need to check if they are playing or to cancel them in recycling process.
Here we run enter animation every time when view is bound and check if it’s right moment to do this (items should be animated once). Probably method name is bit confusing for described case.
In a moment of binding view in
onBindViewHolder() we’re cancelling animations if they are already running. This is because view can be recycled and we don’t know if there is any not finished animation.
updateHeartButton() are responsible for setting content in both ways (animated and static).
Also there is an issue in our code.
We’re passing position to our buttons:
To have it later in our
This position index not always will be correct. Especially when we use this position in both approaches: for placing context menu in a right place on a screen and for passing adapter item to it (well, theoretically in this case 😉).
Since RecyclerView can update data in an async way (item views can be moved without updating data – for example with
notifyItemMoved()) there is a possibility that our position indicates wrong data.
This is very similar to case which Yigit Boyar talked about:
We cannot assume that item position will be final (and code on this slide can cause issues).
Instead we should use those two methods from
Let’s start from beginning. Our Feed will be built with those pieces:
RecyclerView.ItemAnimator). It will give us base for default animations performed by RecyclerView (mainly fade in/out) which we can extend in places which are important for us.
LinearLayoutManager– like previously, to make our feed looks like standard list view.
FeedAdapter– for binding data with views (and just for that).
Here is the full code of FeedItemAnimator.
And here we have more interesting code from it:
When we’re animating RecyclerView items we have a chance to ask RecyclerView to keep the previous ViewHolder of the item as-is and provide a new ViewHolder which will animate changes from the previous one (only new ViewHolder will be visible on our RecyclerView). But when we are writing a custom item animator for our layout we should use same ViewHolder and animate the content changes manually. This is why our method returns
true in this case.
When we’re calling
notifyItemChanged() method we can pass additional parameters which will help us to decide what animation should be performed.
Examples from our
recordPreLayoutInformation() is used for catching data before it was changed. Then RecyclerView calls
onBindViewHolder() (in adapter) and at the end ItemAnimator calls
recordPostLayoutInformation() which catches data after this.
Thanks to those operations we can have informations about item state before and after its change.
At the end of everything method
animateChange() is called, with both pre and post
ItemHolderInfo objects. Here is how it looks in our example:
We can clearly see that heart button animation is triggered always, but big photo animation is triggered only when user clicks on feed image. This is what we assumed in list of our desired effects.
And the second thing – appear animation. It should be triggered when we see our list for the first time. And this is how it’s handled:
RecyclerView.ItemAnimator’s method is called when we invoke
notifyItemRangeInserted() in our FeedAdapter. Another way is to call
It’s good to implement those two methods. Thanks do this RecyclerView will be able stop animations on item view, when it will disappear from a screen (and will be ready to recycle).
Also those two used methods are worth mentioning:
- dispatchAddFinished() – should be called when animation from animateAdd() is finished (this will inform RecyclerView that view is ready for recycle).
- dispatchAnimationFinished() – as above, for animateChange().
And that’s all. Our updated
FeedAdapter has 200 lines of code less than before and is responsible only for data-view binding. Here is the full source code of it.
Most recent version of InstaMaterial source code is available on Github repository.