背景
1.一直有那么一个冲动,想写一个自己的控件,然后开源在Github,充满着莫名的成就感。
2.正好朋友的需求给了我灵感,然后对这个控件产生了自定义的冲动。
3.本身最近在学习RecyclerView和自定义View,正好可以巩固一下知识点,并且还有成就感。
4.这个控件,看起来挺小众的,但是你看图片会感觉这个控件挺有创意的,学会了HexagonRecyclerView,那么类似的奇怪的需求几乎都没有问题了。
5.这也是这个月的最后一篇博客,这个月给自己的小目标也算完成了。
前言
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
HexagonRecyclerView介绍篇
HexagonRecyclerView的基本介绍
HexagonRecyclerView是一个由2列正六边形组成的RecyclerView,可以做侧边索引,可以作为导航栏来使用,是通过自定义LayoutManager,来实现这样的效果。
Github开源地址,欢迎Star
具体的介绍这里可以参考Github的ReadMe或者我的这篇博客:
HexagonRecyclerView的使用介绍
HexagonRecyclerView的基本组成
虽然名字中带有RecyclerView,但是HexagonRecyclerView的库中却没有重新定义RecyclerView,而是重新定义LayoutManager,因为LayoutManager是负责RecyclerView的绘制、布局、测量的。
HexagonRecyclerView库中包括:
(1)PolygonItemView :为自定义的正六边形View
(2)PolygonLayoutManager : 为RecyclerView展示需要的LayoutManager
(3)Pool : 自定义的View的存储池,负责View的复用。
(4)MathUtil : 存储公式的静态类,负责三角函数公式的调用。
HexagonRecyclerView的简单使用
Adapter的ItemView
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
RecyclerView的LayoutManager
- 1
- 2
- 3
- 1
- 2
- 3
详情,请见HexagonRecyclerView的具体使用Simple
自定义LayoutManager的思路
其实,当你了解LayoutManager之后,你只需要配合一些数学基础,就能写出这样的控件。如果你有心,看了这个库,你会发现其实没多少代码,当然,这个库能写出来最大的核心点就是自定义LayoutManager。
那么下面就开始介绍下自定义LayoutManager的思路:
(1)先设置RecyclerView的子View的LayoutParams参数。
(2)通过Rect将每个子View的测量位置记录,然后缓存起来。
(3)判断当前Rect是否在屏幕上,如果存在就对其布局操作即执行layoutDecorate()方法。
(4)设置RecyclerView的滑动方向,分别通过canScrollHorizontally(),canScrollVertically()设置横向或者竖向能否滑动。
(5)设置RecyclerView的滑动,先设置其上下边界,然后赋予偏移量,重新布局即形成滑动效果。
设置子View的LayoutParams参数
首先,要继承RecyclerView.LayoutManager就必须实现generateDefaultLayoutParams()这个方法,然后设置其子View的LayoutParmas
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
复写onLayoutChildren()方法
然后,我们复写其onLayoutChildren()方法,因为在自定义View的过程中,测量,布局,绘制是必不可少的步骤,而在完成基本自定义LayoutManager,我们只需要重点关注其布局和滑动步骤,而onLayoutChildren()作为LayoutManager的布局方法,在自定义LayoutManager中,属于最核心的部分。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在这里我们可以,将onLayoutChildren()大致分为三个阶段,分别为准备阶段、测算阶段、布局阶段。
(1)第一阶段 - 准备阶段
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
(2)第二阶段 - 测算阶段
- 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
在这里需要自定义一个Rect的池,来储存生成Rect的范围,最后用于判断该View是否处于屏幕上,负责会被回收掉。
(3)第三阶段 - 布局阶段
- 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
这里的displayRect是当前显示的区域,然后我们通过Rect.intersects()方法,判断子View是否与displayRect相交,如果相交即子View显示在RecyclerView的展示区域上,然后会对该子View进行布局操作。
这里需要注意的是,在计算每个子View的位置时,需要考虑RecyclerView滑动的偏移量。
处理RecyclerView的滑动
其实,经过上面的两个步骤,RecyclerView显示已经没有什么大的问题了,如果是自定义LayoutManager不需要考虑滑动,其实这样已经能看到效果了,那么接下来我们应该处理RecyclerView的滑动,来使控件能够得到更好的体验。
处理RecyclerView滑动也分两个阶段:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
(1)第一阶段 - 设置控件能否滑动
设置RecyclerView在横向能否滑动:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
设置RecyclerView在竖向能否滑动:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
上述两个方法,根据返回值来决定当前RecyclerView在各个方向上是否可以滑动。
(2)记录偏移量,并且设置子View偏移,重新进行布局操作
设置纵向偏移
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
设置横向偏移
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
通过上面两个方法,可以很容易的发现,其实处理横向或者纵向滑动很简单,上述两个方法,几乎就是模板方法。
不管是横向、或者竖向滑动,这两个方法处理的流程几乎都是一致的,如下:
(1)首先进行边界判断,滑动到边界,就不能继续在滑动了。
(2)然后记录横向或纵向的偏移量,进行offsetChildrenHorizontal()或者offsetChildrenVertical()操作,使子View进行偏移。
(3)最后调用fill(),进行布局操作,完成RecyclerView的滑动处理。
在这里getHorizontalSpace()、和getVerticalSpace()都属于辅助方法 :
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
而mTotalWidth 和mTotalHeight 则是在onLayoutChildren()提前计算好的,分别代表LayoutManager中自定义的内容宽度,和高度。
到这里,其实自定义LayoutManager的基本流程,已经基本完成。
最后来一张图,总结一下:
友情提示:由于图片幅度过大,需要放大到150%,才能清晰观看。
HexagonRecyclerView的自定义流程
下面的内容,将会着重介绍HexagonRecyclerView这个组件的自定义流程,如果对此感兴趣,不妨来一发Star。
容许我在来一次Github链接
HexagonRecyclerView的自定义流程分为两个阶段:
(1)自定义 LayoutManager,以此来控制正六边形的显示。
(2)自定义正六边形 View,以此来高度定制 RecyclerView 中的展示风格。
PolygonLayoutManager的自定义流程
(1)先设置RecyclerView的子View的LayoutParams参数。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
在这里对子 View 的大小没有特殊要求,所以宽高自适应就可以。
(2)onLayoutChildren()的三个流程的实现
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
第一阶段 : 准备阶段
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
一般来说,这个阶段,所需要做的操作,几乎都是固定的。所以不需要做太大改动。
第二阶段 :测算阶段
这一阶段比较重要,决定着正六边形的展示形式,可以调整边距,以及展示位置。
- 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
- 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
在这一阶段,主要做了这几件事:
(1)测量第一个子 View 的宽高,然后获得当前正六边形的外接圆半径。
其实每个正六边形,都是这么画出来的,先做一个辅助的外接圆,然后寻找每一个相交的点就可以。
这里我们能够测量出该控件的宽为 AB,高为 AD,这时候外接圆的半径为 AB的一半。
(2)我们需要算出正六边形竖向之间的距离,请看下图:
在这里先说明两个常量,mLandscapeInterval 和 mVerticalInterval :
mLandscapeInterval:代表的是正六边形形成的正三角形的中线,如图就是那条蓝色的线。
注意:横向间距这里是通过自定义实现的,也就是你自己来设定的。
mVerticalInterval:代表的是竖向间距,即等边三角形的边长 减去 2倍的 外接圆与正六边形的边界差。
我们看自定义的正六边形图,可以发现View的边界不是在正六边形上,而是在外接圆上。所以需要将多余的地方减掉。
- 1
- 1
对比下这两幅图,可以很容易的出这样的公式:
纵向距离 = AB - 2 * (R - R *sin60°)
(3) 计算每组正六边形的最大宽度,是否小于RecyclerView的宽度,如果小于,则计算出偏移量,使其居中。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这里需要说明的是,我们指的一组是两个正六边形,上图中正六边形A、正六边形C是一组。
然后通过横向间距,我们可以很好的得出当前的每组的宽度,然后比较。
(4)计算每个Item所处的Rect,这里的计算分成两个部分,一个是第一排的Rect,另一个是第二排的Rect,因为横向间距不同,这里做一个分别处理。
isItemInFirstLine是判断当前Item处于第一排还是第二排
第二排的Rect,这里有几个特点:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
(5)最后需要根据item的个数,计算出总的内容长度和高度为计算滑动边界做准备
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
第三阶段 :布局阶段
这一阶段,最为核心的方法是layoutDecorated()方法,因为LayoutManager是通过这个方法,来给RecyclerView的子View进行布局操作的。
- 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
这里displayRect 就是滑动过后显示的区域Rect,然后我们通过Rect.intersects()方法,判断当前的Rect,是否与displayRect相交,来判断当前的Item是否显示在当前的窗口上。
如果相交,然后测量View,将子View布局在RecyclerView上。
到这里,onLayoutChildren()已经重写完毕,现在可以运行可以查看,自定义的布局的状况,但是无法滑动,因为我们还没有给LayoutManager设置滑动的效果。
(3)设置RecyclerView的滑动效果
设置滑动效果,就比较固定了,有点类似模板的方法,如下:
- 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
这里只希望RecyclerView竖向滑动,不希望横向滑动,所以只设置了canScrollVertically()为true。
而scrollVerticallyBy()中无非是三个步骤:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
到此,LayoutManager已经自定义完毕了,顺着这个思路下来,可以发现,其实和第一部分介绍自定义LayoutManager一个模式,都是一些通用的流程,然后加上一些辅助计算的常量,就可以实现需要的效果。
其实自定义LayoutManager并没有那么难,了解了这些你也能写出属于你的LayoutManager。
PolygonItemView的自定义流程
大家读完上面,其实对自定义LayoutManager有一定印象了,那么直接给Item设置一张图片背景正常就可以结束了。这样只能说too young了。
点击区域重合
通过上图大家可以发现,这里的点击区域发生了重合,这时候就会存在问题。所以这时候才会选择自定义View。
而且自定义View相比使用图片背景,会有很多优点,也可以高度定制。
- 1
- 2
- 3
- 1
- 2
- 3
PolygonItemView的初始化
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
在初始化的过程中,主要初始化各类的绘制参数,包括画笔,已经绘制路径的path,和判断其是否在点击区域内的Region。
绘制正六边形
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
看了这样图,就可以很好的画出正六边形了,将360度分成6分,然后( mCenterX + R*cos α ,mCenterY + R * sinα)就是正六边形的每个端点的坐标,在用Path依次将其连接就可以了。
判断点击区域
其实自定义View,主要解决的是点击区域重合的问题,这里应该在dispatchTouchEvent()中拦截掉事件,然后判断其点击区域是否在正六边形内,来解决点击重合。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
到此,自定义View,也介绍完毕了。相比于自定义LayoutManager,自定义View更好理解一点。在这里如果有想法可以将正六边形换成其他的图形,完成属于你自己的创作。
HexagonRecyclerView后续的一些扩展方向
(1)从PolygonLayoutManager上,后续会提供多列的RecyclerView,通过设置参数来控制列数。
(2)在自定义View上,可能会提供设置图片的功能。
(3)后续可能会提供更多种不同形式的RecyclerView,其实原理都是类似的,更多的是创意。