RecyclerView Animations Part 2 – Behind The Scenes

This is the second part of a 2 articles series. Please read Part 1 first if you’ve not read it yet.

In the first article, I’ve covered the main idea on how predictive animations run in RecyclerView. There is actually a lot more going on to achieve this simplicity (for the LayoutManager). Here are some important points that you should know about.

RecyclerView keeps some children attached although they have been removed by the LayoutManager. How does it work? Does it invalidate the contract between the LayoutManager and RecyclerView?

Yes it does ‘kind of’ violate the contract with LayoutManager, but:

RecyclerView does keep the View as a child of the ViewGroup but hides it from the LayoutManager. Each time LayoutManager calls a method to access its children, RecyclerView takes into account the hidden Views. Lets look at the example at Part 1 where ‘C’ was being removed from the adapter.

Predictive Animation

While ‘C’ is fading out, if LayoutManager calls getChildCount(), RecyclerView returns 6 although it has 7 children. If LayoutManager calls getChildAt(int), RecyclerView offsets that call properly to skip child ‘C’ (or any hidden children). If LayoutManager calls addView(View, position), RecyclerView offsets the index properly before calling ViewGroup#addView.

When the animation ends, RecyclerView will remove the View and recycle it.

For more details, you can check ChildHelper internal class.

How does RecyclerView handle item positions in the preLayout pass since they don’t match Adapter contents?

This is doable thanks to the specific notify events added to the Adapter. When Adapter dispatches notify** events, RecyclerView records them and requests a layout to apply them. Any events that arrives before the next layout pass will be applied together.

When onLayout is called by the system, RecyclerView does the following:

  1. Reorder update events such that move events are pushed to the end of the list of update ops. Moving move events to the end of the list is a simplification step so I’ll not go into details here. You can check OpReorderer class for details if you are interested.
  2. Process events one by one and update existing ViewHolders’ positions with respect to the update. If a ViewHolder is removed, it is also marked as removed. While doing this, RecyclerView also decides whether the adapter change should be dispatched to the LayoutManager before or after the preLayout step. This decision process is as follows:
    • If it is an add operation, it is deferred because item should not exist in preLayout.
    • If it is an update or remove operation and if it affects existing ViewHolders, it is postponed. If it does not effect existing ViewHolders, it is dispatched to the LayoutManager because RecyclerView cannot resurrect the previous state of the item (because it does not have a ViewHolder that represents the previous state of that Item).
    • If it is a move operation, it is deferred because RecyclerView can fake its location in the pre-layout pass. For example, if item at position 3 moved to position 5, RecyclerView can return the View for position 5 in pre-layout when View for position 3 is asked.
    • RecyclerView rewrites update operations as necessary. For example, if an update or delete operation affects some of the ViewHolders, RecyclerView divides that operation. If an operation should be dispatched to LayoutManager but a deferred operation may affect it, RecyclerView re-orders these operations so that they are still consistent.

    For example, if there is an Add 1 at 3 operation which is deferred followed by a Remove 1 at 5 operation which cannot be deferred, RecyclerView dispatches it to the LayoutManager as Remove 1 at 4. This is done because the original Remove 1 at 5 was notified by the Adapter after Add 1 at 3 so it includes that item. Since RecyclerView did not tell LayoutManager about the Add 1 at 3, it rewrites the remove operation to be consistent.

    This approach makes tracking items dead simple for a LayoutManager. The abstraction between the Adapter and the LayoutManager makes all of this possible, which is why RecyclerView never passes the Adapter to the LayoutManager, instead, provides methods to access Adapter via State and Recycler.

    ViewHolders also have their old position, pre layout position and final adapter positions. When ViewHolder#getPosition is called, they return either preLayout position or final adapter position depending on the current layout state (pre or post). LayoutManager doesn’t need to know about this because it will always be consistent with the previous events that were dispatched to the LayoutManager.

  3. After Adapter updates are processed, RecyclerView saves positions and dimensions of existing Views which will later be used for animations.
  4. RecyclerView calls LayoutManager#onLayoutChildren for the preLayout step. As I’ve mentioned in the first article, LayoutManager runs its regular layout logic. All it has to do is to layout more items for those which are being deleted or changed (LayoutParams#isItemRemoved , LayoutParams#isItemChanged). As a reminder, the deleted or changed item still ‘appears’ in the Adapter API given to the LayoutManager. This way, LayoutManager simply treats it as any other View (adds, measures, positions etc).
  5. After preLayout is complete, RecyclerView records the positions of the Views again and dispatches the remaining Adapter updates to the LayoutManager.
  6. RecyclerView calls LayoutManager’s onLayout again (postLayout). This time, all item positions match the current contents of the Adapter. LayoutManager runs its regular layout logic again.
  7. After post layout is complete, RecyclerView checks positions of Views again and decides which items are added, removed, changed and moved. It ‘hides’ removed Views and for views not added by the LayoutManager, adds them to the RecyclerView (because they should be animated).
  8. Items which require an animation are passed to the ItemAnimator to start their animations. After the animation is complete, Item Animator calls a callback in RecyclerView which removes and recycles the View if it is no longer necessary.

What happens if LayoutManager keeps some internal data structure using item positions?

Everything works… kind of :). Thanks to the re-writing of Adapter updates by the RecyclerView, all LayoutManager has to do is to update its own bookkeeping when one of its adapter data changed callbacks is called due to Adapter changes. RecyclerView ensures that these updates are called at the appropriate time and order.

At any time during a layout, if LayoutManager needs to access the adapter for additional data (some custom API), it can call Recycler#convertPreLayoutPositionToPostLayout to get the item’s Adapter position. For example, GridLayoutManager uses this API to get the span size of items.

What happens if notifyDataSetChanged is called? How do predictive animations run?

They don’t, which is why notifyDataSetChanged should be your last resort. When notifyDataSetChanged is called on the adapter, RecyclerView does not know where items moved so it cannot properly fake getViewForPosition calls. It simply runs animations as a LayoutTransition would do.

I hope this two part series helped you understand how animations work in RecyclerView and why they work this way. Feel free to ask more questions in the comments and I’ll try to update the article with answers.

Disclaimer: After joining the Android Framework Team on March 2014, I spent fair amount of my time working on RecyclerView.

  • Special thanks to Chet for reviewing early versions of this blog post!
  • Joaquin Martin

    Great article! Unfortunately, when implementing a basic RecyclerView with the default item animator and a LinearLayoutManager, the remove animation works correctly except for the first item. When the first item is removed, the new item appearing at the end still uses the fade-in animation. Have you noticed this behavior?

    • yeet

      that seems like a regression. Can you create a public report here to follow up?

  • Alex Bravo

    Sounds like the tag in ChildHelper needs to be renamed:

    private static final String TAG = “ChildrenHelper”;

  • Pavan

    Great article!! Really helped get a hang of this awesome technique to do animations. I have one problem in one of my implementations…
    When doing predictive animations when one view is removed, while doing the move animation will a scale animation not be performed? I have layout my views in such a way that during the preLayout phase, the views have been positioned with a slight change in the scaleX and scaleY. But no scaling animation is seen though I see the views move to their new positions.

    • yeet

      We do not have scale animations built in. Change animations are handled by creating another view holder and cross-fading the old and new.
      We have plans to support better change animations (w/o a second ViewHolder) but that requires back porting TransitionManager which is not a trivial task due to API limitations.
      Meanwhile, we’ll try to provide an API where you can manually implement in place change animations.
      You can partially achieve it but setting “supportsChangeAnimations” to false and creating the scale animation when you rebind the view (We pass the same ViewHolder when rebinding if you have stable ids or dispatch granular notify events instead of notifyDataSetChanged).
      Just make sure you implement onFailedToRecycleView in your Adapter in case view needs to be recycled before scale animation is complete.

  • Eric Berry

    I’m having trouble applying a custom animation(s) when the state of an item in my adapter has “changed”. Basically, I want to have the view of the item “hop” when the state has changed. I have created a custom ItemAnimator and implemented the animateChange method to do the “hop” animation on the newHolder.itemView. I’ve implemented the hop as 2 animations (up, and down) on the same view with the second (down) animation being started in the endAnimation method of the first (up) animation’s listener. However, it seems only the first (up) animation get’s played, the second one never executes. Based on the documentation animateChange seems like the right place to do this, is this a correct assumption, or is there a better way to implement something like this?

  • Brad Campbell

    Is it possible to do multiple move animations at once? I have been trying for ages, but I feel like I’m fighting an unwinnable battle. I’m trying to animate my sorting of a list, so that the user can see how the items have been rearranged. Even though I’m going through each change and calling notifyItemMoved, it’s still fading it/out items each time rather than doing the move animation.