RecyclerView(3)-LayoutMagager源码解析,LinearLayoutManager

上一节RecyclerView(2)- 自定义Decoration打造时光轴效果也已经写完了,希望有看到我文章的同学能有一些收获。layoutManager可以说是一个重中之重,代码量非常多,且涉及到复用机制的调用等等。等源码分析过后,同学们应该可以通过自定义LayoutManager打造奇形怪状的奇葩的UI需求了。

· RecyclerView(1)- Decoration源码解析
· RecyclerView(2)- 自定义Decoration打造时光轴效果
· RecyclerView(3)- LayoutMagager源码解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler复用机制_1
· RecyclerView(4)- 核心、Recycler复用机制_2
· RecyclerView(5)- 自定义LayoutManager(布局、复用)
· RecyclerView(6)- 自定义ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章视频地址:链接:http://pan.baidu.com/s/1hssvXC4 密码:18v1

分析LinearLayoutManager,先罗列几个想弄明白的问题

· 1、如何摆位置;
· 2、按需加载布局,位置摆放的规则
· 3、滑动时itemview的位置改变遵循的规则

LayoutManager如何摆位置;

先从 测量 onMeasure开始

RecyclerView.class
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout.mAutoMeasure) { // 自动测量
             mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);// 调用一次recyclerview的测量
             //....
            dispatchLayoutStep2();
        }
        else{//自定义测量规则
        
        }
    }

    private void dispatchLayoutStep2() {
        //....
         mState.mItemCount = mAdapter.getItemCount();
        //.... 自定义摆放位置
        mLayout.onLayoutChildren(mRecycler, mState);
        //....
    }

可以看到在recyclerview的 onMeasure 调用了 mLayout的onLayoutChildren方法 并将Recycler与 包含了 适配器一些信息的包装成一个 State参数 传入mLayout的onLayoutChildren

接下来看一下LinearLayoutManager

LinearLayoutManager.class
    //初始化
    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        setOrientation(orientation);
        setReverseLayout(reverseLayout);
        setAutoMeasureEnabled(true);
    }
    public void setAutoMeasureEnabled(boolean enabled) {
        mAutoMeasure = enabled;
    }
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
            if (state.getItemCount() == 0) {
            // 如果没有项目 移除布局
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        
        // ...
        this.updateLayoutStateToFillStart(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        //...
        this.updateLayoutStateToFillEnd(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForEnd;
        this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
        this.fill(recycler, this.mLayoutState, state, false);
    }
    // 填充view
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
            //.. 摆位置
             layoutChunk(recycler, state, layoutState, layoutChunkResult);
            //...  布局回收
              this.recycleByLayoutState(recycler, layoutState);
    }
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        //...
        addView(view);  // -> addView(View child, int index) ->  addViewInt(View child, int index, boolean disappearing)
        //计算位置
         this.measureChildWithMargins(view, 0, 0);
            result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
            int left;
            int top;
            int right;
            int bottom;
            if(this.mOrientation == 1) {
                if(this.isLayoutRTL()) {
                    right = this.getWidth() - this.getPaddingRight();
                    left = right - this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                } else {
                    left = this.getPaddingLeft();
                    right = left + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                }

                if(layoutState.mLayoutDirection == -1) {
                    bottom = layoutState.mOffset;
                    top = layoutState.mOffset - result.mConsumed;
                } else {
                    top = layoutState.mOffset;
                    bottom = layoutState.mOffset + result.mConsumed;
                }
            } else {
                top = this.getPaddingTop();
                bottom = top + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                if(layoutState.mLayoutDirection == -1) {
                    right = layoutState.mOffset;
                    left = layoutState.mOffset - result.mConsumed;
                } else {
                    left = layoutState.mOffset;
                    right = layoutState.mOffset + result.mConsumed;
                }
            }
        
        //布局
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        //...
    }
    private void addViewInt(View child, int index, boolean disappearing) {
          final ViewHolder holder = getChildViewHolderInt(child);
          mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);//依附在parent  这边是 parent 是 recyclerView
          
          // 对holder 做一些变量改变
    }

RecyclerView.class   
    static class LayoutState {
        View next(RecyclerView.Recycler recycler) {
            //..
            //从 recyler 中取出view
            final View view = recycler.getViewForPosition(mCurrentPosition);
            //...
            return view;
        }
    }

一开始的注释说了整个大概的流程:
1、先检查children和其它变量,找到一个锚点坐标与锚点(如果实在线性布局中,相当于找到当前界面内第一个VIew,与第一个view的坐标点)
2、填充从底部开始堆叠
3、从顶部填充到端部
4、计算是否还有滚动,添加各种变量,创建布局。

其实原理还是挺简单的,循环判断是否超出边界,测量view,添加view 布局view,判定值改变 在跳到上面循环。好多人看不懂是因为google工程师将这些判断数据抽取封装了起来,而其中字段非常多,让人眼花缭乱。
一些总结:
开始调用 onLayoutChildren() 调用fill() 摆放位置
fill():判断相应规则 回收一部分view, 调用layoutChunk() 获取 view、填充、摆放view;
layoutChunk(): 获取view,计算 view的位置
layoutDecoratedWithMargins():拿到装饰器设置的偏移量,摆放view;

2、按需加载布局,位置摆放的规则

上面也可以知道 在fill()方法内我们先去判断位置摆放的规则 在决定是否加载下一个/上一个View(也就是调用 layoutChunk),那么我们就来看一下判断规则是怎么样的吧

   while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
           int start = layoutState.mAvailable;
        if(layoutState.mScrollingOffset != -2147483648) {
            if(layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }

            this.recycleByLayoutState(recycler, layoutState);
        }

        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LinearLayoutManager.LayoutChunkResult layoutChunkResult = this.mLayoutChunkResult;
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }

可以看到while关键字内,有lauoutState.mInfinite remainingspace layoutState.haseMore(state)变量,就是这三个变量控制着我们的摆放view的数量。
字面意思可以猜出来:无穷、可用空间>0; 是否有更多

那我们来一个一个看一下,这几个变量都经历了什么。

2.1、layoutState.mInfinite

字面意思 mInfinite是无穷的意思...

    this.mLayoutState.mInfinite = this.resolveIsInfinite();
    boolean resolveIsInfinite() {
    return this.mOrientationHelper.getMode() == 0 && this.mOrientationHelper.getEnd() == 0;
}
new OrientationHelper(layoutManager) {
         public int getEnd() {
                return this.mLayoutManager.getHeight();
            }
         public int getMode() {
                return this.mLayoutManager.getHeightMode();
          }    
        }

可以看到 mInfinite 的值与 recyclerview的高度 与规格有关,判断==0 ? 这是什么鬼! 看起来一点卵用都没有 ಥ_ಥ(是在下输了)

2.1、remainingspace

  int remainingSpace = layoutState.mAvailable + layoutState.mExtra;

第一次 layoutState.mAvailable的取值

    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
        }
        
    private boolean updateAnchorFromPendingData(State state, LinearLayoutManager.AnchorInfo anchorInfo) {
    //.....
               if(anchorInfo.mLayoutFromEnd) {
            anchorInfo.mCoordinate = this.mOrientationHelper.getEndAfterPadding() - this.mPendingSavedState.mAnchorOffset;
        } else {
            anchorInfo.mCoordinate = this.mOrientationHelper.getStartAfterPadding() + this.mPendingSavedState.mAnchorOffset;
        }
        //.........
    }
    
    private void updateLayoutStateToFillStart(LinearLayoutManager.AnchorInfo anchorInfo) {
        this.updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
    }

    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
        this.mLayoutState.mAvailable = offset - this.mOrientationHelper.getStartAfterPadding();
    }
    

可以看到 第一次 layoutState.mAvailable的值是通过 锚点 AnchorInfo来计算的。
其中还判断了mLayoutFromEnd ...

看到这里我是晕的,变量太多了。。。

第一次 layoutState.mExtra的值
extra意思是额外...

2.3、layoutstate.hasMore(state)

        boolean hasMore(State state) {
            return this.mCurrentPosition >= 0 && this.mCurrentPosition < state.getItemCount();
        }

就是判断 position;

2.4、摆完一个布局以后,干了些什么

布局是一个一个添加的,那么在加载完一个children view后在加载了什么呢?

    int fill(Recycler recycler, LinearLayoutManager.LayoutState layoutState, State state, boolean stopOnFocusable) {
    
        while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

    // 加上偏移量
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                //  减去剩余空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
            //....
        }
    }
void layoutChunk(Recycler recycler, State state, LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result){
  //......
    //得到 消耗的高度/宽度
    result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
    // 布局
    addView(view); 
    layout
    // 判断是否被隐藏忽略
     if(params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
     }
     // 焦点
     result.mFocusable = view.isFocusable();
}

mOrientationHelper.calss
        public int getDecoratedMeasurement(View view) {
            LayoutParams params = (LayoutParams)view.getLayoutParams();
            return this.mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
        }

可以看到 layoutChunk()中赋值layoutChunkResult,在之后偏移量添加layoutState.mOffset+=...,剩余空间减去了view消耗的数量 remainingSpace-= layoutChunkResult.mConsumed。

还是回到了刚开始的注释: 先找到锚点,计算第一个坐标, 在循环添加view、计算偏移量确定摆放位置。

2.5 布局的一些总结

来一点总结吧:

涉及到的主要类
1、 LinearLayoutManager.LayoutState:布局状态类(有布局方向(向上向下)、结束布局、偏移量...)
2、Recycler 布局支持类、可回收view、创建view...
3、AnchorInfo 锚点信息类

布局流程:
1、以开始方向先更新锚点信息
2、在通过锚点信息 赋值layoutState 得到可用空间,偏移量等信息, while(剩余空间) 摆放childrenView 改变偏移量;
3、以底部更新锚点信息
4、重复第2步
5、结束

原理其实是很简单的,只不过其中变量太多了,代码也是很多所以看得云里雾里的,有时候还会跑偏找不到南北。
画一个流程图吧。

onChildren填充机制

其中填充规则 fill()方法流程如下

fill填充规则

3、 滑动时干了哪些事情

第一次布局看起来比较简单,我们就可以去猜测 滑动时应该加载布局也是一样的。 去看看源码吧。

    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
        return this.mOrientation == 1?0:this.scrollBy(dx, recycler, state);
    }

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
        return this.mOrientation == 0?0:this.scrollBy(dy, recycler, state);
    }

有两个, 分析scrollVerticallyBy

    int scrollBy(int dy, Recycler recycler, State state) {
        if(this.getChildCount() != 0 && dy != 0) {
            this.mLayoutState.mRecycle = true;
            this.ensureLayoutState();
            int layoutDirection = dy > 0?1:-1;
            int absDy = Math.abs(dy);
            this.updateLayoutState(layoutDirection, absDy, true, state);
            // 滑动且 返回消耗的距离
            int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
            if(consumed < 0) {
                return 0;
            } else {
            // 计算滑动的量 
                int scrolled = absDy > consumed?layoutDirection * consumed:dy;
                this.mOrientationHelper.offsetChildren(-scrolled);
                this.mLayoutState.mLastScrollDelta = scrolled;
                return scrolled;
            }
        } else {
            return 0;
        }
    }
    
      private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }
        // 可用距离  偏移距离 
        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

scrollVerticallyBy 的作用是 ,改变layoutState,调用fill,布局view,返回实际消耗的距离。

fill的流程我们在上面也已经做了说明了。
补充一点的是,当我们滑动的距离在一定范围内(没有超出页面上 第一个或最后一个child在屏幕上的范围)是不会重新

4、 锚点的位置是如何确定的

从以上流程中我们了解到了layoutManager的一些布局的规则,我们的布局都是通过一个基准点来进行上下布局,那么最重要的肯定就是这个基准点即锚点的位置。
我们知道锚点的信息 保存在 LinearLayoutManager.AnchorInfo中,进而layouState控制布局

1、第一次锚点得位置

void onLayoutChildren(Recycler recycler, State state) {
}
    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
            if(!this.updateAnchorFromChildren(recycler, state, anchorInfo)) {
            //分配坐标   
                anchorInfo.assignCoordinateFromPadding();
            //得到数据集位置
                anchorInfo.mPosition = this.mStackFromEnd?state.getItemCount() - 1:0;
            }
        }
    }
        class AnchorInfo {
            void assignCoordinateFromPadding() {
            this.mCoordinate = this.mLayoutFromEnd?LinearLayoutManager.this.mOrientationHelper.getEndAfterPadding():LinearLayoutManager.this.mOrientationHelper.getStartAfterPadding();
            }
        }

第一次特别简单,就判断布局方向获取mCoordinate 开始或结束的padding与itemView的数据集位置position。

2、 滑动位置的确定

    int scrollBy(int dy, Recycler recycler, State state) {
            this.updateLayoutState(layoutDirection, absDy, true, state);
    }
    private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection; 
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }

        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

给张图吧

文章视频地址:链接:http://pan.baidu.com/s/1o7Ai48E 密码:98pc

recyclerView滚动时布局流程

· RecyclerView(1)- Decoration源码解析
· RecyclerView(2)- 自定义Decoration打造时光轴效果
· RecyclerView(3)- LayoutMagager源码解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler复用机制_1
· RecyclerView(4)- 核心、Recycler复用机制_2
· RecyclerView(5)- 自定义LayoutManager(布局、复用)
· RecyclerView(6)- 自定义ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章视频地址:链接:http://pan.baidu.com/s/1hssvXC4 密码:18v1


希望我的文章不会误导在观看的你,如果有异议的地方欢迎讨论和指正。
如果能给观看的你带来收获,那就是最好不过了。

人生得意须尽欢, 桃花坞里桃花庵
点个关注呗,对,不信你点试试?

推荐阅读更多精彩内容