RecyclerView系列(7)—自定义LayoutManager(上),视觉上定义一个具备上下边界的RecyclerView.layoutMnager

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/user11223344abc/article/details/78080671

转载请注明出处:
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

条目回收和复用准备放到下一篇博客内进行讲解。
传送门:https://blog.csdn.net/user11223344abc/article/details/79168157

猜你喜欢

转载自blog.csdn.net/user11223344abc/article/details/78080671