前言
前阵子帮团队招人,我很喜欢问候选人这个问题。
点击区域是不规则的,该如何让手势事件在不规则区域内做出响应?
如果候选人的答案是通过数学计算的方式来确定落点。
我会微微一笑的反问他,如果是五角星怎么办?
本文主要会介绍以下内容
1、绘制饼图的几种方式
2、不规则区域的点击
3、文字居中公式
先看一下最终实现的效果,代码地址在文末
如何绘制饼图?
基础知识
Path
类型 | API | 描述 |
---|---|---|
添加弧形 | addArc | 添加弧形 |
逻辑运算 | op | A\B(DIFFERENCE), A∩B(INTERSECT), B\A(REVERSE_DIFFERENCE), A∪B(UNION), A⊕B(XOR) |
计算边界 | computeBounds | 计算路径的边界 |
因为之后会用到逻辑运算,先来看一下逻辑运算的API,minSdkVersion必须大于等于19
op
Added in API level 19
boolean op (Path path1, Path path2, Path.Op op)
Set this path to the result of applying the Op to the two specified paths. The resulting path will be constructed from non-overlapping contours. The curve order is reduced where possible so that cubics may be turned into quadratics, and quadratics maybe turned into lines.
参数1,参数2是需要计算的两个路径。参数3是运算种类。两个path经过逻辑运算后得到新的路径会存放在调用这个api的路径当中。
逻辑运算包含五种类型
描述 | 示意图 | |
---|---|---|
Path.Op. DIFFERENCE | Subtract the second path from the first path. 从第一路径中减去第二个路径。 | |
Path.Op. REVERSE_DIFFERENCE | Subtract the first path from the second path. 从第二个路径中减去第一个路径。 | |
Path.Op. INTERSECT | Intersect the two paths. 两个路径的交集。 | |
Path.Op. UNION | Union (inclusive-or) the two paths. 两个路径的合集。 | |
Path.Op. XOR | Exclusive-or the two paths. 两个路径的异或。即两个路径的合集减去两个路径的交集。 |
Canvas
类型 | API | 描述 |
---|---|---|
绘制弧形 | drawArc | 绘制弧形 |
绘制路径 | drawPath | 绘制路径,路径样式取决于paint |
饼图绘制
结合上面的API绘制饼图的方式有很多,我先后尝试了以下3种方式
//方法1 调用canvas.drawArc方法绘制不闭合的弧形
canvas.drawArc(pieInRectF, startAngle, sweepAngle, false, piePaint);
//方法2 添加弧形路径,调用canvas.drawPath绘制
Path path = new Path();
path.addArc(pieInRectF, startAngle, sweepAngle);
canvas.drawPath(path, piePaint);
//以上两种绘制弧形的思路均是绘制不闭合的弧形路径,通过设置路径宽度最终达到环形效果
//方法3
Path path1 = new Path();
path1.moveTo(in.centerX(), in.centerY());
path1.arcTo(in, startAngle, angle);
Path path2 = new Path();
path2.moveTo(out.centerX(), out.centerY());
path2.arcTo(out, startAngle, angle);
Path path = new Path();
path.op(path2, path1, Path.Op.DIFFERENCE);
//从path2中减去path1获取新的路径
画了一个示意图,简单感受一下...
Path2闭合后是个扇形,Path1是个较小的扇形,Path2 - Path1后就能得到一块饼图所需的区域了。
那么问题来了,已经有两种简单易懂绘制饼图的方法,为什么要舍近求远搞个这么复杂的?
如果你只需要完成饼图绘制,前两种方法确实够用,但如果想实现饼图不同区域点击效果,推荐使用第三种方式。
如何实现不规则区域的点击?
关于不规则区域的点击实现,先提供一些其他思路
1、像素点判断
缺点:容易OOM、图形不能动态修改
png图都是矩形的,无效部分都设置为透明色,把图片作为控件的背景图片。点击时判断点击点的像素是否为透明,不透明则的控件响应点击事件。具体实现方法请见《Android不规则点击区域详解》。
2、数学逻辑判断
在研究不规则区域点击的时候,看到了这种方案。
采用数学公式判断点击坐标是否在圆环上,再通过角度判断落在具体哪一块上。但由于考虑到数学计算方式的复杂(其实是我懒...),最终我没有尝试这种方式。感兴趣的同学具体实现方法请见《Android自定义饼状图,且能区分点击的区域》和《PieChar扇形图实现》。
3、通过Region判断坐标落点
由于Path是由多个坐标点构成的,将path转化成region,可以通过region.contains(int x,int y)来判断点击坐标是否在该区域。该方法不仅适用于饼图,其他更加复杂图形同样适用,当然前提是你能构建出绘制这个图形所需要的路径。
public void setRegion(Path path) {
Region re = new Region();
RectF rectF = new RectF();
path.computeBounds(rectF, true);
re.setPath(path, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
this.region = re;
}
上文中已经通过Path的逻辑运算获取到饼图块的path,通过这个path再生成region。
判断是否在这个区域也非常简单
public boolean isInRegion(float x, float y) {
return region != null && region.contains((int)x, (int)y);
}
有人可能会有疑问,调用path.addArc方法,也是通过path绘制图案,这种方式行不行呢?事实是检验真理的唯一标准。亲测使用第二种方案获得的path去生成region获得的范围区域完全错误。详见下图。
每个饼图块都对应一个单独的region,黑色的点是我手指点击的时候绘制的落点,即region.contains((int)x, (int)y)返回true。可以看到判断点位是否在区域内的位置完全错了。
文字居中的公式
在此记录一下文字居中公式,具体推导过程网上很多博客讲解了,就不展开了。
Paint.FontMetrics fm = mTxtPaint.getFontMetrics();
float y = centerY - fm.top / 2 - fm.bottom / 2;
本文源码
感兴趣的朋友可以点击本文源码查看。