In RecyclerView, it is very easy to add animations. We can inherit the SimpleItemAnimator and implement the animation details of the internal parts. RecyclerView also provides its own DefaultItemAnimator that inherits from SimpleItemAnimator. If we do not set the animation, the default animation effect will be used. We first introduce SimpleItemAnimator, and then on this basis, study the internal RecyclerView about the implementation of animation.

Animation business layer use

The business layer uses RecyclerView animation to have three layers of inheritance structure for our use.

Graph TD ItemAnimator --> SimpleItemAnimator SimpleItemAnimator --> DefaultItemAnimator SimpleItemAnimator --> Our own animation implementation

ItemAnimator is an abstract class that RecyclerView uses to animate objects and store the view’s location via ItemHolderInfo. Internally, there are four abstract methods that we need to implement.

Abstract methods meaning
animateAppearance From invisible to visible, corresponding to addView or swipe to screen
animatePersistence The state remains the same
animateDisappearance From visible to invisible, corresponding to remvoe or underline screen
animateChange Corresponding Update operation

See these four methods, may be different from our imagination of the animation implementation, we generally run the animation through local refresh, such as INSERT, remove, move and other operations, if only the use of the above four methods, it is difficult to know the corresponding operations. So the SimpleItemAnimator continues to wrap a layer of Adapter on top of this, translating into the familiar local refresh interface. Equivalent to a layer of skeleton implementation classes. SimpleItemAnimator is also an abstract class with the following abstractions:

Abstract methods meaning
animateAdd Corresponding notifyInsert
animateChange Corresponding notifyUpdate
animateMove Corresponding notifyMove
animateRemove Corresponding notifyMove

We should be familiar with the above method. Corresponding to our local refresh operation, we can implement the animation details of each changing part by ourselves. RecyclerView DefaultItemAnimator, is inherited from SimpleItemAnimator, to achieve the default animation effect, need to achieve their own effect can refer to DefaultItemAnimator. Above is the business layer to use RecyclerView animation details, or relatively easy to understand.

Animation low-level implementation

The previous chapters have hidden the animation part, because the animation processing and the normal flow at the same time will interfere with our analysis of the normal flow. When analyzing the mapping process, we mentioned that dispatchLayoutStep1() and dispatchLayoutStep3() mainly deal with the logic of animation, and dispatchLayoutStep2() deals with the normal mapping process. The operation part of the animation is sandwiched between the normal mapping. What should we guess about this whole process? DispatchLayoutStep1 () mainly records the state before normal mapping, and after the formal mapping of dispatchLayoutStep2(), In dispatchLayoutStep3(), compare the historical data before surveying and mapping with the latest data, and run the animation according to the comparison results. This is the general underlying implementation logic. The specific methods of design are analyzed below.

dispatchLayoutStep1

The responsibility of dispatchLayoutStep1 is to record the state before normal mapping and initialize the parameters of the animation operation. Internal logic by first processAdapterUpdatesAndSetAnimationFlags () method handles animation and judge whether predictive animation needs to be. Store data before formal surveying and mapping for use after surveying and mapping is completed. Predictive animations call LayoutManager#onLayoutChildren() for pre-layout.

Private void processAdapterUpdatesAndSetAnimationFlags () {... if (predictiveItemAnimationsEnabled()) { mAdapterHelper.preProcess(); } else { mAdapterHelper.consumeUpdatesInOnePass(); } boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator ! = null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && ! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); }Copy the code

Internal processing UpdateOp local refresh data first, predictiveItemAnimationsEnabled () the main judge whether to support a predictive layout, if the support is invoked the preProcess (), the method in partial refresh the article described the. If not, call the consumeUpdatesInOnePass() method. There are two main differences between the two approaches.

  1. PreProcess () reorders the local request data, moving the MOVE operation to the end
  2. PreProcess () flushes data locallyUpdateOpIn themPostponedList.
  3. PreProcess () splits the requested data into two parts. Display area and display area outside, screen update directly to Viewholder and Recycler. Off-screen basismPostponedListTo process the data and decide whether to update to Viewholder and Recycler.

The specific code was covered in the previous partial refresh, so you can take a look at it. ConsumeUpdatesInOnePass () internally handles locally refreshed data directly to Viewholder and Recycler.

What is predictive animation? Prediction is making a guess about something that hasn’t happened yet, and expecting the guess to be correct, maybe not correct. But do computers make predictions? Predictions are just calculations based on data. Does the animation need to make predictions? There are two possible scenarios,

  1. Animating the displayed item, such as performing the remove operation, will cause other items to be added. In this case, we need to predict the new entry item and set the animation motion.
  2. Multiple animations are superimposed on each other. For example, if a display area is removed and a new display is updated, the new update also needs to be animated.

So we rewrite the LayoutManager# supportsPredictiveItemAnimations () close the predictive animation, the effect of the above, interested can try.

The final calculated variables mRunSimpleAnimations and mRunPredictiveAnimations indicate whether to run animations and predictive animations, respectively.

The code excerpt above private void processAdapterUpdatesAndSetAnimationFlags () {... boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator ! = null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); . }Copy the code

MRunSimpleAnimations should be assigned to true if:

  1. MFirstLayoutComplete is true and that’s what was assigned when onLayout was executed. That is, finish the first layout. So the first time you do a layout, there’s no animation.
  2. MItemAnimator for ture means we need to set the animation Animator, RecyclerView has the default animation inside
  3. MDataSetHasChangedAfterLayout mDataSetHasChangedAfterLayout judgment to true said changed the data set, Call setAdapter, swapAdapter, notifyDataSetChanged, and so on. MDataSetHasChangedAfterLayout to false, the need to support local animation types of refresh animationTypeSupported said four operation will make animationTypeSupported come true. MDataSetHasChangedAfterLayout to true, only set up hasStableIds () tag, animation.

We called the notifyDataSetChanged method, which is generally considered to have no animation effect, but we set hasStableIds(), which has animation effect, so be aware of this.

The code excerpt above private void processAdapterUpdatesAndSetAnimationFlags () {... mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && ! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); . } private boolean predictiveItemAnimationsEnabled() { return (mItemAnimator ! = null && mLayout.supportsPredictiveItemAnimations()); }Copy the code

MRunPredictiveAnimations should be assigned to true if:

  1. MRunSimpleAnimations above are true and need to be able to run animations
  2. AnimationTypeSupported, described above
  3. The data set was not changed, that is, setAdapter, swapAdapter, notifyDataSetChanged, and so on were not called
  4. LayoutManager need supportsPredictiveItemAnimations () returns true. The default is false.

Perform processAdapterUpdatesAndSetAnimationFlags finished, by which the two values for data storage, if you can perform animations, store display ViewHolder location information, if you execute a predictive animation, Call onLayoutChildren to advance the layout. Let’s take a look at the code that supports performing animations

mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; 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, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); if (mState.mTrackOldChangeHolders && holder.isUpdated() && ! holder.isRemoved() && ! holder.shouldIgnore() && ! holder.isInvalid()) { long key = getChangedHolderKey(holder); mViewInfoStore.addToOldChangeHolders(key, holder); }}}Copy the code

To traverse the first display viewHolder, through recordPreLayoutInformation methods put the position information of the viewHolder ItemHolderInfo inside, Then through mViewInfoStore. Stored in addToPreLayout mViewInfoStore, set to FLAG_PRE state, mViewInfoStore animation center, responsible for animation information storage and execution. MTrackOldChangeHolders indicates whether the viewHolder performed the change operation, notityChanged. If related to change, so will mViewInfoStore. AddToOldChangeHolders storage viewHolder of this change.

Here’s a quick look at the ViewInfoStore class. He is responsible for the animation’s information storage, including new and old location information before and after the layout is completed. And perform animations with this old and new information. Is the core class for animation execution. The ViewHolder it stores internally has four states

state meaning
FLAG_DISAPPEARED Visible to invisible, such as the remove operation
FLAG_APPEAR No visible visible, new item displayed after remove operation
FLAG_PRE All items before the layout are set to this state, indicating that they belong to the old layout
FLAG_POST After the layout is complete, all items will set this state to indicate that they belong to the new layout

Let’s take a look at how predictive layout is implemented

If (mState. MRunPredictiveAnimations) {... mLayout.onLayoutChildren(mRecycler, mState); for (int i = 0; i < mChildHelper.getChildCount(); ++i) { final View child = mChildHelper.getChildAt(i); final ViewHolder viewHolder = getChildViewHolderInt(child); if (viewHolder.shouldIgnore()) { continue; } 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); } } } clearOldPositions(); } else { clearOldPositions(); }Copy the code

The general logic of the interior is to do onLayoutChildren for predictive layout, the purpose of which we talked about above. What’s the difference between calling onLayoutChildren in a normal layout

  1. The first ismState.mInPreLayoutSet it to true. This is an important variable, and previous analyses have used itmState.mInPreLayoutThe onLayoutChildren method performs a lot of unique logic internally to support predictive layout. In extraction cache when, for example, from the secondary test when performing validateViewHolderForOffsetPosition method, legitimacy if remove the ViewHolder remove had been made, and they depend on isPreLayout legitimacy, That is, is it in the predictive layout phase.
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
    if (holder.isRemoved()) {
        return mState.isPreLayout();
    }
Copy the code
  1. ProcessAdapterUpdatesAndSetAnimationFlags methods dealing with partial refresh data above, if the support predictive layout, will be filled inmPostponedListCollection. Cache are extracted from four level cache, through mAdapterHelper. Through findPositionOffset methodmPostponedListDetermines the latest position of the current position.
  2. The extract from the cache is one more levelmChangedScrapThis level of cache mainly stores notifyChanged’s viewHolder. Primarily dealing with the ViewHolder of the notifyChanged design.
void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
Copy the code
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
    return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
            viewHolder.getUnmodifiedPayloads());
}
Copy the code
@ Override DefaultItemAnimator within public Boolean canReuseUpdatedViewHolder (@ NonNull ViewHolder ViewHolder, @NonNull List<Object> payloads) { return ! payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); }Copy the code

MChangedScrap must be holder. IsUpdated (). CanReuseUpdatedViewHolder (holder) is false, this method returns false said need to update the animation. In the RecyclerView DefaultItemAnimator, it checks whether the payloads are empty. If null, the updated viewHolder is cached in mChangedScrap, whereas the updated viewHolder is cached in mAttachedScrap. During cache extraction, mChangedScrap is first extracted from the predictive layout phase. The mChangedScrap will be emptied after the extraction is complete and no other levels of cache will be placed. If the viewHolder cannot be retrieved from the cache, a new viewHolder will be created to support the animation of change. By default, the viewHolder will fade out. So we call notifyItemChanged(int Position, Object Payload), payload sends the payload that is not null, and it only takes effect on DefaultItemAnimator. That is, the cache is recycled into mAttachedScrap, which can be reused. There’s no animation at this point.

The above differences, may not be well understood, give you an example here, for example, we remove a display area location for 2 item, so the screen is not visible outside the item need to be performed up animation display, so we need to know his position, before and after the animation animation before position is the location of the normal layout before. How do you know where you are in front of the layout, which he hasn’t shown yet, by combining the two points above. In isPreLayout () is true, from the secondary cache to take out the position of 2 viewHolder, he must be isRemoved state, because processAdapterUpdatesAndSetAnimationFlags method to deal with local refresh data, The state of the ViewHolder is refreshed. Her availability is available because isPreLayout is true. However, mIgnoreConsumed variable is set to true below, that is, no space consumed. Since the ViewHolder being removed does not consume space, the invisible view will also be calculated, and the exact position of the item that is not displayed below will be obtained in advance. You can now animate the beginning and end positions.

if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
}
Copy the code

From the remove example above, you get a general idea of the logic of predictive layout onLayoutChildren. The other local flush logic is similar. With this predictive onLayoutChildren relayout, we get the old data we need to animate before the actual layout. The following should be stored. Pass mViewInfoStore. IsInPreLayout (viewHolder) judgment, in logic, the running animation on the screen will enter the viewHolder isInPreLayout state, so there will only be stored new add to the view of viewGroup. Combined with the above examples, it should be clear. Into a state. At last, by mViewInfoStore addToAppearedInPreLayoutHolders let ViewHolder Appeared. That is, never seen or seen.

Through the above analysis, the storage of old data is completed through the execution process that supports animation and supports predictive animation. Now we will execute dispatchLayoutStep2() for the actual layout, the specific process can see the previous detailed introduction of the article. DispatchLayoutStep3 () continues to store the new data after the layout and uses the old and new data to perform the animation.

dispatchLayoutStep3

DispatchLayoutStep3 method through dispatchLayoutStep2 mapping completed, get the existing latest location. And finally call ViewInfoStore#process to run the animation.

if (mState.mRunSimpleAnimations) { for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore()) { continue; } long key = getChangedHolderKey(holder); final ItemHolderInfo animationInfo = mItemAnimator .recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); if (oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { 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( oldChangeViewHolder); 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); } } mViewInfoStore.process(mViewInfoProcessCallback); }Copy the code

The above logic traversing the latest ViewHolder and were compared with the old data, the end result will pass ViewInfoStore. AddToPostLayout in a to ViewInfoStore. AnimateChange is handled directly here, and all other animations are handled uniformly in viewinfostore.process. The detailed logic can be read for yourself if you are interested. Take a look at the process method that ultimately executes the animation.

static final int FLAG_DISAPPEARED = 1; static final int FLAG_APPEAR = 1 << 1; static final int FLAG_PRE = 1 << 2; static final int FLAG_POST = 1 << 3; static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; void process(ProcessCallback callback) { for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); final InfoRecord record = mLayoutHolderMap.removeAt(index); if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { callback.unused(viewHolder); } else if ((record.flags & FLAG_DISAPPEARED) ! = 0) { if (record.preInfo == null) { callback.unused(viewHolder); } else { callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); } } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { callback.processAppeared(viewHolder, record.preInfo, record.postInfo); } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { callback.processPersistent(viewHolder, record.preInfo, record.postInfo); } else if ((record.flags & FLAG_PRE) ! = 0) { callback.processDisappeared(viewHolder, record.preInfo, null); } else if ((record.flags & FLAG_POST) ! = 0) { 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 above logic is interesting. At this point, the data before and after the mapping is mapped to record.flags, where the design of the bit operation, each bit represents a state. The four states we talked about earlier when we introduced ViewInfoStore. Generally speaking, it is the state of the end of the initial trial and the state of the visibility change. So different combinations represent different meanings. FLAG_PRE_AND_POST, for example, indicates that the ViewHolder is present at the end of the initial trial and has not experienced any visible line changes, so processPersistent is executed directly. The state of other combinations is similar to this logic. This method can also be used for reference in our business for state transfer judgment. Through different judgments, then through ViewCompat. PostOnAnimation, perform mItemAnimator. RunPendingAnimations () to perform the animation. The animation is executed. If you are interested in the animation logic, see DefaultItemAnimator instead of analyzing it here.