Android 掌握自定义LayoutManager(二) 实现流式布局

来源:互联网 发布:nginx lua redis 限流 编辑:程序博客网 时间:2024/06/06 01:03

转载请标明出处: 
http://blog.csdn.net/zxt0601/article/details/52956504 
本文出自:【张旭童的博客】

本系列文章相关代码传送门: 
自定义LayoutManager实现的流式布局 
欢迎star,pr,issue。

本系列文章目录: 
掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
掌握自定义LayoutManager(二) 实现流式布局

一 概述

在开始之前,我想说,如果需求是每个Item宽高一样,实现起来复杂度比每个Item宽高不一样的,要小10+倍。 
然而我们今天要实现的流式布局,恰巧就是至少每个Item的宽度不一样,所以在计算坐标的时候算的我死去活来。先看一下效果图: 
这里写图片描述 
艾玛,换成妹子图后貌似好看了许多,我都不认识它了,好吧,项目里它一般长下面这样: 
这里写图片描述 
往常这种效果,我们一般使用自定义ViewGroup实现,我以前也写了一个。自定义VG实现流式布局 
这不最近再研究自定义LayoutManager么,想来想去也没有好的创意,就先拿它开第一刀吧。 
(后话:流式布局Item宽度不一,不知不觉给自己挖了个大坑,造成拓展一些功能难度倍增,观之网上的DEMO,99%Item的大小都是一样的,so,这个系列的下一篇我计划 实现一个Item大小一样 的酷炫LayoutManager。但是最终做成啥样的效果还没想好,有朋友看到酷炫的效果可以告诉我,我去高仿一个。)

自定义LayoutManager的步骤:

以本文的流式布局为例,需求是一个垂直滚动的布局,子View以流式排列。先总结一下步骤:

一 实现 generateDefaultLayoutParams() 
二 实现 onLayoutChildren() 
三 竖直滚动需要 重写canScrollVertically()和scrollVerticallyBy()

下面我们就一步一步来吧。

二 实现generateDefaultLayoutParams()

如果没有特殊需求,大部分情况下,我们只需要如下重写该方法即可。

    @Override    public RecyclerView.LayoutParams generateDefaultLayoutParams() {        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);    }
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

RecyclerView.LayoutParams是继承自Android.view.ViewGroup.MarginLayoutParams的,所以可以方便的使用各种margin。

这个方法最终会在recycler.getViewForPosition(i)时调用到,在该方法浩长源码的最下方:

            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();            final LayoutParams rvLayoutParams;            if (lp == null) {            //这里会调用mLayout.generateDefaultLayoutParams()为每个ItemView设置LayoutParams                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();                holder.itemView.setLayoutParams(rvLayoutParams);            } else if (!checkLayoutParams(lp)) {                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);                holder.itemView.setLayoutParams(rvLayoutParams);            } else {                rvLayoutParams = (LayoutParams) lp;            }            rvLayoutParams.mViewHolder = holder;            rvLayoutParams.mPendingInvalidate = fromScrap && bound;            return holder.itemView;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

重写完这个方法就能编译通过了,只不过然并卵,界面上是一片空白,下面我们就走进onLayoutChildren()方法 ,为界面添加Item。

注:99%用不到的情况:如果需要存储一些额外的东西在LayoutParams里,这里返回你自定义的LayoutParams即可。 
当然,你自定义的LayoutParams需要继承自RecyclerView.LayoutParams

三 onLayoutChildren()

该方法是LayoutManager的入口。它会在如下情况下被调用: 
1 在RecyclerView初始化时,会被调用两次。 
2 在调用adapter.notifyDataSetChanged()时,会被调用。 
3 在调用setAdapter替换Adapter时,会被调用。 
4 在RecyclerView执行动画时,它也会被调用。 
即RecyclerView 初始化 、 数据源改变时 都会被调用。 
(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)

在系列开篇我已经提到,它相当于ViewGroup的onLayout()方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:

    private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断    private int mFirstVisiPos;//屏幕可见的第一个View的Position    private int mLastVisiPos;//屏幕可见的最后一个View的Position        @Override    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {        if (getItemCount() == 0) {//没有Item,界面空着吧            detachAndScrapAttachedViews(recycler);            return;        }        if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的            return;        }        //onLayoutChildren方法在RecyclerView 初始化时 会执行两遍        detachAndScrapAttachedViews(recycler);        //初始化        mVerticalOffset = 0;        mFirstVisiPos = 0;        mLastVisiPos = getItemCount();        //初始化时调用 填充childView        fill(recycler, state);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这个fill(recycler, state);方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务: 
在考虑滑动位移的情况下: 
1 回收所有屏幕不可见的子View 
2 layout所有可见的子View

在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.

            int topOffset = getPaddingTop();//布局时的上偏移            int leftOffset = getPaddingLeft();//布局时的左偏移            int lineMaxHeight = 0;//每一行最大的高度            int minPos = mFirstVisiPos;//初始化时,我们不清楚究竟要layout多少个子View,所以就假设从0~itemcount-1            mLastVisiPos = getItemCount() - 1;            //顺序addChildView            for (int i = minPos; i <= mLastVisiPos; i++) {                //找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。                View child = recycler.getViewForPosition(i);                addView(child);                measureChildWithMargins(child, 0, 0);                //计算宽度 包括margin                if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下                    layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));                    //改变 left  lineHeight                    leftOffset += getDecoratedMeasurementHorizontal(child);                    lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));                } else {//当前行排列不下                    //改变top  left  lineHeight                    leftOffset = getPaddingLeft();                    topOffset += lineMaxHeight;                    lineMaxHeight = 0;                    //新起一行的时候要判断一下边界                    if (topOffset - dy > getHeight() - getPaddingBottom()) {                        //越界了 就回收                        removeAndRecycleView(child, recycler);                        mLastVisiPos = i - 1;                    } else {                        layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));                        //改变 left  lineHeight                        leftOffset += getDecoratedMeasurementHorizontal(child);                        lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));                    }                }            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

用到的一些工具函数(在系列开篇已介绍过):

    //模仿LLM Horizontal 源码    /**     * 获取某个childView在水平方向所占的空间     *     * @param view     * @return     */    public int getDecoratedMeasurementHorizontal(View view) {        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)                view.getLayoutParams();        return getDecoratedMeasuredWidth(view) + params.leftMargin                + params.rightMargin;    }    /**     * 获取某个childView在竖直方向所占的空间     *     * @param view     * @return     */    public int getDecoratedMeasurementVertical(View view) {        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)                view.getLayoutParams();        return getDecoratedMeasuredHeight(view) + params.topMargin                + params.bottomMargin;    }    public int getVerticalSpace() {        return getHeight() - getPaddingTop() - getPaddingBottom();    }    public int getHorizontalSpace() {        return getWidth() - getPaddingLeft() - getPaddingRight();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

如上编写一个超级简单的fill()方法,运行,你的程序应该就能看到流式布局的效果出现了。 
可是千万别开心,因为痛苦的计算远没到来。 
如果这些都看不懂,那么我建议: 
一,直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。 
二,去学习一下自定义ViewGroup的知识。

此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。

四,动起来

想让我们自定义的LayoutManager动起来,最简单的写法如下:

    @Override    public boolean canScrollVertically() {        return true;    }    @Override    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {        int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复        offsetChildrenVertical(-realOffset);        return realOffset;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

offsetChildrenVertical(-realOffset);这句话移动所有的childView. 
返回值会被RecyclerView用来判断是否达到边界, 如果返回值!=传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。

写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。

好了,下面开始正经的写它吧,

    @Override    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {        //位移0、没有子View 当然不移动        if (dy == 0 || getChildCount() == 0) {            return 0;        }        int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复        //边界修复代码        if (mVerticalOffset + realOffset < 0) {//上边界            realOffset = -mVerticalOffset;        } else if (realOffset > 0) {//下边界            //利用最后一个子View比较修正            View lastChild = getChildAt(getChildCount() - 1);            if (getPosition(lastChild) == getItemCount() - 1) {                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);                if (gap > 0) {                    realOffset = -gap;                } else if (gap == 0) {                    realOffset = 0;                } else {                    realOffset = Math.min(realOffset, -gap);                }            }        }        realOffset = fill(recycler, state, realOffset);//先填充,再位移。        mVerticalOffset += realOffset;//累加实际滑动距离        offsetChildrenVertical(-realOffset);//滑动        return realOffset;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

这里用realOffset变量保存实际的位移,也是return 回去的值。大部分情况下它=dy。 
在边界处,为了防止越界,做了一些处理,realOffset 可能不等于dy。 
别的文章不同的是,我参考了LinearLayoutManager的源码,先考虑滑动位移进行View的回收、填充(fill()函数),然后再真正的位移这些子Item。


fill()的过程中

流程:

一 会先考虑到dy回收界面上不可见的Item。 
二 填充布局子View 
三 判断是否将dy都消费掉了,如果消费不掉:例如滑动距离太多,屏幕上的View已经填充完了,仍有空白,那么就要修正dy给realOffset。

注意事项一:考虑滑动的方向

在填充布局子View的时候,还要考虑滑动的方向,即填充的顺序,是从头至尾填充,还是从尾至头部填充。 
如果是向底部滑动,那么是顺序填充,显示底端position更大的Item。( dy>0) 
如果是向顶部滑动,那么是逆序填充,显示顶端positon更小的Item。(dy<0)

注意事项二:流式布局 逆序布局子View的问题

再啰嗦最后一点,我们想象一下这个逆序填充的过程: 
正序过程可以自上而下,自左向右layout 子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度,如果超过了就另起一行。 
逆序时,有两种方案:

1 利用Rect保存子View边界

正序排列时,保存每个子View的Rect, 
逆序时,直接拿出来,layout

2 逆序化

自右向左layout子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度, 
如果超过了就另起一行。并且判断最后一个子View距离父控件左边的offset,平移这一行的所有子View,较复杂,采用方案1. 
(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。) 
下面上码:

private SparseArray<Rect> mItemRects;//key 是View的position,保存View的bounds ,/**     * 填充childView的核心方法,应该先填充,再移动。     * 在填充时,预先计算dy的在内,如果View越界,回收掉。     * 一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.     *     * @param recycler     * @param state     * @param dy       RecyclerView给我们的位移量,+,显示底端, -,显示头部     * @return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)     */    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {        int topOffset = getPaddingTop();        //回收越界子View        if (getChildCount() > 0) {//滑动时进来的            for (int i = getChildCount() - 1; i >= 0; i--) {                View child = getChildAt(i);                if (dy > 0) {//需要回收当前屏幕,上越界的View                    if (getDecoratedBottom(child) - dy < topOffset) {                        removeAndRecycleView(child, recycler);                        mFirstVisiPos++;                        continue;                    }                } else if (dy < 0) {//回收当前屏幕,下越界的View                    if (getDecoratedTop(child) - dy > getHeight() - getPaddingBottom()) {                        removeAndRecycleView(child, recycler);                        mLastVisiPos--;                        continue;                    }                }            }            //detachAndScrapAttachedViews(recycler);        }        int leftOffset = getPaddingLeft();        int lineMaxHeight = 0;        //布局子View阶段        if (dy >= 0) {            int minPos = mFirstVisiPos;            mLastVisiPos = getItemCount() - 1;            if (getChildCount() > 0) {                View lastView = getChildAt(getChildCount() - 1);                minPos = getPosition(lastView) + 1;//从最后一个View+1开始吧                topOffset = getDecoratedTop(lastView);                leftOffset = getDecoratedRight(lastView);                lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(lastView));            }            //顺序addChildView            for (int i = minPos; i <= mLastVisiPos; i++) {                //找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。                View child = recycler.getViewForPosition(i);                addView(child);                measureChildWithMargins(child, 0, 0);                //计算宽度 包括margin                if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下                    layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));                    //保存Rect供逆序layout用                    Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);                    mItemRects.put(i, rect);                    //改变 left  lineHeight                    leftOffset += getDecoratedMeasurementHorizontal(child);                    lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));                } else {//当前行排列不下                    //改变top  left  lineHeight                    leftOffset = getPaddingLeft();                    topOffset += lineMaxHeight;                    lineMaxHeight = 0;                    //新起一行的时候要判断一下边界                    if (topOffset - dy > getHeight() - getPaddingBottom()) {                        //越界了 就回收                        removeAndRecycleView(child, recycler);                        mLastVisiPos = i - 1;                    } else {                        layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));                        //保存Rect供逆序layout用                        Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);                        mItemRects.put(i, rect);                        //改变 left  lineHeight                        leftOffset += getDecoratedMeasurementHorizontal(child);                        lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));                    }                }            }            //添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy            View lastChild = getChildAt(getChildCount() - 1);            if (getPosition(lastChild) == getItemCount() - 1) {                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);                if (gap > 0) {                    dy -= gap;                }            }        } else {            /**             * ##  利用Rect保存子View边界             正序排列时,保存每个子View的Rect,逆序时,直接拿出来layout。             */            int maxPos = getItemCount() - 1;            mFirstVisiPos = 0;            if (getChildCount() > 0) {                View firstView = getChildAt(0);                maxPos = getPosition(firstView) - 1;            }            for (int i = maxPos; i >= mFirstVisiPos; i--) {                Rect rect = mItemRects.get(i);                if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {                    mFirstVisiPos = i + 1;                    break;                } else {                    View child = recycler.getViewForPosition(i);                    addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定                    measureChildWithMargins(child, 0, 0);                    layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);                }            }        }        Log.d("TAG", "count= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size() + ", dy:" + dy + ",  mVerticalOffset" + mVerticalOffset+", ");        return dy;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132

思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。 
值得一提的是,可以通过getChildCount()recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0. 
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。 
原因在系列开篇也提过,不再赘述。

至此我们的自定义LayoutManager已经可以用了,使用的效果就和文首的两张图一模一样。

下面再提及一些其他注意点和适配事项:

五 适配notifyDataSetChanged()

此时会回调onLayoutChildren()函数。因为我们流式布局的特殊性,每个Item的宽度不一致,所以化简处理,每次这里归零。

        //初始化区域        mVerticalOffset = 0;        mFirstVisiPos = 0;        mLastVisiPos = getItemCount();
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

如果每个Item的大小都一样,逆序顺序layoutChild都比较好处理,则应该在此判断,getChildCount(),大于0说明是DatasetChanged()操作,(初始化的第二次也会childCount>0)。根据当前记录的position和位移信息去fill视图即可。

六 适配 Adapter的替换。

我根据24.2.1源码,发现网上的资料对这里的处理其实是不必要的。

一 资料中的做法如下:

当对RecyclerView设置一个新的Adapter时,onAdapterChanged()方法会被回调,一般的做法是在这里remove掉所有的View。此时onLayoutChildren()方法会被再次调用,一个新的轮回开始。

    @Override    public void onAdapterChanged(final RecyclerView.Adapter oldAdapter, final RecyclerView.Adapter newAdapter) {        removeAllViews();    }
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

二 我的新观点:

通过查看源码+打断点跟踪分析,调用RecyclerView.setAdapter后,调用顺序依次为

1 Recycler.setAdapter():

    public void setAdapter(Adapter adapter) {        // bail out if layout is frozen        setLayoutFrozen(false);        setAdapterInternal(adapter, false, true); //张旭童注:注意第三个参数是true        requestLayout();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

那么我们查看setAdapterInternal()方法:

private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,            boolean removeAndRecycleViews) {        ...        //张旭童注:removeAndRecycleViews 参数此时为ture        if (!compatibleWithPrevious || removeAndRecycleViews) {            ...            if (mLayout != null) {             //张旭童注: 所以如果我们更换Adapter时,mLayout不为空,会先执行如下操作,                mLayout.removeAndRecycleAllViews(mRecycler);                mLayout.removeAndRecycleScrapInt(mRecycler);            }            // we should clear it here before adapters are swapped to ensure correct callbacks.            //张旭童注:而且还会清空Recycler的缓存            mRecycler.clear();        }        ...        if (mLayout != null) {        //张旭童注:这里才调用的LayoutManager的方法            mLayout.onAdapterChanged(oldAdapter, mAdapter);        }        //张旭童注:这里调用Recycler的方法        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);        ...    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

也就是说 更换Adapter一开始,还没有执行到LayoutManager.onAdapterChanged()界面上的View都已经被remove掉了,我们的操作属于多余的

2 LayoutManager.onAdapterChanged()

空实现:也没必要实现了

        public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {        }
  • 1
  • 2
  • 1
  • 2

3 Recycler.onAdapterChanged():

该方法先清空scapCache区域(貌似也是多余,一开始被清空过了),然后调用RecyclerViewPool.onAdapterChanged() 

        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,                boolean compatibleWithPrevious) {            clear();            getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);        }        public void clear() {            mAttachedScrap.clear();            recycleAndClearCachedViews();        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4 RecyclerViewPool.onAdapterChanged()

如果没有别的Adapter在用这个RecyclerViewPool,会清空RecyclerViewPool的缓存。

        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,                boolean compatibleWithPrevious) {            if (oldAdapter != null) {                detach();            }            if (!compatibleWithPrevious && mAttachCount == 0) {                clear();            }            if (newAdapter != null) {                attach(newAdapter);            }        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5 LayoutManager.onLayoutChildren()

新的布局开始。

七 总结:

引用一段话

They are also extremely complex, and hard to get right. For every amount of effort RecyclerView requires of you, it is doing 10x more behind the scenes.

本文Demo仍有很大完善空间,有些需要完善的细节非常复杂,需要经过多次试验才能得到正确的结果(这里我更加敬佩Google提供的三个LM)。每一个我们想要实现的需求,可能要花费比我们想象的时间*10倍的时间。 
上篇也提及到的,不要过度优化,达成需求就好。

可以通过getChildCount()recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0. 
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。

感兴趣的同学可以对网上的各个Demo打印他们onCreateViewHolder执行的次数,以及上述两个参数的值,和官方的LayoutManager比较,这三个参数先达标,才算是及格的LayoutManager,但后续优化之路仍很长。

本系列文章相关代码传送门: 
自定义LayoutManager实现的流式布局 
欢迎star,pr,issue。

0 0
原创粉丝点击