26
2017
09

RecyclerView系列(7)—自定义LayoutManager

转载请注明出处:
http://blog.csdn.net/user11223344abc/article/details/78080671
出自【蛟-blog】

0.前言

本文分为俩个Step来研究如何自定义一个合格的LinearlayoutMnager
- Step 1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager
这里边又分为几个小步,后面会细说。
- Step 2:item回收,以及性能的验证
当然我们不能满足于视觉上,条目的离屏回收是一个合格Rv的基本标准。

内容涉及到部分原理,更多是代码层面的讲解,就是说,代码为什么这样写

Ps:第1主要是描述的一些基础,在1.3内有关于回收机制的叙述,若有基础的同学不想看预备知识点,而只想看实现细节,则可以直接跳到第2,3步,看实现细节的分析。

1.准备知识

1.0.自定义的第一步:extends RecyclerView.LayoutManager

看看系统给我们提供的3个LayoutManager:

LinearLayoutManager

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
        .......
}

StaggeredGridLayoutManager

public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider {
        .......
}

GridLayoutManager,这个是LinearLayoutManager子类,本质上还是extends RecyclerView.LayoutManager。

public class GridLayoutManager extends LinearLayoutManager {
        .......
}

所以,我们写出了如下代码:


public class CustomerLayoutManger extends RecyclerView.LayoutManager{


    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }

    /** * * @param recycler * @param state */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
    }
}

1.1.关于generateDefaultLayoutParams()

这个方法就是RecyclerView Item的布局参数,换种说法,就是RecyclerView 子 item 的 LayoutParameters,若是想修改子Item的布局参数(比如:宽/高/margin/padding等等),那么可以在该方法内进行设置。
一般来说,没什么特殊需求的话,则可以直接让子item自己决定自己的宽高即可(wrap_content)。

public abstract LayoutParams generateDefaultLayoutParams();

1.2.关于onLayoutChildren()

你可以看到这里多了个方法onLayoutChildren,这个方法就类似于自定义ViewGroup的onLayout方法,这也是自定义LayoutOutManager的主要入口(重要)。后面会详细的描述如何定义该方法。

        public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
        }

1.3.关于回收和缓存(重要):

上面说了实际上自定义layoutManager的过程也就是自定义onLayoutChildren()的过程,其中分为多个步骤,其中一个重要的步骤就是处理回收这个步骤。需要一定的理论知识,即在一定程度上的去理解recyclerView的缓存机制。

1.3.1.相关概念:

先来一张图:(摘自RV缓存机制详解-腾讯Bugly的专栏

这张图讲的是Rv和Lv的缓存机制对比,作者视图结合用Lv的2级缓存来让我去理解Rv的4级缓存机制。
关于这里出现的几个RecyclerView相关概念:

  • scrap
    里面缓存的View是接下来需要用到的,即里面的绑定的数据无需更改,可以直接拿来用的,是一个轻量级的缓存集合;
  • Recycle
    Recycle的缓存的View为里面的数据需要重新绑定,即需要通过Adapter重新绑定数据(onBindViewHolder/onCreateViewHolder)。

1.3.2.取缓存的流程:

当我们去获取一个新的View时,RecyclerView首先去检查Scrap缓存是否有对应的position的View,如果有,则直接拿出来可以直接用,不用去重新绑定数据;如果没有,则从Recycle缓存中取,并且会回调Adapter的onBindViewHolder方法(如果Recycle缓存为空,还会调用onCreateViewHolder方法),最后再将绑定好新数据的View返回。

1.3.3.缓存的手段:

  • scrap >> detach
    detachAndScrapView()
    场景:当我们对View进行重新排序的时候,可以选择Detach,因为屏幕上显示的就是这些position对应的View,我们并不需要重新去绑定数据,这明显可以提高效率。
  • Recycle >> remove
    removeAndRecycleView()
    场景:是当View不在屏幕中有任何显示的时候,你需要将它Remove掉,以备后面循环利用。

1.4 滑动处理:

滑动主要涉及4个方法:
- canScrollVertically //是否能垂直滑动
- scrollVerticallyBy //处理垂直滑动
- canScrollHorizontally //是否能水平华东
- scrollHorizontallyBy //处理水平滑动

由本例是模拟一个LinearlayoutManager,所以我们就关心vertical俩个方法就好了。看上面的注释,2个can方法都好理解,就是返回一个boolean值来告诉手机当前列表可否横竖滑动,true代表可以滑动,false反之,另外,相对于滑动而言,咱们主要来分析2个scrollBy方法,其中的难点也在这里,后面写代码的时候再细说。

2:实践

终于到了本文的主菜,开局说了分为俩大步,那么到了细节,我们再来拆分这俩大步所包含的细节。
- Step 1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager
将各个item.addView 【addView】
测量每个item 【measure】
放置各个item 【layout】
处理滚动 【scoll】
- Step 2:item回收,以及性能的验证
条目的回收 【recycler/scrap】

2.1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager(模拟一个LinearLayout LayoutManager):

4步:
- 将各个item.addView 【addView】
- 测量每个item 【measure】
- 放置各个item 【layout】
- 处理滚动 【scoll】

2.1.1:の 将各个item.addView【addView】:

addView(itemView);

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
   ......添加view
      for (int i = 0; i < getItemCount(); i++) {
            View scrap = recycler.getViewForPosition(i);
            addView(itemView);
        }
   ......
}

2.1.2:の 测量每个item【measure】:

核心api:

layoutDecorated(itemView, 0, 0);

  ......放置
        View scrap = recycler.getViewForPosition(i);
            int width = getDecoratedMeasuredWidth(scrap);
            int height = getDecoratedMeasuredHeight(scrap);
            layoutDecorated(scrap, offsetX, offsetY, offsetX + width, offsetY + height);
            offsetY += height;
   ......

到这一步理论上来说,屏幕上应该能看见一个vertical的列表了

在此汇总一下之前的代码:

    /** * @param recycler * @param state */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);

        int offsetY = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View scrap = recycler.getViewForPosition(i);
            addView(scrap);

            measureChildWithMargins(scrap, 0, 0);

            int perItemWidth = getDecoratedMeasuredWidth(scrap);
            int perItemHeight = getDecoratedMeasuredHeight(scrap);

            layoutDecorated(scrap, 0, offsetY, perItemWidth, offsetY + perItemHeight);
            offsetY += perItemHeight;
        }

        mTotalHeight = offsetY;
    }

but,现在还不能滚动。

2.1.3.の 处理滚动【scoll】:

预备知识内已经讲解了滑动相关的回调方法,这里主要讲api和实现。

首先,需要明确,滑动的核心API
也就是说,要滑动,这api是必调的(一个方向对应一个方法)。

      /** * Offset all child views attached to the parent RecyclerView by dy pixels along * the vertical axis. * * @param dy Pixels to offset by */
        public void offsetChildrenVertical(int dy) {
            if (mRecyclerView != null) {
                mRecyclerView.offsetChildrenVertical(dy);
            }
        }
       /** * Offset all child views attached to the parent RecyclerView by dx pixels along * the horizontal axis. * * @param dx Pixels to offset by */
        public void offsetChildrenHorizontal(int dx) {
            if (mRecyclerView != null) {
                mRecyclerView.offsetChildrenHorizontal(dx);
            }
        }

根据上面的分析,我们现在来加上这俩个方法的代码:

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        offsetChildrenVertical(dy);
        return super.scrollVerticallyBy(dy,recycler,state);
    }

执行效果如下:

呵呵,必须不正常,毕竟只写了一行代码,目前存在4个问题:

总结下:

问题1:方向是反的。
问题2:头,底,边界设置。
问题3:滑动惯性。
问题4:关于dy的修正。

那么接下来解决这4个问题。

2.1.3.1.问题1:方向是反的:

scrollVerticallyBy()方法:

这个方法的回调参数内有个dy。他代表手指在屏幕上每次滑动的位移。

从日志观察:
- 只有在列表可滚动的时候,该值才具有意义。(canScrollVertically 返回true)。
- 手指由下往上滑时,dy值为 >0 的。
- 手指由上往下滑时,dy值为 <0 的。
- 当手指滑动的幅度,速率越大,dy的绝对值越大。
- offsetChildrenVertical(dy),这个方法传入的dy需要乘以-1,才能让列表滑动符合我们的的生活习惯,否则列表是反向滑动的。

我的理解方式是:看源码注释

@param dy            
distance to scroll in pixels. Y increases as scroll position approaches the bottom.

滑动的距离(像素为单位),Y随着滚动位置靠近底部而增加。

也就是说,我可以理解为,手指滑动方向往下,Dy会变大(正),手指方向往上,Dy会变小(负)。

给张示意图:

2.1.3.2.问题2:头,底,边界设置,以及对滑动位置的修正:

2个问题:
- 头:如何判断列表处于顶部?
- 底:如何判断列表处于底部?

换种方式来思考:
一个点,做垂直移动,每次移动的起点是上一次的终点,并且会给出每一次移动的距离值,当一次移动的终点在起点之上时,这个距离值的符号为正,当终点在本次移动的起点之下时,移动距离的符号为负,问,如何判断该点抵达了上边界或下边界。

我先给出代码,然后再分析这个代码为何这样写:

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    //手指 从上往下move是 下拉 dy是负
    //手指 从下往上move是 上拉 dy是正
    int mTheMoveDistance = 0;

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {

        int theRvVisibleHeight = getVerticalVisibleHeight();
        int theMoreHeight = mTotalHeight - theRvVisibleHeight;

        Log.e("zj", "mRealMoveDistance == " + mTheMoveDistance);
        if (mTheMoveDistance + dy < 0) { //抵达上边界
           ...
        } else if (mTotalHeight > theRvVisibleHeight && mTheMoveDistance + dy > theMoreHeight) {//抵达下边界
           ...
        } else {

        }

        ....
        mTheMoveDistance += dy;

        return dy;
    }

    public int getVerticalVisibleHeight() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

上边界问题:
if (mTheMoveDistance + dy < 0) { //抵达上边界

这个没什么可讲的,可以试着想象一下mTheMoveDistance初始化为0,而dy每次移动都是一个从0开始偏移的变量,这么计算则是将每次偏移的距离进行一个记录,这样有助于后续用这个距离来进行边界的判断。

mTheMoveDistance为滑动的距离,这里之所以要先+dy是因为它是一个预判的动作,就是说在滑动距离增加之前,我先判断它究竟是正常的+=计算增加,还是需要修正之后再进行赋值。若不进行预判,则有可能出现列表上拉到边界时出现列表闪烁的问题,但闪烁之后会回复正常,有兴趣的同学可以自己进行试验,这里我就不贴图上来了。

下边界问题:
else if (mTotalHeight > theRvVisibleHeight && mTheMoveDistance + dy > theMoreHeight) {//抵达下边界

mTheMoveDistance + dy 若大于隐藏的部分的高度,则视为抵达底部边界。

这里所牵涉到的变量请参考下面的图进行理解。

—>mTotalHeight

mTotalHeight是在layout的时候就进行了一个计算了,它是一个全局变量

—>theRvVisibleHeight

这个是获取Rv在屏幕内显示的可见高度

它的赋值方法是这个:

    int theRvVisibleHeight = getVerticalVisibleHeight();

    public int getVerticalVisibleHeight() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

理解theRvVisibleHeight请看这张图:

—>theMoreHeight

这个值就是Rv所隐藏的高度,就是这个列表总高度减去可见高度

理解theMoreHeight请看这张图:

总高度 - 可见高度 == 被隐藏的多余部分(也就是蓝色那部分)

2.1.3.3.问题3:滑动惯性问题:

惯性的计算(flings),是由该方法的返回值决定的,当返回值和dy不一致时则会失去惯性效果,并且边界会产生发亮的效果。也就是说,正确的对dy修正并让其返回是fling惯性正常的一个重要前提条件。

2.1.3.4.问题4:边界修正问题:

这是一个衍生的问题,就是说我们光判断了边界,但不对返回值dy进行修正的话,就会导致moveDistance计算失误,计算失误产生的直接后果就是判断条件错误,因为移动距离moveDistance是作为我们的一个判断条件而存在的。那么我们该如何修正边界呢?

关于边界修复的思路就是,在特定的边界,对moveDistance计算出特定的值,而又因为这个边界的赋值是动态的 moveDistance+=dy ,且因返回值为dy的因素(返回值的影响惯性的效果在上面已经说过了),所以,我们真正需要修正的,实际是dy。

上边界修正:

dy = -mTheMoveDistance;

上边界时,我们认定滑动距离为0。
则,moveDistance+=dy 需要等于 0
得出:dy = -mTheMoveDistance

下边界修正:

dy = theMoreHeight - mTheMoveDistance;

当滑动距离超过底部距离时,将滑动距离修正为底部距离。
因为:底部距离为:mTheMoveDistance = mTheMoveDistance + dy
且,需要修正成为的距离为:mTheMoveDistance = theMoreHeight;
得出表达式:mTheMoveDistance + dy = theMoreHeight;
转换后的结果则是:dy = theMoreHeight - mTheMoveDistance;

配上一张示意图:不明白的同学把图上的值带入情景计算一下便明白了。

上图,蓝色部分是屏幕隐藏的部分(也就是说当蓝色部分全部显示时,表明已经抵达列表底部边界),绿色部分是可见部分,橙黄色部分是表示多滑动的距离,也就是需要被修正的部分。

至此,视觉上看着已经没问题了,其实有效代码也就50行左右。
但我们的条目还没有进行缓存和回收。接下来进行缓存的回收及利用。

目前阶段的代码:
http://download.csdn.net/download/user11223344abc/9993385

接下来就要开始进行回收代码的处理了……

2.2:item回收,以及性能的验证:

在开始性能优化之前,我们先看看就目前为止的代码的条目数量,来验证性能。

55是我在adapter内设置了55条数据源,滚动的时候,childCount还是为55。然而实际上我一屏可见的条目也就20条左右,那么显然,这是不合格的。

PS:注意getitemCount和getchildCount的区别。getitemCount指的的adapter绑定的数据源,它对应的是整个列表(数量不受缓存行为影响,因为不在一个业务含义上),而childCount则是当前容器内实实在在用作布局的数据源,也就是说,它是跟屏幕内容相关的,理解这俩者对于后面的逻辑很重要(数量受缓存行为影响)。

2.2.1:关于回收的思路(重要):

  • 1.先算出当前的列表可见区域(Rect recyclerViewVisibleRect = … 注意这个列表可见的区域是随着滑动而变化的,联想用用放大镜从上往下移动去看一张地图,放大镜就是当前可见区域)
  • 2.然后将全列表内每一个item的位置记录下来(SparseArray)
  • 3.记录下来之后判断每一个item的位置是否包含在当前可见区域(public static boolean intersects(Rect a, Rect b))
  • 4.若当条目处于列表之外,则选择回收
  • 5.回收完成之后进行全表重新布局(且,于scrollby内能替代滑动API-offsetChildrenVertical)

2.2.2:代码编写:

知道了思路了之后,我们写出了如下代码:

step 1:先算出当前的列表可见区域(Rect recyclerViewVisibleRect = … 注意这个列表可见的区域是随着滑动而变化的,联想用用放大镜从上往下移动去看一张地图,放大镜就是当前可见区域)
step 2:然后将全列表内每一个item的位置记录下来(SparseArray)


    private SparseArray itemsRectMap = new SparseArray();

    /** * 初始化itemRects * * @param recycler onLayoutChildren方法传递进来的recycler */
    private void initItemRects(RecyclerView.Recycler recycler) {

        int offsetY = 0;

        //init the itemsRectMap
        for (int i = 0; i < getItemCount(); i++) {


            View scrapItem = recycler.getViewForPosition(i);

            addView(scrapItem);
            measureChildWithMargins(scrapItem, 0, 0);

            int measureItemHeight = getDecoratedMeasuredHeight(scrapItem);
            mDecoratedMeasuredWidth = getDecoratedMeasuredWidth(scrapItem);
            itemsRectMap.put(i, new Rect(0, offsetY, mDecoratedMeasuredWidth, offsetY + measureItemHeight));

            offsetY += measureItemHeight;

            //write value to mTotalHeight
            mTotalHeight = offsetY;
        }
    }

step 3.记录下来之后判断每一个item的位置是否包含在当前可见区域(public static boolean intersects(Rect a, Rect b))
step 4.若当条目处于列表之外,则选择回收
step 5.回收完成之后进行全表重新布局(且,于scrollby内能替代滑动API-offsetChildrenVertical)

直接贴出代码吧:

    /** * 在rectMap初始化了的基础之上,对item进行布局。 * layout visible items and remove inVisible items. */
    // **step 3.记录下来之后判断每一个item的位置是否包含在当前可见区域(public static boolean intersects(Rect a, Rect b))**
    // **step 4.若当条目处于列表之外,则选择回收**
    // **step 5.回收完成之后进行全表重新布局(且,于scrollby内能替代滑动API-offsetChildrenVertical)**
    private void fillItems(RecyclerView.Recycler recycler, RecyclerView.State state) {

        if (getItemCount() == 0) {
            return;
        }

        if (state.isPreLayout()) { // 跳过preLayout,preLayout主要用于支持动画
            return;
        }

        //scrap items...
        detachAndScrapAttachedViews(recycler);

        //先算出当前的列表可见区域(Rect recyclerViewVisibleRect = ... 注意这个列表可见的区域是随着滑动而变化的,联想用用放大镜上下移动的去看一张地图,放大镜就是当前可见区域)
        Rect recyclerViewVisibleRect = new Rect(0, mTheMoveDistance, getWidth(), mTheMoveDistance + getVerticalVisibleHeight());
        Rect childRect = new Rect();
        for (int visiblePos = 0; visiblePos < getChildCount(); visiblePos++) {
            View childView = getChildAt(visiblePos);
            childRect.left = getDecoratedLeft(childView);
            childRect.top = getDecoratedTop(childView);
            childRect.right = getDecoratedRight(childView);
            childRect.bottom = getDecoratedBottom(childView);

            //如果Item没有在显示区域,就说明需要回收
            if (!Rect.intersects(recyclerViewVisibleRect, childRect)) {
                //回收掉滑出屏幕的View
                removeAndRecycleView(childView, recycler);
            }
        }


        //全列表布局
        for (int i = 0; i < getItemCount(); i++) {
            View viewForPosition = recycler.getViewForPosition(i);
            Rect rect = (Rect) itemsRectMap.get(i);
            if (null != rect && Rect.intersects(recyclerViewVisibleRect, rect)) {
                measureChild(viewForPosition, 0, 0);
                addView(viewForPosition);
                layoutDecorated(viewForPosition, 0, rect.top - mTheMoveDistance, rect.right, rect.bottom - mTheMoveDistance);
            }
        }

    }

这里简单说下注意点:
- 全表布局的时候切记,addView之前先Measure,就因为这个小毛病给浪费一小时
- 回收用getChild是因为这是一个实时变化的行为,后进行全表布局的时候是集合之前初始化过的itemsRectMap来进做的
- layout的适合记得减去移动距离,否则布局错误

2.2.3:调用fillitems方法,并对之前的代码进行重构:

进行了如下重构:
- 抽取了一个计算全表item区域的方法
- 在onLayoutChildren和scrollVerticallyBy方法处调用fillitems方法
- 在fill逻辑开始处进行一个attachScrap回收
- 优化处理,check方法

于是代码就成了这德性:

public class MyLayoutManager extends RecyclerView.LayoutManager {

    private int mTotalHeight = -1;
    private SparseArray itemsRectMap = new SparseArray();
    private int mDecoratedMeasuredWidth;

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }

    /** * 在rectMap初始化了的基础之上,对item进行布局。 * layout visible items and remove inVisible items. */
    // **step 3.记录下来之后判断每一个item的位置是否包含在当前可见区域(public static boolean intersects(Rect a, Rect b))**
    // **step 4.若当条目处于列表之外,则选择回收**
    // **step 5.回收完成之后进行全表重新布局(且,于scrollby内能替代滑动API-offsetChildrenVertical)**
    private void fillItems(RecyclerView.Recycler recycler, RecyclerView.State state) {

        if (getItemCount() == 0) {
            return;
        }

        if (state.isPreLayout()) { // 跳过preLayout,preLayout主要用于支持动画
            return;
        }

        //scrap items...
        detachAndScrapAttachedViews(recycler);

        //先算出当前的列表可见区域(Rect recyclerViewVisibleRect = ... 注意这个列表可见的区域是随着滑动而变化的,联想用用放大镜上下移动的去看一张地图,放大镜就是当前可见区域)
        Rect recyclerViewVisibleRect = new Rect(0, mTheMoveDistance, getWidth(), mTheMoveDistance + getVerticalVisibleHeight());
        Rect childRect = new Rect();
        for (int visiblePos = 0; visiblePos < getChildCount(); visiblePos++) {
            View childView = getChildAt(visiblePos);
            childRect.left = getDecoratedLeft(childView);
            childRect.top = getDecoratedTop(childView);
            childRect.right = getDecoratedRight(childView);
            childRect.bottom = getDecoratedBottom(childView);

            //如果Item没有在显示区域,就说明需要回收
            if (!Rect.intersects(recyclerViewVisibleRect, childRect)) {
                //回收掉滑出屏幕的View
                removeAndRecycleView(childView, recycler);
            }
        }


        //全列表布局
        for (int i = 0; i < getItemCount(); i++) {
            View viewForPosition = recycler.getViewForPosition(i);
            Rect rect = (Rect) itemsRectMap.get(i);
            if (null != rect && Rect.intersects(recyclerViewVisibleRect, rect)) {
                measureChild(viewForPosition, 0, 0);
                addView(viewForPosition);
                layoutDecorated(viewForPosition, 0, rect.top - mTheMoveDistance, rect.right, rect.bottom - mTheMoveDistance);
            }
        }

    }


    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);


        if (check(state)) return;

        //scrap items...
        detachAndScrapAttachedViews(recycler);

        //初始化itemRects
        initItemRects(recycler);

        //layout visible items and remove inVisible items.
        fillItems(recycler, state);
    }

    /** * 优化项,减少循环次数 * * @param state * @return */
    private boolean check(RecyclerView.State state) {
        if (getItemCount() <= 0)
            return true;
        if (state.isPreLayout()) {
            return true;
        }
        return false;
    }


    /** * 初始化itemRects * * @param recycler onLayoutChildren方法传递进来的recycler */
    private void initItemRects(RecyclerView.Recycler recycler) {

        int offsetY = 0;

        //init the itemsRectMap
        for (int i = 0; i < getItemCount(); i++) {

            View scrapItem = recycler.getViewForPosition(i);
            addView(scrapItem);
            measureChildWithMargins(scrapItem, 0, 0);
            int measureItemHeight = getDecoratedMeasuredHeight(scrapItem);
            mDecoratedMeasuredWidth = getDecoratedMeasuredWidth(scrapItem);
            itemsRectMap.put(i, new Rect(0, offsetY, getWidth(), offsetY + measureItemHeight));
            offsetY += measureItemHeight;
            //write value to mTotalHeight
            mTotalHeight = offsetY;
        }
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    //手指 从上往下move是 下拉 dy是负
    //手指 从下往上move是 上拉 dy是正
    int mTheMoveDistance = 0;

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {

        //先detach掉所有的子View
        detachAndScrapAttachedViews(recycler);

        int theRvVisibleHeight = getVerticalVisibleHeight();
        int theMoreHeight = mTotalHeight - theRvVisibleHeight;

        if (mTheMoveDistance + dy < 0) { //抵达上边界
            Log.e("www", "抵达上边界");
            dy = -mTheMoveDistance;
        } else if (mTotalHeight > theRvVisibleHeight && mTheMoveDistance + dy > theMoreHeight) {//抵达下边界
            Log.e("www", "抵达下边界");
            dy = theMoreHeight - mTheMoveDistance;
        } else {

        }

        mTheMoveDistance += dy;

        fillItems(recycler, state);
        Log.e("zj", "child COUNT == " + getChildCount());

        return dy;
    }

    public int getVerticalVisibleHeight() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

}

2.2.4:结果验证:

3:结语

好了,终于完事,有错的地方留言,互相学习,欢迎交流。
若觉得有的地方讲的不好不能为你解惑,可以去看看本文最后列出的参考文献,基本网上讲的比较好的我都列出来的。

最后附上Demo:
http://download.csdn.net/download/user11223344abc/9993334

THANKS

huachao1001博客-打造属于你的LayoutManager
hehonghui/android-tech-frontier-GITHUB
张旭童博客-【Android】掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
张旭童博客-【Android】掌握自定义LayoutManager(二) 实现流式布局
vilyever博客-Android如何自定义RecyclerView的LayoutManager
腾讯Bugly博客-RV缓存机制详解-腾讯Bugly的专栏
CoorChice博客-你可能误会了!原来自定义LayoutManager可以这么简单

上一篇:Linux bash 之declare 下一篇:Android7.0 Settings 源码剖析一——Settings概括