Distance from a RecyclerView source code analysis of the article has passed more than 10 days, today we will look at the RecyclerView animation mechanism. However, this article will not analyze the knowledge related to ItemAnimator, but understand how to use RecyclerView to execute ItemAnimator. I will write a special article to analyze ItemAniamtor knowledge later.

References for this article:

  1. RecyclerView animations – AndroidDevSummit write-up
  2. RecyclerView. ItemAnimator ultimate interpretation (a) – RecyclerView source code parsing

Note that all the code in this article is from 27.1.1.

1. An overview of the

RecyclerView’s popularity is partly due to its animation mechanics. We can use RecyclerView’s setItemAnimator method to set each Item to perform different animations under different behaviors. It’s very simple. Although we know how to set animation for RecyclerView, how RecyclerView uses ItemAnimator to animate each Item is worth studying and learning.

Before the formal analysis of RecyclerView animation mechanism, we first have a concept of a few words, let’s have a look:

words meaning
Disappearance The ItemView is visible before the animation, but not after the animation. The operations here include the remove operation and plain sliding that causes the ItemView to cross out the screen
Appearance The ItemView is invisible before the animation, and visible after the animation. The actions here include the Add operation and plain sliding to bring the ItemView to the screen
Persistence The state is constant before and after the animation. The operations include none
change The state is constant before and after the animation. The operations include the change operation.

Another thing to note is that the ViewHolder is not used to record the location of the ItemView, but rather to do the data binding. So in the animation, the location is recorded not by the ViewHolder, but by a class called ItemHolderInfo. In this class, there are four member variables that record the left, top, right, and bottom positions of the ItemView.

Finally, we need to pay attention to the three processes of RecyclerView. Within RecyclerView, dispatchLayout is divided into three steps, among which dispathchLayoutStep1 is called pre-layout. In this case, the OldViewHolder of the ItemView is stored, and the position of each ItemView before the animation is recorded. The corresponding dispathchLayoutStep3 is called post-layout, which mainly combines the relevant information of real layout and pre-layout to realize animation, of course, the premise is that RecyclerView itself supports animation.

This paper intends to analyze RecyclerView animation from two perspectives, one is from the ordinary three processes, which is the core of the animation mechanism; From the Adapeter’s point of view, let’s look at how we animate the execution each time we call Adapter’s notify method. #1. Look at the three RecyclerView processes again. Take this topic, I feel it has special meaning. First of all, this analysis of animation mechanism is to look at the three processes again, of course, this time is not as careful as the previous three processes, secondly, the emphasis is different; Secondly, this time to look at the three RecyclerView processes, can also fill the pit left by the three processes of RecyclerView before the analysis.

This analysis focuses on dispathchLayoutStep1 and dispathchLayoutStep3, will not analyze the three complete processes, so, there are students who do not understand the three processes of RecyclerView, Can refer to my article :RecyclerView source code analysis (a) – RecyclerView three processes.

Let’s first look at the dispatchLayoutStep1 method:

    private void dispatchLayoutStep1() {/ / · · · · · ·if (mState.mRunSimpleAnimations) {
            // Step 0: Find out where all non-removed items are, pre-layout
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                if(holder.shouldIgnore() || (holder.isInvalid() && ! mAdapter.hasStableIds())) {continue;
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                if(mState.mTrackOldChangeHolders && holder.isUpdated() && ! holder.isRemoved() && ! holder.shouldIgnore() && ! holder.isInvalid()) { long key = getChangedHolderKey(holder); // This is NOT the only placewhere a ViewHolder is added to old change holders
                    // list. There is another case where:
                    //    * A VH is currently hidden but not deleted
                    //    * The hidden item is changed in the adapter
                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
                    // When this caseis detected, RV will un-hide that view and add to the old // change holders list. mViewInfoStore.addToOldChangeHolders(key, holder); }}}if (mState.mRunPredictiveAnimations) {
            // Step 1: run prelayout: This will use the old positions of items. The layout manager
            // is expected to layout everything, even removed items (though not to add removed
            // items back to the container). This gives the pre-layout position of APPEARING views
            // which come into existence as part of the real layout.

            // Save old positions so that LayoutManager can run its mapping logic.
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            // temporarily disable flag because we are asking for previous layout
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mStructureChanged = didStructureChange;

            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                if (viewHolder.shouldIgnore()) {
                if(! mViewInfoStore.isInPreLayout(viewHolder)) { int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder); boolean wasHidden = viewHolder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);if(! wasHidden) { flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; } final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());if (wasHidden) {
                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                    } else {
                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
            // we don't process disappearing list because they may re-appear in post layout pass. clearOldPositions(); } else { clearOldPositions(); } onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT; }Copy the code

I’ve divided the dispatchLayoutStep1 method into two steps (which we can actually figure out from Google dad’s notes).

  1. Find each ItemView that has not been removed and place its ViewHolder(OldViewHolder) inViewInfoStoreInside, also put it in the pre-layout positionViewInfoStoreThe inside. Both of these will be used later in the animation.
  2. If the currentRecyclerViewtheLayoutManagersupportpredictive item animations(supportsPredictiveItemAnimationsMethod returns true, I think it’s good to describe this animation in English, because I don’t know how to translate), will actually do the pre-layout. In this step, it calls firstLayoutManagertheonLayoutChildrenDo a layout, but this layout knowledge pre-layout, that is, not the real layout, just determine eachItemViewThe location of the. After the pre-layout, each is taken at this timeItemViewtheViewHolderandItemHolderInfo, is everyItemViewFinal information.

The information in the second step echoes the information in the first step, the information before the change, and the information after the change. This is all in preparation for the dispatchLayout phase 3 animation. Among them, we find that the second step is much more complicated than the first step. However, we can see that, no matter how complex it is, the ViewHolder of the current ItemView is saved by calling addToOldChangeHolders. (In the same location before and after the onLayoutChildren method of LayoutManager, Not necessarily the same ItemView, not necessarily the same ViewHolder), and then call the addXXXLayout method to save the location information (ItemHolderInfo).

Then, let’s look at the dispatchLayoutStep3 phase:

    private void dispatchLayoutStep3() { mState.assertLayoutStep(State.STEP_ANIMATIONS); startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.mLayoutStep = State.STEP_START; // Fetch the relevant information to, and then add it to ViewInfoStoreif (mState.mRunSimpleAnimations) {
            // Step 3: Find out where things are now, and process change animations.
            // traverse list in reverse because we may call animateChange in the loop which may
            // remove the target view holder.
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                if (holder.shouldIgnore()) {
                long key = getChangedHolderKey(holder);
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPostLayoutInformation(mState, holder);
                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                if(oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { // run a change animation // If an Item is CHANGED but the updated version is disappearing, it creates // a conflicting case. // Since a view that is marked as disappearing is likely to be going out of // bounds,  we run a change animation. Both views will be cleaned automatically // once their animations finish. // On the other hand,ifit is the same view holder instance, we run a // disappearing animation instead because we are not going to rebind the updated // VH unless it is enforced by  the layout manager. final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);if (oldDisappearing && oldChangeViewHolder == holder) {
                        // run disappear animation instead of change
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    } else {
                        final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                        // we add and remove so that any post info is merged.
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                        ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                        if (preInfo == null) {
                            handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                        } else{ animateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); }}}else{ mViewInfoStore.addToPostLayout(holder, animationInfo); }} // Step 4: Process view info Lists and trigger animations // MViewinfostore. Process (mViewInfoProcessCallback); } // Cleanup phase}Copy the code

I’ve divided the above code into three phases.

  1. Get the relevant location information (ItemHolderInfo) and passaddToPostLayoutThe method saves the location inViewInfoStoreThe inside.
  2. callViewInfoStoretheprocessMethod triggers an animation.
  3. Carry out relevant cleaning work.

Here, let’s just focus on the first two steps.

The first step is easy to understand. The first step is to retrieve the location of the current ItemView and store it in the ViewInfoStore. Here, we find that if OldViewHolder is not empty, special processing is done. Why is this done? In fact, we consider the change operation because the change operation involves animating two ItemViews, so we can see that if an ItemView calls the animateChange method to start the animation, Rather than follow the generic logic of storing location information in the addToPostLayout method and then calling the Process method for a uniform invocation.

And then step two. Let’s look at the ViewInfoStore process method, but before we do that, let’s look at a few methods of the ProcessCallback interface.

methods role
processDisappeared An ItemView going from visible to invisible calls back to this method, essentially performing the animation in this case
processAppeared An ItemView never visible will call back to this method.
processPersistent The state of an ItemView changes before and after the animation. This includes: an ItemView that has no action on its own, and an ItemView that has a change operation
unused An ItemView change does not support the animation to call back to this method. This includes situations such as an ItemView that Appeared and then disappearedRecyclerView[Fixed] Unable to find suitable animation This method is also called when the current ItemView is missing preInfo, i.e. the location information is not recorded in the prelayout. This is usually the case when the ItemView is removed, butAdapterCall isnotifyDataSetChangedmethods

Now, let’s formally look at the process method:

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                // Appeared then disappeared. Not useful for animations.
            } else if((record.flags & FLAG_DISAPPEARED) ! = 0) { // Set as"disappeared" by the LayoutManager (addDisappearingView)
                if (record.preInfo == null) {
                    // similar to appear disappear but happened between different layout passes.
                    // this can happen when the layout manager is using auto-measure
                } else{ callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); }}else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                // Appeared in the layout but not in the adapter (e.g. entered the viewport)
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                // Persistent in both passes. Animate persistence
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if((record.flags & FLAG_PRE) ! = 0) { // Wasin pre-layout, never been added to post layout
                callback.processDisappeared(viewHolder, record.preInfo, null);
            } else if((record.flags & FLAG_POST) ! = 0) { // Was notin pre-layout, been added to post layout
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if((record.flags & FLAG_APPEAR) ! = 0) { // Scrap view. RecyclerView will handle removing/recycling this. }else if (DEBUG) {
                throw new IllegalStateException("record without any reasonable flag combination:/"); } InfoRecord.recycle(record); }}Copy the code

The process method is simple enough to call the ProcessCallback method with the associated flag. Let’s now take a look at how each method of ProcessCallback is implemented.

    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                        @Nullable ItemHolderInfo postInfo) {
                    animateDisappearance(viewHolder, info, postInfo);
                public void processAppeared(ViewHolder viewHolder,
                        ItemHolderInfo preInfo, ItemHolderInfo info) {
                    animateAppearance(viewHolder, preInfo, info);

                public void processPersistent(ViewHolder viewHolder,
                        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                    if (mDataSetHasChangedAfterLayout) {
                        // since it was rebound, use change instead as we'll be mapping them from // stable ids. If stable ids were false, we would not be running any // animations if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) { postAnimationRunner(); } } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { postAnimationRunner(); } } @Override public void unused(ViewHolder viewHolder) { mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); }};Copy the code

The animateXXX method is called, and what does the animateXXX method do? ViewCompat postOnAnimation posts a Runnable to the end of the task queue. The code is as follows:

    void postAnimationRunner() {
        if(! mPostedAnimatorRunner && mIsAttached) { ViewCompat.postOnAnimation(this, mItemAnimatorRunner); mPostedAnimatorRunner =true; }}Copy the code

In the above code, we need to note that postAnimationRunner is called only once at a time. What if more than one animation is executed in a single operation? ProcessCallback Each callback will call the animateXXX method, which will call the corresponding ItemAnimator method. Within the ItemAnimator method, the current animation will be added to an array. We then call the runPendingAnimations method of ItemAnimator via mItemAnimatorRunner. The runPendingAnimations method is where all animations start. We will not discuss the internal implementation of ItemAnimator here; there will be a special article to analyze it later.

2. Look at the animation execution mechanism from the Adapter point of view

As we know, the notifyDataSetChanged method of Adapter, RecyclerView does not animate; Methods like notifyItemRemoved have animations, and we’re analyzing the animations from the Adapter point of view. Like the ItemAnimator, we will not analyze the Adapter here, there will be a special article to analyze it later.

So before we look at Adapter, let’s look at one thing, and that’s how RecyclerView communicates with Adapter.

(1) The communication between RecyclerView and Adapter can be realized through the observer mode

Before we think about this, we should first rule out that Addapter and RecyclerView are strongly coupled, that is, the Adapter holds a RecyclerView object inside. RecyclerView itself is the plug and pull design, if Adapter and RecyclerView is strong coupling, it violates the plug and pull design idea. So how exactly do they communicate? The answer is obvious: the two communicate through the observer model.

Among them, Adapter serves as the observed and RecyclerView serves as the observer. When the data of Adapter changes, it will notify each of its observers.

RecyclerView itself design is special, RecyclerView did not realize the Observer(temporarily called here) interface, but internal hold an Observer(RecyclerViewDataObserver) object, To listen for Adapter state changes; Adapter does not implement an Observable interface, but internally holds an Observable(AdapterDataObservable).

Let’s look at how the Notify method of Adapter corresponds to the Observer method.

The notify method of Adapter The corresponding Observer method
notifyItemRemoved notifyItemRangeRemoved
notifyItemChanged notifyItemRangeChanged
notifyItemInserted notifyItemRangeInserted
notifyItemMoved notifyItemMoved

When a method is called to the Observer, the Observer calls the AdapterHelper related methods, and within the AdapterHelper an UpdateOp object is created for each operation and added to a PendingUpdate array. Let’s take a look at the code (take add as an example) :

        public void onItemRangeInserted(int positionStart, int itemCount) {
            if(mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { triggerUpdateProcessor(); }}Copy the code

If onItemRangeInserted returns true, the triggerUpdateProcessor method is called. The triggerUpdateProcessor method is guaranteed to be called only once. The triggerUpdateProcessor method is guaranteed to be called only once. The triggerUpdateProcessor method is guaranteed to be called only once.

Then, let’s look at the triggerUpdateProcessor method:

        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true; requestLayout(); }}Copy the code

The requestLayout method is still called in the if statement, or else, and the three processes are retraced.

Visible and know, RecyclerView three processes in the end how important. This time, we look at the dispatchLayoutStep2 method of the three processes. We know that in the Observer phase, each operation actually creates an UpdateOp object, which is added to the PendingUpdate array. So when are all the operations in the array performed? In the dispatchLayoutStep2 method phase:

    private void dispatchLayoutStep2() {/ / · · · · · · mAdapterHelper. ConsumeUpdatesInOnePass (); / /......}Copy the code

PendingUpdate is actually executed in AdapterHelper’s consumeUpdatesInOnePass method.

    void consumeUpdatesInOnePass() {
        // we still consume postponed updates (if there is) in case there was a pre-process call
        // w/o a matching consumePostponedUpdates.
        final int count = mPendingUpdates.size();
        for (int i = 0; i < count; i++) {
            UpdateOp op = mPendingUpdates.get(i);
            switch (op.cmd) {
                case UpdateOp.ADD:
                    mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                case UpdateOp.REMOVE:
                    mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                case UpdateOp.UPDATE:
                    mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                case UpdateOp.MOVE:
                    mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
            if(mOnItemProcessedCallback ! = null) { mOnItemProcessedCallback.run(); } } recycleUpdateOpsAndClearList(mPendingUpdates); mExistingUpdateTypes = 0; }Copy the code

Although there is a lot of code, we can see that the final operation is called into the Callback interface. And what does Callback do? Two main things were done:

  1. Some ViewHolder positions may be updated
  2. Some ViewHolder flags are updated, for example, the remove flag or the update flag.

This part of the content, we later analysis Adapter will be detailed analysis, this paper will not do too much introduction.

At this point, the position of each ViewHolder has been updated, and the flag of each ViewHolder has been updated. This way, at the dispatchLayoutStep3 stage, you know what animation each ViewHolder should do.

Then, let’s see why the notifyDataSetChanged method calling Adapter doesn’t animate?

(2). Why does notifyDataSetChanged not animate?

The notifyDataSetChanged method calls back to the Observer notifyChanged method.

        public void onChanged() {
            mState.mStructureChanged = true;

            if (!mAdapterHelper.hasPendingUpdates()) {
Copy the code

In this method, we need to pay special attention to processDataSetCompletelyChanged method. Let’s take a look:

void processDataSetCompletelyChanged(boolean dispatchItemsChanged) { mDispatchItemsChangedEvent |= dispatchItemsChanged;  mDataSetHasChangedAfterLayout =true;
Copy the code

In processDataSetCompletelyChanged method, call all the ViewHolder markers for the FLAG_INVALID markKnownViewsInvalid method. As a direct result, we did not get the location information and OldViewHolder for each ItemView correctly during the pre-layout phase, which in turn caused the animation to fail during the post-layout phase. This is why the notifyDataSetChanged method does not animate.

3. Summary

RecyclerView animation mechanism is relatively simple, here we do a simple summary of it.

  1. RecyclerViewThe mechanics of performing animations lie in the pre-layout phase of eachItemViewLocation information andViewHolderSave it in the post-layout stage according to eachItemViewtheViewHolderFlag state to determine what animation to perform, according to the location information to determine how to do animation.
  2. Adapter’s notify methods are able to perform animations because they give each of the three processesViewHolderThe response flag, such as the remove flag or update flag, is displayed. In the back layout, different animations are performed according to flag.
  3. notifyDataSetChangedThe method doesn’t support animation becausenotifyDataSetChangedThe method will make eachViewHolderInvalidFLAG_INVALIDFlag), so that during the pre-layout phase, each cannot be obtained correctlyItemViewLocation information andViewHolder, causing the animation to fail to execute.

If nothing goes wrong, the next article will examine the Adapter.