RecyclerView刷新机制

前面分析了RecyclerView的基本结构
本文继续来看一下RecyclerView是如何完成UI的刷新以及在滑动时子View的添加逻辑

本文会从源码分析两件事 :

  1. adapter.notifyXXX()时RecyclerView的UI刷新的逻辑,即子View是如何添加到RecyclerView中的。
  2. 在数据存在的情况下,滑动RecyclerView子View是如何添加到RecyclerView并滑动的。

本文不会涉及到RecyclerView的动画,动画的实现会专门在一篇文章中分析。

adapter.notifyDataSetChanged()引起的刷新

我们假设RecyclerView在初始状态是没有数据的,然后往数据源中加入数据后,调用adapter.notifyDataSetChanged()来引起RecyclerView的刷新:

data.addAll(datas)
adapter.notifyDataSetChanged()

用图描述就是下面两个状态的转换:


adapter.notifyDataSetChanged.png

接下来就来分析这个变化的源码,在上一篇文章中已经解释过,adapter.notifyDataSetChanged()时,会引起RecyclerView重新布局(requestLayout),RecyclerViewonMeasure就不看了,核心逻辑不在这里。因此从onLayout()方法开始看:

RecyclerView.onLayout

这个方法直接调用了dispatchLayout:

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        dispatchLayoutStep2();
    } else if (数据变化 || 布局变化) {
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
}

上面我裁剪掉了一些代码,可以看到整个布局过程总共分为3步, 下面是这3步对应的方法:

STEP_START ->  dispatchLayoutStep1()
STEP_LAYOUT -> dispatchLayoutStep2()
STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()

第一步STEP_START主要是来存储当前子View的状态并确定是否要执行动画。这一步就不细看了。 而第3步STEP_ANIMATIONS是来执行动画的,本文也不分析了,本文主要来看一下第二步STEP_LAYOUT,即dispatchLayoutStep2():

dispatchLayoutStep2()

先来看一下这个方法的大致执行逻辑:

private void dispatchLayoutStep2() {  
    startInterceptRequestLayout(); //方法执行期间不能重入
    ...
    //设置好初始状态
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    mState.mInPreLayout = false;

    mLayout.onLayoutChildren(mRecycler, mState); //调用布局管理器去布局

    mState.mStructureChanged = false;
    mPendingSavedState = null;
    ...
    mState.mLayoutStep = State.STEP_ANIMATIONS; //接下来执行布局的第三步

    stopInterceptRequestLayout(false);
}

这里有一个mState,它是一个RecyclerView.State对象。顾名思义它是用来保存RecyclerView状态的一个对象,主要是用在LayoutManager、Adapter等组件之间共享RecyclerView状态的。可以看到这个方法将布局的工作交给了mLayout。这里它的实例是LinearLayoutManager,因此接下来看一下LinearLayoutManager.onLayoutChildren():

LinearLayoutManager.onLayoutChildren()

这个方法也挺长的,就不展示具体源码了。不过布局逻辑还是很简单的:

  1. 确定锚点(Anchor)View, 设置好AnchorInfo
  2. 根据锚点View确定有多少布局空间mLayoutState.mAvailable可用
  3. 根据当前设置的LinearLayoutManager的方向开始摆放子View

接下来就从源码来看这三步。

确定锚点View

锚点View大部分是通过updateAnchorFromChildren方法确定的,这个方法主要是获取一个View,把它的信息设置到AnchorInfo中 :

mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout   // 即和你是否在 manifest中设置了布局 rtl 有关

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
    ...
    View referenceChild = anchorInfo.mLayoutFromEnd
            ? findReferenceChildClosestToEnd(recycler, state) //如果是从end(尾部)位置开始布局,那就找最接近end的那个位置的View作为锚点View
            : findReferenceChildClosestToStart(recycler, state); //如果是从start(头部)位置开始布局,那就找最接近start的那个位置的View作为锚点View

    if (referenceChild != null) {
        anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); 
        ...
        return true;
    }
    return false;
}

即, 如果是start to end, 那么就找最接近start(RecyclerView头部)的View作为布局的锚点View。如果是end to start (rtl), 就找最接近end的View作为布局的锚点。

AnchorInfo最重要的两个属性时mCoordinatemPosition,找到锚点View后就会通过anchorInfo.assignFromView()方法来设置这两个属性:

public void assignFromView(View child, int position) {
    if (mLayoutFromEnd) {
        mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
    } else {
        mCoordinate = mOrientationHelper.getDecoratedStart(child);  
    }
    mPosition = position;
}
  • mCoordinate其实就是锚点ViewY(X)坐标去掉RecyclerView的padding。
  • mPosition其实就是锚点View的位置。

确定有多少布局空间可用并摆放子View

当确定好AnchorInfo后,需要根据AnchorInfo来确定RecyclerView当前可用于布局的空间,然后来摆放子View。以布局方向为start to end (正常方向)为例, 这里的锚点View其实是RecyclerView最顶部的View:

    // fill towards end  (1)
    updateLayoutStateToFillEnd(mAnchorInfo); //确定AnchorView到RecyclerView的底部的布局可用空间
    ...
    fill(recycler, mLayoutState, state, false); //填充view, 从 AnchorView 到RecyclerView的底部
    endOffset = mLayoutState.mOffset; 

    // fill towards start (2)
    updateLayoutStateToFillStart(mAnchorInfo); //确定AnchorView到RecyclerView的顶部的布局可用空间
    ...
    fill(recycler, mLayoutState, state, false); //填充view,从 AnchorView 到RecyclerView的顶部

上面我标注了(1)和(2), 1次布局是由这两部分组成的, 具体如下图所示 :

RecyclerView的布局步骤.png

然后我们来看一下fill towards end的实现:

fill towards end

确定可用布局空间

fill之前,需要先确定从锚点ViewRecyclerView底部有多少可用空间。是通过updateLayoutStateToFillEnd方法:

updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);

void updateLayoutStateToFillEnd(int itemPosition, int offset) {
    mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
    ...
    mLayoutState.mCurrentPosition = itemPosition;
    mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
    mLayoutState.mOffset = offset;
    mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}

mLayoutStateLinearLayoutManager用来保存布局状态的一个对象。mLayoutState.mAvailable就是用来表示有多少空间可用来布局mOrientationHelper.getEndAfterPadding() - offset其实大致可以理解为RecyclerView的高度。所以这里可用布局空间mLayoutState.mAvailable就是RecyclerView的高度

摆放子view

接下来继续看LinearLayoutManager.fill()方法,这个方法是布局的核心方法,是用来向RecyclerView中添加子View的方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;  //前面分析,其实就是RecyclerView的高度
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;  //extra 是你设置的额外布局的范围, 这个一般不推荐设置
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //保存布局一个child view后的结果
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { //有剩余空间的话,就一直添加 childView
        layoutChunkResult.resetInternal();
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);   //布局子View的核心方法
        ...
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; // 一次 layoutChunk 消耗了多少空间
        ...
        子View的回收工作
    }
    ...
}

这里我们不看子View回收逻辑,会在单独的一篇文章中讲。 即这个方法的核心是调用layoutChunk()来不断消耗layoutState.mAvailable,直到消耗完毕。继续看一下layoutChunk()方法, 这个方法的主要逻辑是:

  1. Recycler中获取一个View
  2. 添加到RecyclerView
  3. 调整View的布局参数,调用其measure、layout方法。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);  //这个方法会向 recycler view 要一个holder 
        ...
        if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { //根据布局方向,添加到不同的位置
            addView(view);   
        } else {
            addView(view, 0);
        }
        measureChildWithMargins(view, 0, 0);    //调用view的measure
        
        ...measure后确定布局参数 left/top/right/bottom

        layoutDecoratedWithMargins(view, left, top, right, bottom); //调用view的layout
        ...
    }

到这里其实就完成了上面的fill towards end:

    updateLayoutStateToFillEnd(mAnchorInfo); //确定布局可用空间
    ...
    fill(recycler, mLayoutState, state, false); //填充view

fill towards start就是从锚点ViewRecyclerView顶部来摆放子View,具体逻辑类似fill towards end,就不细看了。

RecyclerView滑动时的刷新逻辑

接下来我们再来分析一下在不加载新的数据情况下,RecyclerView在滑动时是如何展示子View的,即下面这种状态 :

RecyclerView滑动时的状态.png

下面就来分析一下3、4号和12、13号是如何展示的。

RecyclerViewOnTouchEvent对滑动事件做了监听,然后派发到scrollStep()方法:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    startInterceptRequestLayout(); //处理滑动时不能重入
    ...
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    ...
    stopInterceptRequestLayout(false);

    if (consumed != null) { //记录消耗
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

即把滑动的处理交给了mLayout, 这里继续看LinearLayoutManager.scrollVerticallyBy, 它直接调用了scrollBy(), 这个方法就是LinearLayoutManager处理滚动的核心方法。

LinearLayoutManager.scrollBy

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state); //确定可用布局空间
    final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); //摆放子View
    ....
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled); // 滚动 RecyclerView
    ...
}

这个方法的主要执行逻辑是:

  1. 根据布局方向和滑动的距离来确定可用布局空间mLayoutState.mAvailable
  2. 调用fill()来摆放子View
  3. 滚动RecyclerView

fill()的逻辑这里我们就不再看了,因此我们主要看一下1 和 3

根据布局方向和滑动的距离来确定可用布局空间

以向下滚动为为例,看一下updateLayoutState方法:

// requiredSpace是滑动的距离;  canUseExistingSpace是true
void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {

    if (layoutDirection == LayoutState.LAYOUT_END) { //滚动方法为向下
        final View child = getChildClosestToEnd(); //获得RecyclerView底部的View
        ...
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; //view的位置
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); //view的偏移 offset
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
    } else {
       ...
    }
    
    mLayoutState.mAvailable = requiredSpace;  
    if (canUseExistingSpace)  mLayoutState.mAvailable -= scrollingOffset;
    mLayoutState.mScrollingOffset = scrollingOffset;
}

所以可用的布局空间就是滑动的距离。那mLayoutState.mScrollingOffset是什么呢?

上面方法它的值是mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();,其实就是(childView的bottom + childView的margin) - RecyclerView的Padding。 什么意思呢? 看下图:

RecyclerView滚动时可使用的布局空间.png

RecyclerView的padding我没标注,不过相信上图可以让你理解: 滑动布局可用空间mLayoutState.mAvailable。同时mLayoutState.mScrollingOffset就是滚动的距离 - mLayoutState.mAvailable

所以 consumed也可以理解:

int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);   

fill()就不看了。子View摆放完毕后就要滚动布局展示刚刚摆放好的子View。这是依靠的mOrientationHelper.offsetChildren(-scrolled), 继续看一下是如何执行RecyclerView的滚动的

滚动RecyclerView

对于RecyclerView的滚动,最终调用到了RecyclerView.offsetChildrenVertical():

//dy这里就是滚动的距离
public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}

可以看到逻辑很简单,就是改变当前子View布局的top和bottom来达到滚动的效果。

本文就分析到这里。接下来会继续分析RecyclerView的复用逻辑。

欢迎关注我的Android进阶计划。看更多干货

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

推荐阅读更多精彩内容