一个有特点的正六边形RecyclerView---HexagonRecyclerView详解篇
本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢 博客地址:http://blog.csdn.net/l540675759/article/details/75635290
背景
1.一直有那么一个冲动,想写一个自己的控件,然后开源在Github,充满着莫名的成就感。
2.正好朋友的需求给了我灵感,然后对这个控件产生了自定义的冲动。
3.本身最近在学习RecyclerView和自定义View,正好可以巩固一下知识点,并且还有成就感。
4.这个控件,看起来挺小众的,但是你看图片会感觉这个控件挺有创意的,学会了HexagonRecyclerView,那么类似的奇怪的需求几乎都没有问题了。
5.这也是这个月的最后一篇博客,这个月给自己的小目标也算完成了。
前言
(1)HexagonRecyclerView介绍篇,它是由什么组成,然后基本的展示.
(2)自定义LayoutManager的基本思路.
(3)HexagonRecyclerView的自定义LayoutManager的过程,包括正六边形计算相关的拆分.
(4)正六边形的自定义View详解.
(5)HexagonRecyclerView后续的一些扩展方向.
HexagonRecyclerView介绍篇
HexagonRecyclerView的基本介绍
HexagonRecyclerView是一个由2列正六边形组成的RecyclerView,可以做侧边索引,可以作为导航栏来使用,是通过自定义LayoutManager,来实现这样的效果。
具体的介绍这里可以参考Github的ReadMe或者我的这篇博客:
HexagonRecyclerView的基本组成
虽然名字中带有RecyclerView,但是HexagonRecyclerView的库中却没有重新定义RecyclerView,而是重新定义LayoutManager,因为LayoutManager是负责RecyclerView的绘制、布局、测量的。
HexagonRecyclerView库中包括:
(1)PolygonItemView :为自定义的正六边形View
(2)PolygonLayoutManager : 为RecyclerView展示需要的LayoutManager
(3)Pool : 自定义的View的存储池,负责View的复用。
(4)MathUtil : 存储公式的静态类,负责三角函数公式的调用。
HexagonRecyclerView的简单使用
Adapter的ItemView
<com.vander.hexlayout.PolygonItemView
android:id="@+id/itemview"
android:layout_width="110dp"
android:layout_height="110dp"
app:innerColor="@android:color/white"
app:isFull="true"
app:outerColor="#f5c421"
app:outerWidth="1dp"
app:radius="50dp" />
RecyclerView的LayoutManager
PolygonLayoutManager manager = new PolygonLayoutManager(true);
manager.setLandscapeInterval(0);
mMainRv.setLayoutManager(manager);
详情,请见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
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
//比较默认的设置,可以根据自己的需求来定制。
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
复写onLayoutChildren()方法
然后,我们复写其onLayoutChildren()方法,因为在自定义View的过程中,测量,布局,绘制是必不可少的步骤,而在完成基本自定义LayoutManager,我们只需要重点关注其布局和滑动步骤,而onLayoutChildren()作为LayoutManager的布局方法,在自定义LayoutManager中,属于最核心的部分。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//准备阶段
initLayout();
//测算阶段
measureLayout();
//布局阶段
fill();
}
在这里我们可以,将onLayoutChildren()大致分为三个阶段,分别为准备阶段、测算阶段、布局阶段。
(1)第一阶段 - 准备阶段
/**
* onLayoutChildren 准备阶段
* @param recycler
* @param state
*/
private void initLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
//将所有子View先detach一遍,然后标记“Scrap”缓存起来
detachAndScrapAttachedViews(recycler);
}
(2)第二阶段 - 测算阶段
private void measureLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
//测量每个子View的基本信息
View normalView = recycler.getViewForPosition(0);
measureChildWithMargins(normalView, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(normalView);
int itemHeight = getDecoratedMeasuredHeight(normalView);
for (int i = 0; i < getItemCount(); i++) {
//mItemFrames为Rect的池对象,就是Rect的一个容器,索引为i。
Rect item = mItemFrames.get(i);
.....
//设置每个子View的Rect的范围
.....
}
}
在这里需要自定义一个Rect的池,来储存生成Rect的范围,最后用于判断该View是否处于屏幕上,负责会被回收掉。
(3)第三阶段 - 布局阶段
/**
* 布局阶段
*
* @param recycler
* @param state
*/
private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
//考虑到当前RecyclerView会处于滑动的状态,所以这里的Rect的作用是展示当前显示的区域
//需要考虑到RecyclerView的滑动量
//mHorizontalOffset 横向的滑动偏移量
//mVerticalOffset 纵向的滑动偏移量
Rect displayRect = new Rect(0, mVerticalOffset,
getHorizontalSpace(),
getVerticalSpace() + mVerticalOffset);
/**
* 对这些View进行测量和布局操作
*/
for (int i = 0; i < getItemCount(); i++) {
Rect frame = mItemFrames.get(i);
if (Rect.intersects(displayRect, frame)) {
View scrap = recycler.getViewForPosition(i);
addView(scrap);
//测量子View
measureChildWithMargins(scrap, 0, 0);
//布局方法
layoutDecorated(scrap, frame.left - mHorizontalOffset, frame.top - mVerticalOffset,
frame.right - mHorizontalOffset, frame.bottom - mVerticalOffset);
}
}
}
这里的displayRect是当前显示的区域,然后我们通过Rect.intersects()方法,判断子View是否与displayRect相交,如果相交即子View显示在RecyclerView的展示区域上,然后会对该子View进行布局操作。
这里需要注意的是,在计算每个子View的位置时,需要考虑RecyclerView滑动的偏移量。
处理RecyclerView的滑动
其实,经过上面的两个步骤,RecyclerView显示已经没有什么大的问题了,如果是自定义LayoutManager不需要考虑滑动,其实这样已经能看到效果了,那么接下来我们应该处理RecyclerView的滑动,来使控件能够得到更好的体验。
处理RecyclerView滑动也分两个阶段:
(1)设置控件在横向、竖向上能否滑动。
(2)记录各个方向上的偏移量,使RecyclerView的各个子View偏移,然后重新进行布局操作。
(1)第一阶段 - 设置控件能否滑动
设置RecyclerView在横向能否滑动:
@Override
public boolean canScrollHorizontally() {
return true;
}
设置RecyclerView在竖向能否滑动:
@Override
public boolean canScrollVertically() {
return true;
}
上述两个方法,根据返回值来决定当前RecyclerView在各个方向上是否可以滑动。
(2)记录偏移量,并且设置子View偏移,重新进行布局操作
设置纵向偏移
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
//上边界判断
if (mVerticalOffset + dy < 0) {
dy = -mVerticalOffset;
//下边界判断
} else if (mVerticalOffset + dy > mTotalHeight - getVerticalSpace()) {
dy = mTotalHeight - getVerticalSpace() - mVerticalOffset;
}
mVerticalOffset += dy;
//使所有ChildView偏移 如果向上滑动 所有View就向下偏移 反之亦然
offsetChildrenVertical(-dy);
//重新布局
fill(recycler, state);
return dy;
}
设置横向偏移
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
//左边界判断
if (mHorizontalOffset + dx < 0) {
dx = -mHorizontalOffset;
//右边界的判断
} else if (mHorizontalOffset + dx > mTotalWidth - getHorizontalSpace()) {
dx = mTotalWidth - getHorizontalSpace() - mHorizontalOffset;
}
mHorizontalOffset += dx;
//使所有ChildView偏移 如果向左滑动 所有View就向右偏移 反之亦然
offsetChildrenHorizontal(-dx);
//重新进行布局操作
fill(recycler, state);
return dx;
}
通过上面两个方法,可以很容易的发现,其实处理横向或者纵向滑动很简单,上述两个方法,几乎就是模板方法。
不管是横向、或者竖向滑动,这两个方法处理的流程几乎都是一致的,如下:
(1)首先进行边界判断,滑动到边界,就不能继续在滑动了。
(2)然后记录横向或纵向的偏移量,进行offsetChildrenHorizontal()或者offsetChildrenVertical()操作,使子View进行偏移。
(3)最后调用fill(),进行布局操作,完成RecyclerView的滑动处理。
在这里getHorizontalSpace()、和getVerticalSpace()都属于辅助方法 :
//测量RecyclerView的整体横向距离,注意这段距离不包括padding操作,需要减掉
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
//测量RecyclerView的整体纵向距离,注意这段距离不包括padding操作,需要减掉
private int getVerticalSpace() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
而mTotalWidth 和mTotalHeight 则是在onLayoutChildren()提前计算好的,分别代表LayoutManager中自定义的内容宽度,和高度。
到这里,其实自定义LayoutManager的基本流程,已经基本完成。
最后来一张图,总结一下:
友情提示:由于图片幅度过大,需要放大到150%,才能清晰观看。
HexagonRecyclerView的自定义流程
下面的内容,将会着重介绍HexagonRecyclerView这个组件的自定义流程,如果对此感兴趣,不妨来一发Star。
HexagonRecyclerView的自定义流程分为两个阶段:
(1)自定义 LayoutManager,以此来控制正六边形的显示。
(2)自定义正六边形 View,以此来高度定制 RecyclerView 中的展示风格。
PolygonLayoutManager的自定义流程
(1)先设置RecyclerView的子View的LayoutParams参数。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
在这里对子 View 的大小没有特殊要求,所以宽高自适应就可以。
(2)onLayoutChildren()的三个流程的实现
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//准备阶段
initLayout();
//测算阶段
measureLayout();
//布局阶段
fill();
}
第一阶段 : 准备阶段
/**
* onLayoutChildren 准备阶段
* @param recycler
* @param state
*/
private void initLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
//将所有子View先detach一遍,然后标记“Scrap”缓存起来
detachAndScrapAttachedViews(recycler);
}
一般来说,这个阶段,所需要做的操作,几乎都是固定的。所以不需要做太大改动。
第二阶段 :测算阶段
这一阶段比较重要,决定着正六边形的展示形式,可以调整边距,以及展示位置。
/**
* 测量布局 - 阶段
*
* @param recycler
* @param state
*/
private void measureLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
View normalView = recycler.getViewForPosition(0);
measureChildWithMargins(normalView, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(normalView);
int itemHeight = getDecoratedMeasuredHeight(normalView);
//正六边形外圆的半径
int radius = itemWidth / 2;
mVerticalInterval = (mLandscapeInterval / MathUtil.sin(60)) - 2 * (radius - radius * MathUtil.sin(60));
//每组的最大宽度 第一排的宽度加上第二排的宽度
//这里的0.75 * itemWidth 和 (3/2)R表达的意思都是一致的.
int mGroupWidth = (int) (0.75 * itemWidth + itemWidth - mLandscapeInterval);
if (isGravityCenter && mGroupWidth < getHorizontalSpace()) {
mGravityOffset = (getHorizontalSpace() - mGroupWidth) / 2;
} else {
mGravityOffset = 0;
}
for (int i = 0; i < getItemCount(); i++) {
Rect item = mItemFrames.get(i);
int offsetHeight = (int) ((i / GROUP_SIZE) * (itemHeight + mVerticalInterval));
//每组的第一行
if (isItemInFirstLine(i)) {
int left = mGravityOffset;
int top = offsetHeight;
int right = mGravityOffset + itemWidth;
int bottom = itemHeight + offsetHeight;
item.set(left, top, right, bottom);
// Log.d("第一段的高度", "left : " + left);
// Log.d("第一段的高度", "top : " + top);
// Log.d("第一段的高度", "right : " + right);
// Log.d("第一段的高度", "bottom : " + bottom);
} else {
//X轴的偏移是从 正六边形的外圆 3/2 R出开始偏移
int itemOffsetWidth = (int) ((3f / 2f) * radius + mLandscapeInterval);
//Y轴的第一次偏移是 取 (2个正六边形的宽度 + 中间间距) 得到当前第二排正六边形的中点 然后往回减 得到的.
int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
int left = mGravityOffset + itemOffsetWidth;
int top = itemOffsetHeight + offsetHeight;
int right = mGravityOffset + itemOffsetWidth + itemWidth;
int bottom = offsetHeight + itemOffsetHeight + itemHeight;
item.set(left, top, right, bottom);
// Log.d("第二段的高度", "left : " + left);
// Log.d("第二段的高度", "top : " + top);
// Log.d("第二段的高度", "right : " + right);
// Log.d("第二段的高度", "bottom : " + bottom);
}
}
//设置总的宽度
mTotalWidth = Math.max(mGroupWidth, getHorizontalSpace());
//设置总的高度
int totalHeight = (int) (getGroupSize() * itemHeight + (getGroupSize() - 1) * mVerticalInterval);
//判断当前最后一组如果不是第一行,那么高度还得加上第一段偏移量
//Y轴的第一段偏移量
int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
if (!isItemInFirstLine(getItemCount() - 1)) {
totalHeight += itemOffsetHeight;
}
//设置总的高度 取当前的内容 和 RecyclerView的高度的最大值
mTotalHeight = Math.max(totalHeight, getVerticalSpace());
}
在这一阶段,主要做了这几件事:
(1)测量第一个子 View 的宽高,然后获得当前正六边形的外接圆半径。
其实每个正六边形,都是这么画出来的,先做一个辅助的外接圆,然后寻找每一个相交的点就可以。
这里我们能够测量出该控件的宽为 AB,高为 AD,这时候外接圆的半径为 AB的一半。
(2)我们需要算出正六边形竖向之间的距离,请看下图:
在这里先说明两个常量,mLandscapeInterval 和 mVerticalInterval :
mLandscapeInterval:代表的是正六边形形成的正三角形的中线,如图就是那条蓝色的线。
注意:横向间距这里是通过自定义实现的,也就是你自己来设定的。
mVerticalInterval:代表的是竖向间距,即等边三角形的边长 减去 2倍的 外接圆与正六边形的边界差。
我们看自定义的正六边形图,可以发现View的边界不是在正六边形上,而是在外接圆上。所以需要将多余的地方减掉。
mVerticalInterval = (mLandscapeInterval / MathUtil.sin(60)) - 2 * (radius - radius * MathUtil.sin(60));
对比下这两幅图,可以很容易的出这样的公式:
纵向距离 = AB - 2 (R - R sin60°)
(3) 计算每组正六边形的最大宽度,是否小于RecyclerView的宽度,如果小于,则计算出偏移量,使其居中。
int mGroupWidth = (int) (0.75 * itemWidth + itemWidth - mLandscapeInterval);
if (isGravityCenter && mGroupWidth < getHorizontalSpace()) {
mGravityOffset = (getHorizontalSpace() - mGroupWidth) / 2;
} else {
mGravityOffset = 0;
}
这里需要说明的是,我们指的一组是两个正六边形,上图中正六边形A、正六边形C是一组。
然后通过横向间距,我们可以很好的得出当前的每组的宽度,然后比较。
(4)计算每个Item所处的Rect,这里的计算分成两个部分,一个是第一排的Rect,另一个是第二排的Rect,因为横向间距不同,这里做一个分别处理。
isItemInFirstLine是判断当前Item处于第一排还是第二排
第二排的Rect,这里有几个特点:
R:外接圆的半径
(1)left的参数 为(3/2) *R + 横向间距。
(2)top的参数,就是上图的蓝线(形成三角形的中线)的Y轴位置 减去 R/2
(3)(2 * itemHeight+ mVerticalInterval) / 2) 求的就是蓝线的Y轴位置,相当于求一个中点。
(5)最后需要根据item的个数,计算出总的内容长度和高度为计算滑动边界做准备
//设置总的宽度
mTotalWidth = Math.max(mGroupWidth, getHorizontalSpace());
//设置总的高度
int totalHeight = (int) (getGroupSize() * itemHeight + (getGroupSize() - 1) * mVerticalInterval);
//判断当前最后一组如果不是第一行,那么高度还得加上第一段偏移量
//Y轴的第一段偏移量
int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
if (!isItemInFirstLine(getItemCount() - 1)) {
totalHeight += itemOffsetHeight;
}
//设置总的高度 取当前的内容 和 RecyclerView的高度的最大值
mTotalHeight = Math.max(totalHeight, getVerticalSpace());
第三阶段 :布局阶段
这一阶段,最为核心的方法是layoutDecorated()方法,因为LayoutManager是通过这个方法,来给RecyclerView的子View进行布局操作的。
/**
* 第三阶段 - 布局阶段
*
* @param recycler
* @param state
*/
private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
//考虑到当前RecyclerView会处于滑动的状态,所以这里的Rect的作用是展示当前显示的区域
//需要考虑到RecyclerView的滑动量
//mHorizontalOffset 横向的滑动偏移量
//mVerticalOffset 纵向的滑动偏移量
Rect displayRect = new Rect(0, mVerticalOffset,
getHorizontalSpace(),
getVerticalSpace() + mVerticalOffset);
/**
* 对这些View进行测量和布局操作
*/
for (int i = 0; i < getItemCount(); i++) {
Rect frame = mItemFrames.get(i);
if (Rect.intersects(displayRect, frame)) {
View scrap = recycler.getViewForPosition(i);
addView(scrap);
//测量子View
measureChildWithMargins(scrap, 0, 0);
//布局方法
layoutDecorated(scrap, frame.left - mHorizontalOffset, frame.top - mVerticalOffset,
frame.right - mHorizontalOffset, frame.bottom - mVerticalOffset);
}
}
}
这里displayRect 就是滑动过后显示的区域Rect,然后我们通过Rect.intersects()方法,判断当前的Rect,是否与displayRect相交,来判断当前的Item是否显示在当前的窗口上。
如果相交,然后测量View,将子View布局在RecyclerView上。
到这里,onLayoutChildren()已经重写完毕,现在可以运行可以查看,自定义的布局的状况,但是无法滑动,因为我们还没有给LayoutManager设置滑动的效果。
(3)设置RecyclerView的滑动效果
设置滑动效果,就比较固定了,有点类似模板的方法,如下:
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
//上边界判断
if (mVerticalOffset + dy < 0) {
dy = -mVerticalOffset;
//下边界判断
} else if (mVerticalOffset + dy > mTotalHeight - getVerticalSpace()) {
dy = mTotalHeight - getVerticalSpace() - mVerticalOffset;
}
mVerticalOffset += dy;
//使所有ChildView偏移 如果向上滑动 所有View就向下偏移 反之亦然
offsetChildrenVertical(-dy);
//重新布局
fill(recycler, state);
return dy;
}
这里只希望RecyclerView竖向滑动,不希望横向滑动,所以只设置了canScrollVertically()为true。
而scrollVerticallyBy()中无非是三个步骤:
(1) 边界判断
(2) 记录滑动值,使子View偏移
(3) 重新根据滑动后的状态,进行布局
到此,LayoutManager已经自定义完毕了,顺着这个思路下来,可以发现,其实和第一部分介绍自定义LayoutManager一个模式,都是一些通用的流程,然后加上一些辅助计算的常量,就可以实现需要的效果。
其实自定义LayoutManager并没有那么难,了解了这些你也能写出属于你的LayoutManager。
PolygonItemView的自定义流程
大家读完上面,其实对自定义LayoutManager有一定印象了,那么直接给Item设置一张图片背景正常就可以结束了。这样只能说too young了。
点击区域重合
通过上图大家可以发现,这里的点击区域发生了重合,这时候就会存在问题。所以这时候才会选择自定义View。
而且自定义View相比使用图片背景,会有很多优点,也可以高度定制。
1.可以定制正六边形的边框、颜色。
2.可以定制正六边形的大小。
3.可以定制正六边形的内的背景颜色。
PolygonItemView的初始化
private void initData() {
//初始化外边框的画笔
mOuterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOuterPaint.setStyle(Paint.Style.STROKE);
mOuterPaint.setStrokeWidth(mOuterWidth);
mOuterPaint.setColor(mOuterColor);
//初始化内侧的画笔
mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mInnerPaint.setColor(mInnerColor);
//判断点击事件是否在范围内的region
mRegion = new Region();
//绘制正六边形的path
mViewPath = new Path();
}
在初始化的过程中,主要初始化各类的绘制参数,包括画笔,已经绘制路径的path,和判断其是否在点击区域内的Region。
绘制正六边形
/**
* 绘制多边形
*/
public void lineMultShape(int count) {
if (count < POLYGON_COUNT) {
return;
}
mViewPath.reset();
for (int i = 0; i < count; i++) {
//当前角度
int angle = 360 / count * i;
if (i == 0) {
mViewPath.moveTo(mCenterX + mRadius * MathUtil.cos(angle), mCenterY + mRadius * MathUtil.sin(angle));
} else {
mViewPath.lineTo(mCenterX + mRadius * MathUtil.cos(angle), mCenterY + mRadius * MathUtil.sin(angle));
}
}
mViewPath.close();
}
看了这样图,就可以很好的画出正六边形了,将360度分成6分,然后( mCenterX + R_cos α ,mCenterY + R_ sinα)就是正六边形的每个端点的坐标,在用Path依次将其连接就可以了。
判断点击区域
其实自定义View,主要解决的是点击区域重合的问题,这里应该在dispatchTouchEvent()中拦截掉事件,然后判断其点击区域是否在正六边形内,来解决点击重合。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (!isEventInPath(event)) {
return false;
}
}
return super.dispatchTouchEvent(event);
}
/**
* 判断是否在多边形内
*
* @param event
* @return
*/
private boolean isEventInPath(MotionEvent event) {
RectF bounds = new RectF();
//计算Path的边界
mViewPath.computeBounds(bounds, true);
//将边界赋予Region中
mRegion.setPath(mViewPath, new Region((int) bounds.left, (int) bounds.top,
(int) bounds.right, (int) bounds.bottom));
//判断 当前的触摸点是否在这个范围内
return mRegion.contains((int) event.getX(), (int) event.getY());
}
到此,自定义View,也介绍完毕了。相比于自定义LayoutManager,自定义View更好理解一点。在这里如果有想法可以将正六边形换成其他的图形,完成属于你自己的创作。
HexagonRecyclerView后续的一些扩展方向
(1)从PolygonLayoutManager上,后续会提供多列的RecyclerView,通过设置参数来控制列数。
(2)在自定义View上,可能会提供设置图片的功能。
(3)后续可能会提供更多种不同形式的RecyclerView,其实原理都是类似的,更多的是创意。
参考文章
RecyclerView自定义LayoutManager,打造不规则布局
注意:这篇文章有一个BUG,博主可能没看回复,大家如果参考下,注意我在楼下的回复。
http://blog.csdn.net/qibin0506/article/details/52676670#reply