FormLayoutManager -- 解说(1)

吃可爱长大的小学妹 提交于 2020-01-08 16:09:50

FormLayoutManager首页,里面有github地址

目录

前言

构造方法

onLayoutChildren

handleLayoutChildren

总结


前言

接下来会一步步带大家走进FormLayoutManager。在写这篇博客之前,我已经实现可以不同行或不同列多类型布局。所以下面讲解是以最新的v2.0代码来说明,后面也会介绍到怎么实现多类型。FormLayoutManager的完整代码有点长,我就不贴出来,大家可以自己看着源码,一边看文章,我们由上至下来说。

构造方法

    public FormLayoutManager(int columnCount){
        mColumnCount = columnCount;
    }

    public FormLayoutManager(boolean isHorV, int count){
        this(isHorV, count, null);
    }

    /**
     * 什么场景需要传入RecyclerView
     * 在滚动过程会刷新的数据的时候,最好设置RecyclerView
     */
    public FormLayoutManager(int columnCount, RecyclerView recyclerView){
        this(true, columnCount, recyclerView);
    }

    public FormLayoutManager(boolean isHorV, int count, RecyclerView recyclerView){
        mIsHorV = isHorV;
        if (isHorV){
            mColumnCount = count;
        }else{
            mRowCount = count;
        }
        mRecyclerView = recyclerView;
    }

isHorV是标志是水平表格还是垂直表格。默认是大家常用的水平表格,最基本的构造方法就是要传入列数。也提供一个构造方法让大家设置isHorV,来告诉FormLayoutManager你的表格水水平表格还是垂直表格,而当时垂直表格时,传入的count就代表列数了。

    private int getColumnCount() {
        if (mIsHorV){
            return mColumnCount;
        }else{
            return (getItemCount() - 1) / mRowCount + 1;
        }
    }

目光来到getColumnCount,当为水平表格时,列数就是我们构造函数传进来的那个columnCount的值。而当它为垂直表格的时候,我们列数是要根据行数计算。行数的getRowCount同理。

目光请有回到构造函数那里,其中有两个方法是要传入RecyclerView。注释有解析到如果你的表格在你滚动情况下也会突然刷新数据,建议设置一下RecyclerView,因为刷新数据的时候,我们希望表格刷新的是当前可以看到的view,用了RecyclerView来执行的刷新方法比较快一点,后面再细说。

onLayoutChildren

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mItemCount != getItemCount()){
            mItemCount = 0;
        }
        if (mItemCount == 0 || mRecyclerView == null){
            mItemCount = getItemCount();
            handleLayoutChildren(recycler);
        }else{
            // 防止数据在更新的时候,用户又在滑动表格,这时会看到卡顿现象
            // 第二种刷新当前界面可视的view,不过要设置RecyclerView,但刷新时间短
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int position = getPosition(child);
                mRecyclerView.getAdapter().onBindViewHolder(mRecyclerView.getChildViewHolder(child), position);
            }
        }
    }

我们先来看else下面的方法,这段代码就是整个FormLayoutManager里面唯一一段用到RecyclerView的代码。这段代码很好理解,就是遍历界面可视的item数getChildCount,然后逐个调用onBindViewHolder来实现刷新该item。其实if下面的handleLayoutChildren里面也是只会刷新当前可视的item,所以整个FormLayoutManager就算你不设置RecyclerView,是一样可以正常用的。我只是考虑,但表格的总item数不变且要刷新数据的时候,没必要执行一个handleLayoutChildren这么庞大的一段逻辑,直接用else下面的方法更快。

        if (mItemCount != getItemCount()){
            mItemCount = 0;
        }

上面这个让mItemCount归0是干什么的呢?因为要执行handleLayoutChildren的判断条件是mItemCount等于0,而handleLayoutChildren里面初始化了好一些变量,所以当我们刷新数据,导致总item的数目发生变化的时候,必须要调用handleLayoutChildren来重新计算初始化那些变量。

handleLayoutChildren

1、计算每个item的Rect

接下来看一下handleLayoutChildren里面第一个大的for循环干了什么

         for (int i = 0; i < getItemCount(); i++) {
            // item所在的行和列的index
            int row = i / mColumnCount;
            int column = i % mColumnCount;

            View itemView = recycler.getViewForPosition(i);
            Integer itemViewType = getItemViewType(itemView);
            int itemWidth;
            int itemHeight;
            if (mItemViewSizeMap.containsKey(itemViewType)){
                itemWidth = mItemViewSizeMap.get(itemViewType).width;
                itemHeight = mItemViewSizeMap.get(itemViewType).height;
            }else{
                measureChildWithMargins(itemView, 0, 0);
                itemWidth = getDecoratedMeasuredWidth(itemView);
                itemHeight = getDecoratedMeasuredHeight(itemView);
                mItemViewSizeMap.put(itemViewType, new ItemViewSize(itemWidth, itemHeight));
            }

            Rect rect = getViewRect(row, column, itemWidth, itemHeight);
            mItemRects.add(rect);
            mHasAttachedItems.add(false);
        }

上面是遍历了总item的个数,之前没做多类型表(多类型表指不同行或不同列多类型)的时候,获取itemWidth和itemHeight非常简单,直接拿position为0的第一个item的宽高就行了,因为没多类型,故每个格子的宽高都一样。这个新写的for循环是考虑到多类型的情况。

LayoutManager自带有个getItemViewType获取item类型的,然后我们有个字典mItemViewSizeMap来保存不同类item的宽高。最关键是后面getViewRect,我先说一下mItemRects里面保存的是什么,这个列表里面保存的rect,其实是在不滚动的情况下,所有item(不管可视还是不可视)在界面的位置。之前没多类型,每个格子宽高一样时候,每个item的rect很好算的,就是按循环累加一下itemWidth或itemHeight就行了,现在多了多类型,就写了一个getViewRect的方法,下面我们看一下里面写了什么。

    // 获取view对应的Rect
    private Rect getViewRect(int row, int column, int itemWidth, int itemHeight) {
        int left = 0;
        int right = itemWidth;
        int top = 0;
        int bottom = itemHeight;

        if (mItemRects.size() > 0){
            Rect lastRect = mItemRects.get(mItemRects.size() - 1);
            if (mCurRow != row){
                mCurRow = row;
                left = 0;
                right = itemWidth;
                top = lastRect.bottom;
                bottom = top + itemHeight;
            }else if (mCurColumn != column){
                mCurColumn = column;
                left = lastRect.right;
                right = left + itemWidth;
                top = lastRect.top;
                bottom = lastRect.bottom;
            }
        }

        Rect rect = new Rect(left, top, right, bottom);

        return rect;
    }

上面可以看到当mItemRects.size() > 0才会执行一段计算,当它不大于0的时候,其实就是在计算第一个item的rect,它的对应属性其实就是一开始的默认值

        int left = 0;
        int right = itemWidth;
        int top = 0;
        int bottom = itemHeight;

而其他的rect就要通过if下面的代码计算了。先跟大家说明一下我们的所有item要是描绘在整个表格,它是按什么顺序把item加进去。答案就是从左到右,然后上到下,就是一个个item从左到右地加进去,一行加满就向下换行,继续左到右。好,那么就来看if下面的代码了。

            if (mCurRow != row){
                mCurRow = row;
                left = 0;
                right = itemWidth;
                top = lastRect.bottom;
                bottom = top + itemHeight;
            }

当mCurRow不等于row,即当前行不等于传进来的行时,证明开始换行,那换行的第一个item,好明显left就是0,right就是itemWidth啦。而关键的top其实就是上一个rect的bottom了,上一个的rect其实就是它的上一行的最后一个item的rect,而bottom是top加itemHeight就不多说了。那接下来

            else if (mCurColumn != column){
                mCurColumn = column;
                left = lastRect.right;
                right = left + itemWidth;
                top = lastRect.top;
                bottom = lastRect.bottom;
            }

else if  /—~—/,没错就是else if,当行相等的时候才会进入,然后判断列是否相等,不相等就说明换列,因为是同行换列,故left就是上一个rect的right,而right就不多说了,top和bottom跟上一个rect的一样。这样就准确记录了每个item的rect了。

 

2、绘制每个可视的item

        here:
        for (int row = firstShowRow; row < visibleRowCount + firstShowRow; row++) {
            for (int column = firstShowCol; column < visibleColumnCount + firstShowCol; column++) {
                int itemPosition = row * mColumnCount + column;

                if (itemPosition >= mItemRects.size()){
                    break here;
                }
                Rect rect = mItemRects.get(itemPosition);
                if (!Rect.intersects(getVisibleArea(), rect)){
                    continue;
                }
                View view = recycler.getViewForPosition(itemPosition);
                addView(view);
                //addView后一定要measure,先measure再layout
                measureChildWithMargins(view, 0, 0);
                layoutDecorated(view, rect.left - mSumDx, rect.top - mSumDy, rect.right - mSumDx, rect.bottom - mSumDy);
            }
        }

firstShowRow, firstShowCol, visibleColumnCount, visibleRowCount这四个值怎么得来,前面几段代码,我相信你能看明白。我主要说一下我贴出来的这段代码。为什么要获取第一个可视item的行和列呢(firstShowRow和firstShowCol),就是因为这两个值,才让handleLayoutChildren每次刷新,也只会刷新当前可视的item。

两个for循环为了遍历的就是可视区的所有item,我们着重看一下循环里面的代码,第一句其实就是把行和列转换成该item对应的position。先跳开两个判断,浅谈下面绘制view的代码,很明确,就是在recycler,那里根据itemPosition获取对应的itemView,然后调layoutDecorated绘制到RecyclerView上。

第一个判断,为什么itemPosition会可能大于mItemRects.size()。我们来看一下visibleColumnCount, visibleRowCount怎么算的。

        // 可视的最大行数,列数
        int visibleRowCount = (int) Math.ceil(getVerticalSpace() * 1f / minHeight) + 1;
        int visibleColumnCount = (int) Math.ceil(getHorizontalSpace() * 1f / minWidth) + 1;

拿可视最大列数作分析,看下面左图,我们那RecyclerView可视的宽getHorizontalSpace除以item的宽,得到的结果应该是2点多,然后我们向上取整Math.ceil得到的就是3,但往往这个结果都要加1,。因为只要你左滑一下就可以发现,最多可视的列其实是4列,如右图

                                      

关键是我拿来计算的不是item的width,而是所有item最小的minWidth(minWidth怎么得到的,看源码很容易理解的)。之前没多类型的时候,每个格子宽高一样,那个可视最大数是算得很准的。但由于加了多类型,那就是能考虑极端的情况,假设所有item都是最小那个width来计算可视最大数。

回到我们的两个for循环,因此visibleColumnCount, visibleRowCount可能会比实际的大,所以两个for循环所遍历的itemPosition会比实际表格的itemPosition大,有可能会越界,所以才有第一个判断出现,当这个itemPosition已经超过表格最大的那个position,我们就直接跳出两个循环。接下来看第二个判断:

                Rect rect = mItemRects.get(itemPosition);
                if (!Rect.intersects(getVisibleArea(), rect)){
                    continue;
                }

大家可以打开demo看第一个界面,第一个界面的表格上面的数字其实就是格子对应的itemPosition。

看图,不难发现可视区为起始position=16,4X7的一个表,而上面我们说了,两个for循环遍历的范围比我们可视的还大。比如两个for循环遍历可能会出现position=20,但这个格子根本就不在我们的可视区,所以存在第二个判断,当判断这个position对应的rect不在我们的可视区的时候,就继续contiunue。

handleLayoutChildren的内容基本说完了,接下来总结一下这篇博客。

总结

FormLayoutManager内容太多了,所以分几篇来说明。这篇主要还是说了onLayoutManager这个方法的重写。而我们的表格进行了什么操作才会进入onLayoutManager呢?当我们的RecyclerView从gone到可视,还有adapter数据刷新的时候,都会进这方法。这个方法尤其重要,是FormLayoutManager的主入口来的。

下一篇:FormLayoutManager -- 解说(2)还没写……

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!