This article is the fourth article of RecyclerView source code analysis series, the content is mainly based on the first three articles to narrate, so before reading recommended to look at the first three articles:

Basic design structure of RecylcerView

RecyclerView refresh mechanism

Reuse mechanism of RecyclerView

This paper mainly analyzes the realization principle of RecyclerView delete animation, the general realization process of different types of animation is almost the same, so for adding and exchanging this kind of animation will no longer do the analysis. The main goal of this article is to understand clearly RecyclerViewItem remove animation source code logic. The article is longer.

The delete animation of RecyclerView can be triggered by the following two methods:

/ / an item delete animation dataSource. The removeAt (1) recyclerView. Adapter. NotifyItemRemoved (1) / / multiple item delete animation dataSource. RemoveAt (1) The dataSource. RemoveAt (1) recyclerView. Adapter. NotifyItemRangeRemoved (1, 2)Copy the code

The following figure shows the effect of deleting an animation when the animation length is set to 10x. We can first envision how this animation can be roughly implemented:

Next, combine the content of the previous few articles and follow the source code to see how RecyclerView is to achieve this animation:

Adapter. NotifyItemRemoved (1) the callback to RecyclerViewDataObserver:

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

The onItemRangeRemoved() method is used to remove the Item. The animation is divided into two parts:

  1. Add aUpdateOptoAdapterHelper.mPendingUpdatesIn the.
  2. triggerUpdateProcessor()Call therequestLayout, that is, triggeredRecyclerViewRelayout.

Start with mAdapterHelper. OnItemRangeRemoved (positionStart itemCount) :

AdapterHelper

This class is used to record adapter.notifyXXX action, that is, every Operation(add, delete) will have a corresponding record in this class UpdateOp, RecyclerView will check these UpdateOp layout, and do the corresponding Operation. MAdapterHelper onItemRangeRemoved actually is to add a Remove UpdateOp:

    mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
    mExistingUpdateTypes |= UpdateOp.REMOVE;
Copy the code

Add a Remove UpdateOp to the mPendingUpdates collection.

RecyclerView.layout

In the refresh mechanism of RecyclerView, the RecyclerView layout is divided into 3 steps :dispatchLayoutStep1(), dispatchLayoutStep2(), dispatchLayoutStep3(). Next, let’s look at the work of the Item deletion animation in these three steps.

DispatchLayoutStep1 (Save animation live)

Start with dispatchLayoutStep1() directly, this is the first step in RecyclerView layout:

dispatchLayoutStep1():

    private void dispatchLayoutStep1() {... processAdapterUpdatesAndSetAnimationFlags(); .if(mState.mRunSimpleAnimations) { ... }... }Copy the code

I just posted above Item delete animation mainly involves part, first take a look at processAdapterUpdatesAndSetAnimationFlags triggers () operation, the whole operation chain is longer, not with one by one, It is actually a call to AdapterHelper. PostponeAndUpdateViewHolders () :

private void postponeAndUpdateViewHolders(UpdateOp op) { mPostponedList.add(op); // switch (op.cmd) {// switch (op.cmd) {case UpdateOp.ADD:
            mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break;
        case UpdateOp.MOVE:
            mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break;
        case UpdateOp.REMOVE:
            mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount); break;  
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); break; . }}Copy the code

This method adds the updates from mPendingUpdates to the mPostponedList and calls the mCallback back and forth according to op.cmd. The mCallback is called back into RecyclerView:

 void offsetPositionRecordsForRemove(int positionStart, int itemCount, boolean applyToPreLayout) {
        final int positionEnd = positionStart + itemCount;
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for(int i = 0; i < childCount; i++) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); .if (holder.mPosition >= positionEnd) {
                holder.offsetPosition(-itemCount, applyToPreLayout);
                mState.mStructureChanged = true; }... }... }Copy the code

OffsetPositionRecordsForRemove methods: mainly to display the current position in the interface of the ViewHolder corresponding changes, or if the item is located in the delete item, then its position should be minus one, such as the original position is 3 now turned into 2.

Moving on to the action in dispatchLayoutStep1() :

    if (mState.mRunSimpleAnimations) {
        int count = mChildHelper.getChildCount();
        for(int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); // Create an ItemHolderInfo final ItemHolderInfo animationInfo = mItemAnimator based on the ViewHolder layout information currently displayed on the screen .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); // Save animationInfo to mViewInfoStore... }}Copy the code

That’s two things:

  1. For each one currently displayed on the screenViewHolderTo create aItemHolderInfo.ItemHolderInfoIn fact, it saves the current displayitemviewThe layout of theThe top and leftInformation such as
  2. With aViewHolderAnd its correspondingItemHolderInfocallmViewInfoStore.addToPreLayout(holder, animationInfo).

MViewInfoStore. AddToPreLayout () is to keep these information:

void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.preInfo = info;
    record.flags |= FLAG_PRE;
}
Copy the code

That is, save the holder and info to mLayoutHolderMap. It is a collection of information stored in the ViewHolder interface before the animation is executed.

The logic for executing AdapterHelper and dispatchLayoutStep1() when executing the Items delete animation is outlined here.

In fact, these operations can be simply understood as saving the scene of the View before animation. In fact, there is a pre-layout, pre-layout is also to save the View information before the animation, but I won’t talk about it here.

dispatchLayoutStep2

This step is to place the remaining items in the current Adapter, or in this example, the remaining five items in sequence. In the previous article, RecyclerView refresh mechanism, we know that the LinearLayoutManager will ask RecyclerView to fill RecyclerView, so the RecyclerView to fill several views has a lot to do with Recycler. Because Recycler doesn’t give LinearLayoutManager, RecyclerView won’t have View fill. What are the boundary conditions of Recycler to LinearLayoutManager View? Let’s take a look at tryGetViewHolderForPositionByDeadline () method:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:"+ mState.getItemCount() + exceptionLabel()); }}Copy the code

If the location is greater than mstate.getitemCount (), then the RecyclerView will not be populated with child views. Mstate.getitemcount () is the number of data sources currently in the Adapter. So after this step, the View looks like the following:

That’s when you start asking questions, right? Animation? How did it go straight to the final look? Don’t worry, this step is just the layout, and as for how the animation works, we’ll read on:

DispatchLayoutStep3 (Perform delete animation)

DispatchLayoutStep3 () will animate the delete operation after it has been laid out in the previous step:

private void dispatchLayoutStep3() {...if(mState.mRunSimpleAnimations) { ... mViewInfoStore.process(mViewInfoProcessCallback); // Trigger animation execution}... }Copy the code

Mviewinfostore.process () can be divided into two operations:

  1. The firstItem ViewStart state ready before animation
  2. Perform animationItem ViewTo target layout location

Let’s continue with the mviewinfoStore.process () method

theItem ViewStart state ready before animation

 void process(ProcessCallback callback) {
        for(int index = mLayoutHolderMap.size() - 1; index >= 0; 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) { callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); // The deleted item is called back to this place}else if((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { callback.processPersistent(viewHolder, record.preInfo, record.postInfo); // Items that need to be moved up are called back to this place}... InfoRecord.recycle(record); }}Copy the code

This step is to iterate over the mLayoutHolderMap and animate each ViewHolder in it. Here, the callback will be transferred to RecyclerView RecyclerView corresponding animation can be carried for each Item:

ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
        new ViewInfoStore.ProcessCallback() { @Override public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,@Nullable ItemHolderInfo postInfo) { mRecycler.unscrapView(viewHolder); // Remove from the Scrap set animatepattern (viewHolder, info, postInfo); } @Override public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { ...if(mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { postAnimationRunner(); }}... }}Copy the code

First, analyze the disappearing animation of the deleted Item:

Put the disappearing animation of Item intomPendingRemovalsWaiting queue

void animateDisappearance(@NonNull ViewHolder holder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    if(mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { postAnimationRunner(); }}Copy the code

Change Holderattch to RecyclerView (this is because the Holder has already been Dettach in dispatchLayoutStep1 and dispatchLayoutStep2). That is, it reappeared in RecyclerView layout (the position is still not deleted before of course). And then call the mItemAnimator. AnimateDisappearance () the execution of this delete animation, mItemAnimator is RecyclerView implementers of the animation, it is the corresponding DefaultItemAnimator. Continue to see animateDisappearance () it’s final call to DefaultItemAnimator. AnimateRemove () :

public boolean animateRemove(final RecyclerView.ViewHolder holder) {
    resetAnimation(holder);
    mPendingRemovals.add(holder);
    return true;
}
Copy the code

That is, the animation is not actually executed, but the holder is placed in the mPendingRemovals set, which is likely to be executed later.

Moves an animation of the Item that has not been removed intomPendingMovesWaiting queue

Logic and it actually almost DefaultItemAnimator. AnimatePersistence () :

public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if(preInfo.left ! = postInfo.left || preInfo.top ! Postinfo.top) {// Execute move animation if the state is different from that of the pre-layoutreturnanimateMove(viewHolder,preInfo.left, preInfo.top, postInfo.left, postInfo.top); }... }Copy the code

The animateMove logic is very simple. It constructs a MoveInfo based on the offset and adds it to mPendingMoves.

public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
    final View view = holder.itemView;
    fromX += (int) holder.itemView.getTranslationX();
    fromY += (int) holder.itemView.getTranslationY();
    resetAnimation(holder);
    int deltaX = toX - fromX;
    int deltaY = toY - fromY;
    if (deltaX == 0 && deltaY == 0) {
        dispatchMoveFinished(holder);
        return false;
    }
    if(deltaX ! = 0) { view.setTranslationX(-deltaX); // Set their position to negative offset !!!!! }if(deltaY ! = 0) { view.setTranslationY(-deltaY); // Set their position to negative offset !!!!! } mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));return true;
}
Copy the code

Note that this step sets the height of the deleted Item to a negative for both the TranslationX and the TranslationY of the View to be animated, as shown below

That is, all items after the deleted Item are moved down

postAnimationRunner()Execute all pending animations

PostAnimationRunner () will execute the animation from pendding:

//DefaultItemAnimator.java

    public void runPendingAnimations() { boolean removalsPending = ! mPendingRemovals.isEmpty(); .for(RecyclerView.ViewHolder holder : mPendingRemovals) { animateRemoveImpl(holder); // Execute pending removals} mPendingRemovals. Clear ();if(! MPendingMoves. IsEmpty ()) {// Perform pending moves. Final ArrayList<MoveInfo> Moves = new ArrayList<>(); moves.addAll(mPendingMoves); mMovesList.add(moves); mPendingMoves.clear(); Runnable mover = newRunnable() {
                @Override
                public void run() {
                    for(MoveInfo moveInfo : moves) { animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, moveInfo.toX, moveInfo.toY); } moves.clear(); mMovesList.remove(moves); }};if (removalsPending) {
                View view = moves.get(0).holder.itemView;
                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
            } else{ mover.run(); }}... }Copy the code

AnimateRemoveImpl and animateMoveImpl source code details I will not post, directly to say what they do:

  1. animateRemoveImplAnimate the removed Item with opacity from (1 to 0)
  2. animateMoveImpltheirTranslationXandTranslationYLet’s go to zero.

I’ll post a GIF of the deleted animation again, so you can see if this is the execution step:

Welcome to my Android advancement plan. See more dry goods