项目中经常用圆形统计图来显示收益占比,消费占比等数据,像支付宝账单那样,感觉挺有意思,那么动手来自己撸一个,先来看一下最终效果图:
这个东西,想一下思路挺清晰的,就是算一下各个数据的比例,然后根据比例来分一个圆就行了,下面先定义一个数据类:
public class PieData {
//收益
private float income;
//颜色
private int color;
//用于色块点击判断
private boolean isOut = false;
...(get和set方法)
}
上面这个类很简单,不多做解释了,接下来就是根据比例,画圆弧了,这边就是根据传入的List,通过income来计算各自的比例,下面是一些画圆弧之前的初始工作:
//传入数据
public void setPieDataList(List pieDataList) {
total = 0;
regions.clear();
this.pieDataList = pieDataList;
for(PieData data:pieDataList){
total += data.getIncome(); //计算总的income,根据这个算比例
Region region = new Region(); //根据数据的个数创建region,后面用来处理点击事件
regions.add(region);
}
invalidate();
}
//保证view为正方形,为了画圆方便
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int minSize = Math.min(width, height);
setMeasuredDimension(minSize, minSize);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
cx = w/2; //圆心横坐标
cy = h/2; //圆心纵坐标
rect1 = new RectF(19, 19, w-19, h-19);
rect2 = new RectF(20, 20, w-20, h-20); //实际圆形图的矩形区域
mRegion = new Region(20, 20, w-20, h-20); //整个画布的区域
radius = (w - 40)/2; //实际圆形图半径
radiusInside = radius/2; //里面空心圆的默认半径
}
上面都是一些属性的初始化,其中rect1是后面动画要用到的矩形区域,接下里就是画圆弧了,画圆弧有两种方式,Canvas.DrawArc()和Path.addArc(),这里是用了path 的方式来画圆弧,这样做是为了和region配合使用,便于touch事件坐标的判断:
protected void onDraw(Canvas canvas) {
if(pieDataList != null && pieDataList.size() > 0){
float startAngle = 0; //初始角度
for(int i=0;iget(i);
float swapAngle;
//计算扇形的角度
if(i == pieDataList.size() - 1){
swapAngle = 360 - startAngle;
}else{
swapAngle = (pieData.getIncome()/total) * 360;
}
mPaint.setColor(pieData.getColor());
//计算出扇形的路径
arcPath.moveTo(cx, cy);
arcPath.lineTo(cx + (float) Math.sin(Math.toRadians(startAngle)) * radius, cy - (float) Math.cos(Math.toRadians(startAngle)) * radius);
arcPath.addArc(rect2, startAngle - 90, swapAngle);
arcPath.lineTo(cx, cy);
canvas.drawPath(arcPath, mPaint);
//获取每个扇形的区域region
regions.get(i).setPath(arcPath, mRegion);
startAngle += swapAngle;
arcPath.reset();
}
}
}
上面基本画出了一个圆形比例图了,效果如如下:
代码里面在addArc的时候,初始角度 startAngle - 90是为了从12点方向开始画扇形,如果startAngle从0开始,则这个扇形是从3点方向开始画的。在计算整个扇形路径的时候,用了三角函数去求圆上点的坐标,这点很简单,但是要注意的是java中求正弦和余弦的方法要穿进去的参数都是弧度,不是角度,就因为这个错误,开始怎么算都不对,画出来的扇形角度怎么都不对,一度怀疑哥中学是不是学了假的三角函数。。。。
接着来实现整个饼图慢慢旋转展示的动画,这边我用的一种淫荡的方式来做的,就是在饼图的上面盖一个实心圆,就把它当着扇形,不断减小圆心角知道消失,这样就很简单了,直接上代码:
public void animStart(){
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "swapAngle", 0, 360);
animator.setDuration(2000);
animator.start();
}
这边也是直接用的属性动画,来改变角度,然后在onDraw中直接一代码搞定:
canvas.drawArc(rect1, swapAngle - 90, 360 - swapAngle, true, animPaint);
下面的效果就是动画的效果:
嘿嘿,是不是给人的感觉就是一点一点画出来的,而不是上面的圆在一点一点消失~~
接下就是给饼图加入点击事件,每快扇形被点击的时候,就是像被切掉了一样,像后面弹了出去,其实就是将整个扇形平移了一下。要实现这个有两点要做:
①要判断手触摸的坐标在哪个扇形区域里面,这个时候前面拿到的每个扇形的region就派上用场了,完美解决这个问题;
②扇形的平移也就是圆心的平移,从开始那张图上还有一点可以看到扇形平移是与两条直边平行的移动的,也就是沿着圆心角的角平分线移动,也就是说圆心沿着角平分线移动就行了。
ok知道怎么做了直接上代码:
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
for (int i=0;iget(i);
//判断坐标在哪个扇形区域内
if(region.contains((int)event.getX(), (int)event.getY())){
if(clickRegion == i){
pieDataList.get(i).setOut(false);
clickRegion = -1;
invalidate();
return true;
}
if(clickRegion != -1){
pieDataList.get(clickRegion).setOut(false);
}
pieDataList.get(i).setOut(true);
clickRegion = i;
invalidate();
return true;
}
}
}
return super.onTouchEvent(event);
}
在touch方法中,主要就是判断点在哪个区域,然后去改变PieData中的isOut这个属性,下面就是OnDraw中画圆弧改动后的代码:
if(pieData.isOut()){
//角平分线的角度
float middleAngle = startAngle + swapAngle/2;
//根据角平分线的角度计算出平移后的圆心的坐标点(后面20是我设置的默认的平移距离)
float newCX = cx + (float) Math.sin(Math.toRadians(middleAngle)) * 20;
float newCY = cy - (float) Math.cos(Math.toRadians(middleAngle)) * 20;
//平移之后新的圆弧的矩形区域
rect2New.left = newCX - radius;
rect2New.top = newCY - radius;
rect2New.right = newCX + radius;
rect2New.bottom = newCY + radius;
//根据新的圆点和矩形区域画扇形
arcPath.moveTo(newCX, newCY);
arcPath.lineTo(newCX + (float) Math.sin(Math.toRadians(startAngle)) * radius, newCY - (float) Math.cos(Math.toRadians(startAngle)) * radius);
arcPath.addArc(rect2New, startAngle - 90,swapAngle);
arcPath.lineTo(newCX, newCY);
canvas.drawPath(arcPath, mPaint);
}else{
arcPath.moveTo(cx, cy);
arcPath.lineTo(cx + (float) Math.sin(Math.toRadians(startAngle)) * radius, cy - (float) Math.cos(Math.toRadians(startAngle)) * radius);
arcPath.addArc(rect2, startAngle - 90, swapAngle);
arcPath.lineTo(cx, cy);
canvas.drawPath(arcPath, mPaint);
}
上面的代码中,在扇形之前做了判断,是不是被点击的扇形区域,如果是被点击的区域,根据角平分线的角度和平移距离,计算出新的圆心坐标和画圆弧需要的矩形区域,然后画扇形就行了;如果不是被点击区域,则保持之前的位置不变就行了,效果图如下:
最后就剩下空心的效果了,这个就简单了,在中间直接画个圆就行了嘛,ok代码很简单:
if(!isSolid){
canvas.drawCircle(cx, cy, radiusInside, centerPaint);
}
在onDraw中加入上面代码,isSolid判断是否空心,radiusInside空心圆的半径,画出来之后效果如下:
看起来没啥问题了,仔细一点的同学会发现,空心的时候点击圆弧很奇怪,这是因为点击的时候我们只平移了圆扇形区域,并没有移动空心的扇形区域~~这边也跟上面画移动后的扇形区域一样,用同样的方式画一个空白的的扇形就ok了,只需要在onDraw的画扇形的地方加入如下代码就行了:
if(!isSolid){
rect3.left = newCX - radiusInside;
rect3.top = newCY - radiusInside;
rect3.right = newCX + radiusInside;
rect3.bottom = newCY + radiusInside;
canvas.drawArc(rect3, startAngle - 90, swapAngle, true, wPaint);
}
代码很简单,先判断是不是空心圆,如果是就根据该扇形移动后的圆心的坐标和空心圆的半径来画出移动后的空白圆弧,效果图如下:
这下空心圆点击看起来就没那么奇怪了,和上一张图点击的时候有明显的区别,真的开起来就跟从一个圆环上砍掉一段一样有木有~~到这基本就是实现最开始的图上的所有效果。