美文网首页androidiOS进阶Android
把RecyclerView撸成 马 蜂 窝

把RecyclerView撸成 马 蜂 窝

作者: 柴泽建_Jack | 来源:发表于2016-09-15 18:32 被阅读13803次

    前几天我看到一篇文章很有趣:

    Android自定义蜂窝图实现

    于是我将文章中源码下载下来看了一下,发现只支持7张图,不能多不能少。而且在设计上也有一定的欠缺。不过也给我提拱了一种思路。谢谢这位作者的提供的灵感!

    于是想想自己的RecyclerView系列正好要讲LayoutManager了,那么我来做一个类似上面功能的LayoutManager好了。那么下面我来教大家一步一步把你的RecyclerView撸成马蜂窝。

    源码地址:HiveLayoutManager

    1 成果展示

    首先我们先看一下我们要实现的目标:

    静态展示:

    横向的正六边形布局:

    卧似一张弓

    纵向的正六边形布局:

    站似一棵松

    插入:

    南拳

    删除:

    北腿

    移动:

    走路一阵风

    滚动:

    多少我都能显示

    是不是心动了。实现这些只需要一行代码:

    recyclerView.setLayoutManager(new HiveLayoutManager(HiveLayoutManager.VERTICAL));
    

    正六边形图片的显示,请看我的另一篇文章:正六边形ImageView。然后关键就在于这个HiveLayoutManager。那么接下来教大家一步一步通过自定义LayoutManager来实现上面的功能。下面的都会以纵向为例。横向类似。

    2 蜂窝布局策略

    第一步我们先制定布局策略,然后根据我们的布局策略,确定每个View的位置,然后对View进行布局。那么看一看我们我们希望怎样布局?看图:

    一个的时候在中间,很多的时候一圈圈

    那么我们可以抽象的想象一下,把这种布局看成一种从内到外的线性布局。我们把一圈圈的看成层,最中心是第0层,然后外面一圈是第1层,然后依此类推,我们将其定义为floor,下面示意图中的红线。然后,每一层中的又有一定规律数量的View。那么我们规定最右边的是第0个,然后逆时针方向依此为1,2……我们将其定义为index,下面示意图中的绿线。那么我们就可以为RecyclerView中每一个Data的position,确定其在蜂窝布局下的位置,该位置坐标可以用(floor,index)表示。

    示意图

    那么得到position(floor,index)的对应关系,就要找到他们之间的规律。观察图上面图片,然后读者可以自行在纸上多画几层。然后我们将层数与每层包含View的个数列出,规律如下。

    层数 包含的View的个数
    0 1
    1 6
    2 12
    3 18
    …… ……
    n 6n

    这个规律很快就找到了,那么我们由position(floor,index)的算法也很简单了。这里就不讲了,具体计算方法见源码中HiveMathUtils中的getFloorOfPosition方法。

    3 计算View的屏幕显示区域

    布局策略确定之后,我们需要计算出,具体坐标下View在屏幕上显示的区域。那么我们以下步骤来做:

    3.1 计算第一个View的显示区域

    第一个正六边形

    第一个正六边形,我们将它放置在RecyclerView的中心,那么正六边形的中心与RecyclerView中心重合。那么很容易计算出第一个View的显示区域。这里不贴代码了。有兴趣的可以看源码。

    3.2 计算出第一层所有View的显示区域

    第一层的正六边形

    因为第一层是六个围着第一个正六边形的六个正六边形,(PS:打完这句话的我自己差点吐了,这句话有毒!)。那么我们还是先按照第一个正六边的思路,首先想办法得到这六个正六边形的中心点,然后再按上面的方法计算View的显示区域。

    仔细观察可以发现,所有的中心点,都在距离第一个正六边中心点 根号3 倍边长 为半径的圆上。只是角度不同而已。角度的规律也很好找。那么计算出第一层里所有View的中心就很简单了。代码不贴了,请下载源码查看:HiveMathUtilscalculateCenterPoint方法。

    既然中心点可以得到了,那么再按照上一节中的方法得到每一个View的显示区域也是轻而易举。

    3.3 计算出第n层的所有正六边形的位置(n>1)

    那么,第n层的所有View的显示区域,我们要怎么计算呢?这里是这个布局策略计算上最难的一点。这估计也是为什么我看到的那篇文章中的作者只支持7个的原因吧。不过他前7个View显示区域的获得方法也和我完全不一样。再读的你也可以想象如果是你要怎么做?这里提醒一下,我们前面两个步骤可以很大程度的复用。

    好,我来讲思路。比如第2层的所有View,显然可以根据第1层的View获得。那么看图:

    天才第一步

    图中第2层中的这三个橘红色的正六边形是不是可以根据前面的方法,通过第1层中的绿色正六边形获得?显然是可以的。但是我们总不能把第一层的6个View遍历一次,然后每次算出围绕着它六个正六边形的位置。然后再找出位于第2层中。所以我们要确定一个由n-1层生成n层View位置的规律。

    那么看一下第1层到第2层,我们可以这样生成:

    天才第二步

    如果我们把六边形的每一条边按下图编号:

    那么我们将第1层中,六边形生成关系对应的position和对应相邻边列出来:

    position 对应的相邻边
    0 0,1
    1 1,2
    2 2,3
    3 3,4
    …… ……
    p p%6,(p+1)%6

    规律也找到了,那么我们这就可以根据第1层计算出第2层了,而且也不会重复计算。那么第2层到第3层是不是也是如此呢?先看图。

    Fuck

    谁能告诉我那个绿色的是什么?如果再看第4层,就会有两个这种绿色的正六边形。然后我们发现,一条边上的正六边形分为两种,一种是角上的,一种是中间的。那么这两种是不一样的。那么我们就把上图中两个绿色的连起来。这里不贴图了,脑补。那么我们再把position和生成的对应边列出来,floor为对应的层数。

    position 对应的相邻边
    0 0,1
    1 1
    2 1,2
    3 2
    4 2,3
    5 3
    6 3,4
    7 4
    8 4,5
    …… ……
    p%floor==0 p/floor%6,(p/floor+1)%6
    p%floor!=0 (p/floor+1)%6

    那么好,p%floor==0就是角上的正六边形,p%floor!=0就是边上的正六边形。然后我们在此找出了其中的规律,根据这个规律,我们便可以由(n-1)层得到n层的所有的View的显示区域了。好,代码不贴了。请自行下载源码。

    4 填充布局View

    既然根据上面的方法,我们已经可以得到任何一个position上View的显示区域,那么就来重写onLayoutChildren方法,在里面为所有的View布局吧。

    首先:获取当前Item的个数:

    // 先解绑和回收所有的ViewHolder
    detachAndScrapAttachedViews(recycler);
    // 获取当前Item的个数,就是Adatper中数据的个数。
    int itemCount = state.getItemCount();
    // 这里我们将每个View的显示区域信息放在Rect中,然后缓存起来,如果没有的,在这里计算生成。
    checkAllRect(itemCount); 
    // 遍历所有的item
    for (int i = 0; i < itemCount; i++) {
        // 得到当前position下的视图显示区域
        RectF bounds = getBounds(i);
        // 通过recycler得到该位置上的View,Recycler负责是否使用旧的还是生成新的View。
        View view = recycler.getViewForPosition(i);
        // 然后我们将得到的View添加到Recycler中
        addView(view);
        // 然后测量View带Margin的的尺寸
        measureChildWithMargins(view, 0, 0);
        // 然后layout带Margin的View,将View放置到对应的位置
        layoutDecoratedWithMargins(view, (int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);
    }
    

    那么这样我们就可以把所有的View添加到RecyclerView上,并且布局到对应的位置上了。

    但是,现在我们的RecyclerView还不能滑动。而且是将所有的Item都生成了View,并添加进来了,只是不能滑动我们还看不到,那些出了边界的我们看不到。要想将看不到部分的View不现实,判断一下就可以。这里我不贴代码了,有兴趣的看源码。源码已经做了处理。

    5 实现滑动

    实现滑动要重写canScrollHorizontallycanScrollVertically两个方法。canScrollHorizontally控制是否可以水平滑动,canScrollVertically控制是否可以垂直滑动。这两个方法默认返回false。因为我们这里要上下左右都可以滑动,那么我们这两个方法都返回true。

    这样做了之后,我们发现我们在滑动的时候,RecyclerView旁边会出现边界效果,但是我们里面的View却没有动。那么要实现里面View的滑动,就要实现scrollHorizontallyByscrollVerticallyBy两个方法。scrollHorizontallyBy是控制水平滚动的,scrollVerticallyBy是控制垂直滚动的。

    scrollVerticallyBy为例:

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 使用该方法垂直移动RecyclerView中所有的View
        offsetChildrenVertical(-dy);
        return dy ; 
    }
    

    scrollHorizontallyBy方法类似。这里不贴代码了。但是这样会发现可以无限滑动。我们希望的是我滑到没有View了就不能滑动了。那么这样我们需要一些处理来实现。通过控制offsetChildrenVertical方法传入的值来控制滚动的距离,以及控制scrollVerticallyBy的返回值来控制是否触发边界效果,返回值为0触发RecyclerView的边界效果。这里具体代码不贴了,请自行下载源码查看。

    然后,这样之后还会又一个bug,就是当我们执行添加,删除Item的时候,所有View都会复位。那么这样我们就需要在每次滑动的时候,记录累计滑动距离,并在添加布局View的时候加上这个偏移量布局。

    6 滚动过程中View的回收和填充

    在滚动过程中我们希望将新划入的View添加进来,将滑出的View回收掉,那么这里我们就需要在scrollVerticallyByscrollHorizontallyBy添加相关的处理。

    我们将该操作封装到scrapOutSetViews方法中,并在offsetChildrenVertical方法之后调用:

    private void scrapOutSetViews(RecyclerView.Recycler recycler) {
        // 获得当前View的个数
        int count = getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            // 遍历每个View,然后是不是和RecyclerView的边界相交
            View view = getChildAt(i);
            if (!RectF.intersects(new RectF(0, 0, getWidth(), getHeight()), new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()))) {
                // 根据view得到对应的position
                int position = getPosition(view);
                // 清除该位置显示的标志为,表示该位置上的View没有显示在界面上
                booleanMap.clear(position);
                // 如果不相交,回收这个View
                detachAndScrapView(view, recycler);
            }
        }
    }
    

    滑动的时候填充新进入的View,这里我们将之前onLayoutChildren中填充的部分抽离出一个fill方法来,并加入区域过滤,然后在scrapOutSetViews方法执行完调用:

    private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
        int itemCount = state.getItemCount();
        if (itemCount <= 0) {
            return;
        }
    
        checkAllRect(itemCount);
    
        for (int i = 0; i < itemCount; i++) {
            RectF bounds = getBounds(i);
            // layoutState.offsetX和layoutState.offsetY中保存了RecyclerView滑动的累积偏移量。
            bounds.offset(layoutState.offsetX, layoutState.offsetY);
            
            // 在没有显示在界面上,并且和RecyclerView的区域有交集则填充并布局View
            if (!booleanMap.get(i) && RectF.intersects(bounds, new Rect(0, 0, getWidth(), getHeight())) {
                View view = recycler.getViewForPosition(i);
                addView(view);
                measureChildWithMargins(view, 0, 0);
    
                layoutDecoratedWithMargins(view, (int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);
            }
        }
    }
    

    实现到这里,基本上功能都全了。

    注意:本文中的代码并非源码,我只拿出了部分关键代码,有兴趣的欢迎下载查看源码。

    7 总结

    重写一个LayoutManager的需求并不大,系统为我们提供的那几个LayoutManager基本上已经覆盖了99%的RecyclerView的需求,但是现在,即使我们遇到这1%,也不用怂了!那么最后我来总结一下自定义LayoutManager的心得吧。

    实现步骤如下:

    1. 确定自己的布局策略
    2. 重写onLayoutChildren方法实现填充布局
    3. 重写canScrollXX方法支持滚动
    4. 重写scrollXXBy方法实现滚动
    5. 控制滚动范围和边界效果
    6. 处理滚动中View的回收和填充

    注意recycler.getViewForPosition(i)方法只会从缓存中或者新生成一个View,并不会检查是否已经显示,所以自行过滤显示的状态。不在同一position填充View,这种情况很难用肉眼发现。因为这两个View是重叠的,肉眼看不到,但确实存在。

    8 最后

    最后,我想说,祝大家中秋节快乐!月饼节快乐!

    这篇文章写了6个小时,中秋节呢,如果觉得不错,打个赏呗。我现在穷的连月饼都吃不上。要哭脸.png

    即使不打赏,我也会坚强的说谢谢阅读! 23333333333333333

    相关文章

      网友评论

      • Eren丶耶格尔:厉害了大佬,如果弄一个心形,上面都是女朋友的照片,送给她当礼物,岂不美滋滋
        柴泽建_Jack:@Eren丶耶格尔 这个创意好
      • 7ef30dd337f6:进入时设置默认值index = 7就会报错IndexOutOfBoundsException,除了7,设置成任何大于等于零的数字都不会出错,大神能修复一下吗?
      • ad32582be739:《把RecyclerView撸成 马蜂窝 - 简书》写的挺不错的,已经收藏了。

        源码解析:http://tinyurl.com/ycqn86hj


        8afe04d66ee7:恩恩

        还不错那
      • AndyZX:请问作者用了多久研究出来的?我感觉好难,看了好几遍都没有思路!
      • 池塘细雨:果然是马蜂窝
      • sendtion:不错不错
      • space0o0:酷毙了
      • MrWang915:骚年 果断关注你了
        柴泽建_Jack:@MrWang915 嗯……好像是,哈哈
        MrWang915:@柴泽建_Jack 哈哈
        因为我看了你三条博客
        柴泽建_Jack:@MrWang915 一看3条评论,全是你,丧心病狂……
      • c3259d529be1:index 默认值设置大于7就跑不起来了
        柴泽建_Jack:@尘埃落尽 奥,已经改了。忘了回了。:smile:
      • KhadaJhin_:厉害
      • 一个盖世英雄:因为第一层是六个围着第一个正六边形的正六边形
      • 遇见_未见:可以把view计算逻辑改一下:第一层一个;第二层六边形中心点连线又构成一个六边形,每条边上两个小六边形;第三层同样,每条边三个小六边形;计算中心点连线成的六边形的顶点,然后根据订单计算小六边形的六个顶点坐标,两个顶点中间的六边形中心点坐标可以根据顶点坐标计算;这样依次计算每一个小六边形的位置。这样应该是可以的。
      • 莴苣:不能用AS直接编译吗?
        柴泽建_Jack:@张玲飞的QQ号 可以呀,你那里不可以吗?
      • zouzhenglu:关于卡的问题,我看了下源码,在滚动的时候每次重绘都会全部重新计算,这里可否考虑在这个时候去掉这个计算,比如,把recyclerView当成在(0,0)的位置,然后再移动整个recyclerView,这样,在移动的时候就只需要计算外部位置,而不需要重新计算所有item了
        柴泽建_Jack:@zouzhenglu 没明白 :joy: 加我微信,我主页有二维码,我们交流一下!
      • zouzhenglu:赞!学到了。不过我发现了两个问题,1:item 多 了之后就会卡了很多。2:新增item的时候位置的变化让人摸不透规律
        zouzhenglu:@柴泽建_Jack 正常的情况下,正在显示的item如果不移出屏幕,不应该触发任何操作
        柴泽建_Jack:@zouzhenglu 我Demo中,添加就是在随机位置插入的。只是做演示。删除也是删除随机位置的。移动也是。多了卡是有待优化。我觉得我Demo中卡是因为我图片没有做缓存,每次都重新加载的。在滚动的时候就在不断的回收重用ViewHolder,但我偷懒直接在UI线程加载的图片。不过还不确定,我晚上看一下。
      • 567dacd0f6dd:layoutDecoratedWithMargins(view, (int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);这个方法能不能贴出来,我这报错,找不到这个方法
        柴泽建_Jack:@音符跳跃思念每天 你在使用Eclipse!
        567dacd0f6dd:@柴泽建_Jack 好的。谢谢。我把代码复制到这eclipse这块了。
        柴泽建_Jack:@音符跳跃思念每天 这个是RecyclerView中自带的方法。compile 'com.android.support:appcompat-v7:24.2.0'使用这个版本及以上的RecyclerView。如果你不小心把这个版本改了,可能会报这个错,这个版本grable那个地方是红线不用管它,可以编译通过。改完版本之后同步一下gradle。这样试一试。
      • 彼岸_浅陌:github源码中在README.md里面写个简单详细的介绍,相信会有更多star的。加油!
        柴泽建_Jack:@彼岸_浅陌 好!收下!下周搞一下!
      • 于连林520wcf:太牛逼
      • zp_风:不错
      • 大大大大峰哥:等下就撸你的源码
        柴泽建_Jack:部分代码我加了注释,你有空可以撸一下,多多提意见呀!
        柴泽建_Jack:@大大大大峰哥 :smile::smile:可以,现在源码很乱只实现了功能,都没整理。
      • songdehuai:666666
      • aa5ba408bfcb:厉害了我的歌 :grin:
        柴泽建_Jack:@MisaShaw 啊?
      • 7b562f3e5ec9:挺有意思的,不过有点问题,数量多一点的时候随机出现,错误如下:java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled: ViewHolder,楼主可以看看
        柴泽建_Jack:谢谢反馈,这个bug改了。你可以再看一下。
        柴泽建_Jack:@離冬_ 嗯,有人提过了,谢谢!下周把这些问题统一处理一下。:stuck_out_tongue_winking_eye:这周有这周的任务。:joy::joy::joy:,大家有什么问题尽管提,我下周统一解决,然后回复大家!
      • 71caf019970b:干货啊,顶楼主,期待有一天可以跟楼主一样成为大牛。
        柴泽建_Jack:@赛飞 刚入职,还是小白……
      • 虞愚yu:流弊,收藏 学习了
      • d4467e792a81:楼主,你是自学还是培训啊?水平太高了,求带啊 :smile:
        柴泽建_Jack:@d4467e792a81 自学呀。其实也没什么吧。只是看上去炫一点而已。
      • 风之丨旅人: :clap: 呱唧呱唧
      • Zack_zhou:很厉害,你六芒星ImageView昨天刚刚看过。
        但是我想不到使用场景 :cold_sweat:
        柴泽建_Jack:@Zack_zhou 哈哈,其实我是想到这个蜂巢布局管理器,才去做的那个六芒星,而且那篇写的挺不认真的,还被编辑推到了主页,感觉挺惭愧的,随便看看吧。不过就是这个蜂巢布局管理器我感觉使用场景也不多吧。所以,我本想的是演示自定义LayoutManager做做而已,不过没想到大家这么喜欢这个效果。那我接下来好好完善一下,现在应该还有很多bug,然后我再发个库吧,如果想用,方便大家使用。
      • 6c2013a2ecf1:吊吊吊。。。
      • 32135776c20b:java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled: ViewHolder{24bb2157 position=32 id=-1, oldPos=-1, pLpos:-1 tmpDetached no parent}
        at android.support.v7.widget.RecyclerView$Recycler.recycleViewHolderInternal(RecyclerView.java:5235)
        32135776c20b:@柴泽建_Jack 哈哈,修复完推上github了来通知一下哦 :smile: ,持续关注中 , 写的很好 :+1:
        柴泽建_Jack:@晓游 我看看,现在只是实现了基本功能,主要是为了演示自定义LayoutManager,所以没有经过大量测试。没想到大家会这么喜欢这个效果。😄😄哈哈
        32135776c20b:@晓游 add 多点几次,点到满屏幕都是的时候 移动几下 就 崩溃了
      • 32135776c20b:博主巨屌 :+1:
      • 陌痕丶:作者, 请问你画图用的工具是啥?
        柴泽建_Jack:@陌痕丶 ……说出来不要笑话我,PPT:cold_sweat::cold_sweat::cold_sweat:
        陌痕丶:@柴泽建_Jack 不是,就是你的原理分析图, 一圈,两圈,那个是用什么画的呢?
        柴泽建_Jack:@陌痕丶 什么意思?你说的动态图吗?
      • 捡淑:66666
      • Champion是冠军:这个效果很叼
      • wulei2554:真会玩! :+1:
      • 积木Blocks:厉害啊!
      • Tang1024:还能再牛逼点么:stuck_out_tongue_winking_eye:
        柴泽建_Jack:@撒旦之恋歌 能! :smile:
      • 7e9f307192a9:效果不错
      • 进击的包籽:recycleview真的超级厉害的,左轮
        柴泽建_Jack:@Good包籽 哈哈,确实有点像,我都没发现。
        进击的包籽:@柴泽建_Jack 这个插入很像左轮手枪🔫
        柴泽建_Jack:@Good包籽 左轮?
      • 欧阳鹏:有意思
      • 3f31ddd82d9d:很有意思!
      • wulei2554:加油,期待下一篇干货!
        柴泽建_Jack:@wulei2554 嗯,好。有兴趣可以看看我之前的文章。也都是干货。
      • a9c99f65dd58:真棒,彻底玩转rv了,多谢分享
        柴泽建_Jack:@伊水古城 不客气,乍一看吓死我了,看成VR了。我还想我什么时候进军VR。哈哈
      • 彩笔怪盗基德:这干货真是极好的
        柴泽建_Jack:@彩笔怪盗基德 谢谢!:smile::smile:我会继续分享的!
      • 英勇青铜5:收藏学习
        柴泽建_Jack:@英勇青铜5 嗯嗯
      • hfk:不错,作者敬业呀,中秋还在撸码
        柴泽建_Jack:@hfk 台风来了,在下大雨,出不去。:smile::smile::smile:
      • zhouzhuo810:哈哈,不错,我那只是玩玩,刚好看到别人有这种需求
        柴泽建_Jack:@zhouzhuo810 嗯,今年毕业,中南大学。你呢?
        zhouzhuo810:@柴泽建_Jack 是的,在群里看到别人问怎么做 我就做了个 哈哈 你也是今年毕业的啊 你哪个学校的啊
        柴泽建_Jack:@zhouzhuo810 就是看到你来的灵感。还真有人有这种需求?
      • 木TT:很赞
        柴泽建_Jack:@tao 还好吧,要是喜欢等我再完善一下,发个库吧。哈哈
      • 柴柴777:效果很好玩哈哈哈
        柴泽建_Jack:@学编程的女生么 就知道吃,编程就是我的精神食粮。:sunglasses::sunglasses::sunglasses:
        柴柴777:@柴泽建_Jack 吃
        柴泽建_Jack:@学编程的女生么 嗯,饿死了!还没吃饭呢:pensive:

      本文标题:把RecyclerView撸成 马 蜂 窝

      本文链接:https://www.haomeiwen.com/subject/dysbettx.html