RecyclerView动画原理

大家有没有想过,scrap缓存存在的意义是什么?为什么存了后,又把它里面的viewholder又给删了?

在上一篇(RecyclerView源码走读)中我们分析过更新和删除表项的过程,下面来总结下scrap缓存的过程:

更新表项:页面上有表项0到9,总共有10个表项,然后更新表项0,在dispatchLayout1阶段(pre-layout),先把页面上的表项0到9存到scrap缓存中,由于表项0是更新的所以会把表项0放到changeScrap缓存中,把表项1到9存到attachScrap缓存中。同时会把这10个表项从页面上分离(child的parent置空,并把viewgroup中对应的child给置空),接着从scrap缓存中取出viewholder,由于存在更新表项,所以会去创建表项10的viewholder到recyclerview上。此时recyclerview中有11个表项了,同时把他们从scrap缓存中移除。 在dispatchLayout2阶段(post-layout)阶段,会把这11个表项加入到scrap缓存中,表项0还是放到changeScrap中,表项1到10加入到attachScrap缓存中。接着把他们从页面上分离。然后在fill中,从scrap缓存中获取viewholder,由于changeScrap不会在dispatchLayout2阶段生效,所以会创建了表项0,其余的表项正常从attachScrap缓存中获取。接着把所有的表项添加到recyclerview中,并把scrap中的viewholder缓存移除掉。但是表项10不会添加到recyclerview中,因为到表项9的时候,剩余空间就不够了,所以recyclerview中只有0到9的表项。注意:此时还有表项0个表项10分别存在于changeScrap和attachScrap中。

上面更新表项经历了两次的布局,分别是dispatchLayout1和dispatchLayout2,在这两个步骤中,先加入到scrap缓存中,然后再从scrap缓存中移除。从现象来看,dispatchLayout1中会生成11个表项,分别是0到10,然后在dispatchLayout2先移除所有的表项,然后添加0到9的表项,而在dispatchLayout2中表项0是重新生成的,因为它要实现更新。而调用两次的布局是为了实现动画而这么做,先生成11个表项,来把他们的位置都记录下来,然后第二次布局的时候把10个表项的位置也记录下来,然后最后根据存储的位置信息做动画。下面来分析下动画如何实现的:

在dispatchLayoutStep1将页面上可见的表项加入到ViewInfoStore中: alt text

1
2
3
4
5
6
7
8
9
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;
}

将viewholder和infoRecord绑定好关系,然后给该record添加上FLAG_PRE标记。注意:此处的InforRecord使用了对象池。 前面分析过在fill完之后,页面上会多出一个表项10,此时也会添加到ViewInfoStore中: alt text 可以看到在onLayoutChildren之后会调用addToAppearedInPreLayoutHolders,此时只有表项10会调用addToAppearedInPreLayoutHolders:

1
2
3
4
5
6
7
8
9
void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.flags |= FLAG_APPEAR;
    record.preInfo = info;
}

所以只有表项10添加了FLAG_APPEAR标记。

在dispatchLayoutStep2走完后,页面上可见的表项就只剩0到9了,在dispatchLayoutStep3中将变化的viewholder调用animateChange,而其他的viewholder调用了addToPostLayout,在该例子中表项0会调用animateChange,表项1到9会调用addToPostLayout:

animateChange:当有viewholder发生变化的时候,会触发该方法,该方法会触发mItemAnimator的animateChange方法:

1
2
3
4
5
6
7
8
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
        boolean oldHolderDisappearing, boolean newHolderDisappearing) {
    //省略代码
    if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
        postAnimationRunner();
    }
}

调用了mItemAnimator的animateChange方法,并把改变之前的holder、itemholderinfo和改变之后的holder、itemholderinfo传进去了,默认的itemAnimator是DefaultItemAnimator,也是一个SimpleItemAnimator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder,
        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if (DEBUG) {
        Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView);
    }
    final int fromLeft = preInfo.left;
    final int fromTop = preInfo.top;
    final int toLeft, toTop;
    if (newHolder.shouldIgnore()) {
        toLeft = preInfo.left;
        toTop = preInfo.top;
    } else {
        toLeft = postInfo.left;
        toTop = postInfo.top;
    }
    return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
}

fromLeft、fromTop是老的holder的坐标,toLeft、toTop是新的holder的坐标,最后调用了另外一个animateChange:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
        int fromX, int fromY, int toX, int toY) {
    if (oldHolder == newHolder) {
        // Don't know how to run change animations when the same view holder is re-used.
        // run a move animation to handle position changes.
        return animateMove(oldHolder, fromX, fromY, toX, toY);
    }
    final float prevTranslationX = oldHolder.itemView.getTranslationX();
    final float prevTranslationY = oldHolder.itemView.getTranslationY();
    final float prevAlpha = oldHolder.itemView.getAlpha();
    resetAnimation(oldHolder);
    int deltaX = (int) (toX - fromX - prevTranslationX);
    int deltaY = (int) (toY - fromY - prevTranslationY);
    // recover prev translation state after ending animation
    oldHolder.itemView.setTranslationX(prevTranslationX);
    oldHolder.itemView.setTranslationY(prevTranslationY);
    oldHolder.itemView.setAlpha(prevAlpha);
    if (newHolder != null) {
        // carry over translation values
        resetAnimation(newHolder);
        newHolder.itemView.setTranslationX(-deltaX);
        newHolder.itemView.setTranslationY(-deltaY);
        newHolder.itemView.setAlpha(0);
    }
    mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
    return true;
}

如果新老holder是同一个,则调用animateMove,前面分析过oldHolder和newHolder不是同一个,所以先把newHolder的translationX和translationX设置到目标位置,其实此时的deltaX和deltaY为0,因为目标的viewholder位置也在原来表项0的位置,接着把透明度设置为0,最后创建了ChangeInfo,放入到mPendingChanges中。当animateChange返回true后,会调用postAnimationRunner方法:

1
2
3
4
5
6
void postAnimationRunner() {
    if (!mPostedAnimatorRunner && mIsAttached) {
        ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
        mPostedAnimatorRunner = true;
    }
}

ViewCompat.postOnAnimation(this, mItemAnimatorRunner)是给Choreographer中添加一条CALLBACK_ANIMATION类型的事件,等到下一个vsync信号来的时候,就会执行mItemAnimatorRunner:

1
2
3
4
5
6
7
8
9
private Runnable mItemAnimatorRunner = new Runnable() {
    @Override
    public void run() {
        if (mItemAnimator != null) {
            mItemAnimator.runPendingAnimations();
        }
        mPostedAnimatorRunner = false;
    }
};

最终会执行DefaultItemAnimator的runPendingAnimations方法,该方法会执行changesPending的逻辑,最终会执行animateChangeImpl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void animateChangeImpl(final ChangeInfo changeInfo) {
    final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
    final View view = holder == null ? null : holder.itemView;
    final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
    final View newView = newHolder != null ? newHolder.itemView : null;
    if (view != null) {//改变之前的view
        final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
                getChangeDuration());
        mChangeAnimations.add(changeInfo.oldHolder);
        oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
        oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
        oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animator) {
                dispatchChangeStarting(changeInfo.oldHolder, true);
            }
            @Override
            public void onAnimationEnd(Animator animator) {
                oldViewAnim.setListener(null);
                view.setAlpha(1);
                view.setTranslationX(0);
                view.setTranslationY(0);
                dispatchChangeFinished(changeInfo.oldHolder, true);
                mChangeAnimations.remove(changeInfo.oldHolder);
                dispatchFinishedWhenDone();
            }
        }).start();
    }
    if (newView != null) {//改变之后的view
        final ViewPropertyAnimator newViewAnimation = newView.animate();
        mChangeAnimations.add(changeInfo.newHolder);
        newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
                .alpha(1).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchChangeStarting(changeInfo.newHolder, false);
                    }
                    @Override
                    public void onAnimationEnd(Animator animator) {
                        newViewAnimation.setListener(null);
                        newView.setAlpha(1);
                        newView.setTranslationX(0);
                        newView.setTranslationY(0);
                        dispatchChangeFinished(changeInfo.newHolder, false);
                        mChangeAnimations.remove(changeInfo.newHolder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }
}

该方法里面通过view的animate方法给view做动画,改变之前的view位置不会发生变化,只会发生alpha的动画,而改变之后的view会做从0到1的透明度动画。所以最终页面上会出现闪的一下,这个是做更新操作。 上面分析的是表项0会调用animateChange,表项1到9会执行addToPostLayout:

1
2
3
4
5
6
7
8
9
void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.postInfo = info;
    record.flags |= FLAG_POST;
}

在dispatchLayout1的时候,表项1到9会添加上FLAG_PRE标记,接着在此处又添加上FLAG_POST标记,表项10添加了FLAG_APPEAR标记。所有的标记添加完后,最终会执行ViewInfoStore的process:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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_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_APPEAR) != 0) {
            // Scrap view. RecyclerView will handle removing/recycling this.
        }
        InfoRecord.recycle(record);
    }
}

这里省略了无关紧要的代码,表项1到9会调用callback.processPersistent回调,表项10不做相关更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public void processPersistent(ViewHolder viewHolder,
        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    viewHolder.setIsRecyclable(false);
    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();
    }
}

由于mDataSetHasChangedAfterLayout是在setAdapter中设置的,所以会走mItemAnimator.animateChange逻辑,在上面分析过,如果两个viewholder是同一个,则调用animateMove方法,而位置信息又不发生变化,所以animateChange返回false,因此也不会触发postAnimationRunner。

recyclerview中只要被添加到页面上的viewholder,并且是scrap缓存中的,最终都会从scrap缓存中移除。而在上面的例子中,分析dispatchLayout2的时候总共有11个表项,分别是原来的表项0(存在于changeScrap缓存中),dispatchLayout1创建的表项10(存在于attachScrap中)。在dispatchLayout3做完动画后,会清空掉scrap缓存,也就是调用了mLayout.removeAndRecycleScrapInt方法:

1
2
3
4
5
void removeAndRecycleScrapInt(Recycler recycler) {
    //省略代码
    recycler.clearScrap();
    //省略代码
}

最终会调用clearScrap来清空scrap缓存。

关于删除表项的动画处理,可以看这里:RecyclerView 动画原理 | 如何存储并应用动画属性值?

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy