Android RecyclerView工作原理分析(下)

前言
  前文已经在整体上对RecyclerView的实现作出了剖析,但是有些细节上,并没有做太过深入的解释,本文将针对RecyclerView的动画作更深入剖析。    
pre&post layout
  在RecyclerView中存在一个叫“预布局”的阶段,当然这个是我自己作的翻译,本来叫pre layout,与之对应的还有个叫post layout的阶段,它们分别发生在真正的子控件测量&布局的前后。其中pre layout阶段的作用是记录数据集改变前的子控件信息,post layout阶段的作用是记录数据集改变后的子控件信息及触发动画。

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
       ...
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
    ...
}

方法dispatchLayout()会在RecyclerView.onLayout()中被调用,其中dispatchLayoutStep1就是pre layout,dispatchLayoutStep3就是post layout,而dispatchLayoutStep2自然就是处理真正测量&布局的了。 首先来看看pre layout时都记录了什么内容:

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 = ...
            ...
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(...);
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            ...
        }
    }
    ...
}

类ItemHolderInfo中封闭了对应ItemView的边界信息,即ItemView的left、top、right、bottom值。对象mViewInfoStore的作用正如源码注释:

/**
 * Keeps data about views to be used for animations
 */
final ViewInfoStore mViewInfoStore = new ViewInfoStore();

再来看看addToPreLayout()方法:

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

由上可已看出RecyclerView将pre layout阶段的ItemView信息存放在了ViewInfoStore中的mLayoutHolderMap集合中。 接下来我们看看post layout阶段:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        ...
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ...
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            ...
            if (...) {
                ...
                animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // Step 4: Process view info lists and trigger animations
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    ...
}

这是addToPostLayout()方法:

void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.postInfo = info;
    record.flags |= FLAG_POST;
}

与pre layout阶段相同RecyclerView也是将post layout阶段的ItemView信息存放在mViewInfoStore的mLayoutHolderMap集合中,并且不难看出,同一个ItemView(或者叫ViewHolder)的pre layout信息与post layout信息封装在了同一个InfoRecord中,分别叫InfoRecord.preInfo与InforRecord.postInfo,这样InfoRecord就保存着同一个ItemView在数据集变化前后的信息,我们可以根据此信息定义动画的开始和结束状态。


这里写图片描述

如上图所示,当我们插入A时,在完成了上文所诉过程后,以ItemView2为例,通过比较它的preInfo与postInfo——都为非空,源码中是以标志位的形式实现的,就可以知道它将执行MOVE操作;而A自然就是ADD操作。下面是ViewInfoStore.ProcessCallback实现中的其中一个方法,它会在mViewInfoStore.process()方法中被调用:

public void processPersistent(...) {
        ...
        if (mDataSetHasChangedAfterLayout) {
            ...
        } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
            postAnimationRunner();
        }
}

我们知道,RecyclerView中ItemAnimator的默认实现是DefaultItemAnimator,这里我就只以默认实现来说明,这是animatePersistence()方法:

public boolean animatePersistence(...) {
    if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
        ...
        return animateMove(viewHolder,
                preInfo.left, preInfo.top, postInfo.left, postInfo.top);
    }
    dispatchMoveFinished(viewHolder);
    return false;
}

当然这个方法在DefaultItemAnimator的父类SimpleItemAnimator中,通过比较preInfo与postInfo的left和top属性分别确定ItemView在水平或垂直方向是否要执行MOVE操作,而上面的方法postAnimationRunner()就是用来触发动画执行的。
  通过前文我们知道,RecyclerView中定义了4种针对数据集的操作(也可以称为针对ItemView的操作),分别是ADD、REMOVE、UPDATE、MOVE,RecyclerView就是通过比较preInfo与postInfo来确定ItemView要执行哪种操作的,上文我描述了MOVE情况,这个比较过程是在方法ViewInfoStore.process()中实现的,其它情况我就不再赘述了,各位不妨自己去看看。
  在DefaultItemAnimator中实现了上面4种操作下的动画。当postAnimationRunner()执行后,会触发DefaultItemAnimator.runPendingAnimations()方法的调用,这个方法过长,我这里只作下解释便可。4种操作对应的动画是有先后顺序的,remove–>move&change–>add,之所以有这样的顺序,不难看出是为了不让ItemView之间有重叠的区域,这个顺序是由ViewCompat.postOnAnimationDelayed()方法通过控制延时来实现的。在DefaultItemAnimator中,REMOVE和ADD对应的是淡入淡出动画(改变透明度),MOVE对应的是平移动画;UPDATE相对来说要复杂一些,是因为它不再是记录同一个ItemView的变化情况,而是记录2个ItemView的信息来作比较,pre layout阶段的信息来自“oldChangeViewHolder”,post layout阶段的信息来自“holder”,这两个对象在dispatchLayoutStep3方法中可以找到,而且,这2个ItemView的动画是同时执行的,所以它对应的动画是:“oldHolder”淡出且向“newHolder”平移,同时“newHolder”淡入。特别说明,前文有提过一个叫scrapped的集合,其实它除了保存REMOVE操作的ItemView,还保存着UPDATE操作中的“oldHolder”!  
 以上就是RecyclerView默认动画的具体实现逻辑了,总结下来就是:当数据集发生变化时,会导致RecyclerView重新测量&布局子控件,我们记录下这个变化前后的RecyclerView的快照(preInfo与postInfo),通过比较这2个快照,从而确定子控件要执行什么操作,最后再实现不同操作下对应的动画就好了。通常我们会调用notifyItemXXX()系列方法来通知RecyclerView数据集变化,这些方法之所以比notifyDataSetChanged()高效的原因就是它们不会让整个RecyclerView重新绘制,而是只重绘具体的子控件,并且通过动画连接子控件的前后状态,这样也就实现了在Material design中所讲的“Visual continuity”效果。
子控件的测量与布局
  这一节将对preInfo与postInfo是如果确定(赋值)的,作进一步描述。   从前文我们知道,子控件的测量与布局其实在RecyclerView的测量阶段(onMeasure)就执行完了,这样做是为了支持WRAP_CONTENT,具体的方法呢就是dispatchLayoutStep1()与dispatchLayoutStep2(),同样这两个方法也会出现在RecyclerView的布局阶段(onLayout),但并不是说它们就会被调用,这里的调用逻辑是由RecyclerView.State类控制的,它定义了RecyclerView的整个测量布局过程,分为3步STEP_START、STEP_LAYOUT、STEP_ANIMATIONS,具体流程是:初始状态是STEP_START;如果RecyclerView当前在STEP_START阶段dispatchLayoutStep1()会执行,记录下preInfo,将状态改为STEP_LAYOUT;如果RecyclerView在STEP_LAYOUT阶段dispatchLayoutStep2()会执行,测量布局子控件,将状态改为STEP_ANIMATIONS;如果RecyclerView在STEP_ANIMATIONS阶段dispatchLayoutStep3()会执行,记录下postInfo,触发动画,将状态改为STEP_START。每次数据集更改都会执行上述3步。   在测量布局子控件的过程中,最重要的莫过于确定布局锚点了,以LinearLayoutManager垂直布局为例,在onLayoutChildren()方法中,会调用updateAnchorInfoForLayout()方法来确定布局锚点:

private void updateAnchorInfoForLayout(...) {
    if (updateAnchorFromPendingData(state, anchorInfo)) {
        ...
        return;
    }

    if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
        ...
        return;
    }
    ...
    anchorInfo.assignCoordinateFromPadding();
    anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}

这里布局锚点的确定方法有3种依据。首先,如果是第一次布局(没有ItemView),这种情况已经在前文有过描述了,这里就不再说明;剩余的2种分别是“滑动位置”与“子控件”,这2种情况都是发生在已经有ItemView时的,而且这里的“滑动位置”是指由方法scrollToPosition()确认的,并赋给了mPendingScrollPosition变量。现在先来看看“滑动位置”updateAnchorFromPendingData()方法:

private boolean updateAnchorFromPendingData(...) {
    ...
    // if child is visible, try to make it a reference child and ensure it is fully visible.
    // if child is not visible, align it depending on its virtual position.
    anchorInfo.mPosition = mPendingScrollPosition;
    ...
    if (mPendingScrollPositionOffset == INVALID_OFFSET) {
        View child = findViewByPosition(mPendingScrollPosition);
        if (child != null) {
            ...
        } else { // item is not visible.
            ...
        }
        return true;
    }
    ...
    return true;
}

布局锚点中的mCoordinate与mPosition,在前文描述为起始绘制偏移量与索引位置,再直白点就是屏幕位置与数据集位置,就是告诉RecyclerView从屏幕的mCoordinate位置开始填充子控件,与子控件绑定的数据从数据集的mPosition位置开始取得。上面这个方法中确定“屏幕位置”分为2种情况,就是对应于mPendingScrollPosition是否存在子控件,mCoordinate值的确定我就不再讲述了,无非是一边界判断的语句。   下面来看看“子控件”依据的情况,这是updateAnchorFromChildren():

private boolean updateAnchorFromChildren(...) {
    ...
    View referenceChild = anchorInfo.mLayoutFromEnd
            ? findReferenceChildClosestToEnd(recycler, state)
            : findReferenceChildClosestToStart(recycler, state);
    if (referenceChild != null) {
        anchorInfo.assignFromView(referenceChild);
        ...
        return true;
    }
    return false;
}

这种情况也并不复杂,就是找到最外边的一个子控件,以它的位置信息来确定布局锚点,就是方法assignFromView(),我也就不再列出来了。以上就是详细的布局锚点确认过程了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容