? ? ? ?? 上面的一篇博客 , 已经介绍了安卓Canvas 绘制的柱状图 , 具体到项目中, 使用起来不要很简单 ; 当然了 , 项目中用到的统计图表远不止柱状图这么简单 , 比如饼图, 相比柱状图而言 ,饼状图样式显得尤为新颖 , 增添了几分趣味性 ,?接下来就动手实现一下动态绘制的饼状图 ,?顺表加了些辅助的功能.
? ? ? ? 废话不多说, 直接上图:
? ? ? ?? 饼状图相对于柱状图 , 稍微复杂一点 , 不过 , 只要掌握了原理, 分分钟搞定它 ; 绘制饼状图 , 首先要确定的是圆心位置 和 半径大小 , 刚开始写的时候 , 只考虑了在手机上绘制的情况 , 绘制的效果不要太好看了 ; 不巧 有一天 , 搞了个平板 , 发现坐标越界了 , 饼图半径太大 , 导致有一部分区域的圆弧没有显示出来 , 后来才想到 , 手机宽度小, 长度大 , ? 平板和TV电视 宽度大 , 长度小 , 所以 ,综合手机和TV电视 , 得出最终的解决方案? :? 半径R的取值 为? R =? width? /2? >? height? / 2? ?? height /2 : width/2? ;? 就是取小值 , 这样的话 , 就能保证饼图在既定的区域中绘制了 .
? ? ? ? 老样子 ,还是在onLayout ( ) 中获取控件的宽高等信息 : ? ? ? ??
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
startX = getPaddingLeft() + basePadding;
endX = getMeasuredWidth() - getPaddingRight() - basePadding;
startY = getMeasuredHeight() - getPaddingBottom() - basePadding;
endY = getPaddingTop() + basePadding;
radius = new float[2];
float R1 = startY - endY;
float R2 = endX - -startX;
radius[0] = startX + R2 / 2;
radius[1] = endY + R1 / 2;
whiteR = R1 > R2 ? R2 / 10 : R1 / 12;//宽度>高度 选择高度 , 宽度
pieR = R1 > R2 ? R2 / 4 : R1 / 4;
areaArc = new RectF(radius[0] - pieR, radius[1] - pieR, radius[0] + pieR, radius[1] + pieR);
}
}
? ? ? ?? 简单分析下需要用到的数据 , 各区域圆弧的画笔 , 颜色不同 , 所以需要计算各区域所占的比重 ,绘制圆弧开始的角度startAngle和扫过的角度sweepAngle ,这两个值需要单独计算 ,并且 标记当前区域的颜色
public ChartPie setData(List data) {
if (data != null) {
float total = getTotal(data);
Paint paint;
float rate;
float[] point;
double arcPI = Math.PI * 2 / 360;//π的值
float startAngle = 0;// 扇形开始的角度
for (int i = 0; i < data.size(); i++) {
ChartPieBean bean = data.get(i);
rate = bean.value / total;//当前对象值所占比例
bean.rate = rate;
bean.startAngle = startAngle;
bean.sweepAngle = rate * 360;//当前对象所占比例 对应的 角度
//计算当前圆弧上的中心点'
if (radius != null) {
point = new float[2];
point[0] = (float) (radius[0] + pieR * Math.cos(arcPI * bean.startAngle + bean.sweepAngle));
point[1] = (float) (radius[1] + pieR * Math.sin(arcPI * bean.startAngle + bean.sweepAngle));
arcPoints.put(i, point);
}
paint = new Paint(basePaint);
paint.setColor(ContextCompat.getColor(getContext(), bean.colorRes));
paintMap.put(i, paint);
pieBeanMap.put(i, bean);
startAngle += bean.sweepAngle;//不要忘记累加
}
}
return this;
}
? ? ? 先不着急画圆弧, 直接绘制饼图, 显得太生硬了 ,我都 还没准备好 ,你就给我显示完了 ?? 如果绘制的时候加上动画 , 让用户看到整个绘制过程 , 岂不是更加有趣味性呢 ! 因为是画的圆弧 , 所以为了让动画更流畅 , 这里的值动画的取值范围是 0 ~ 360 , 这样在动画更新的时候 ,更新UI的进度效果会更好
private void startAnimator() {
if (!isFirst) return;//只能绘制一次
if (starting) {
return;
}
starting = true;
valueAnimator = ValueAnimator.ofFloat(startAnimatorValue, endAnimatorValue).setDuration(duration);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
if (starting) {
invalidate();
}
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
starting = false;
isFirst = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator.start();
}
? ? ?终于到了onDraw了 , 看上面的效果图,中间的白色区域 , 其实我就是画了个圆 , 动画更新的时候不断地去绘制它, 覆盖外面的饼图 , 哈哈 ,暂时也想不到什么好的方案 ...
private void drawCenter(Canvas canvas) {
canvas.drawCircle(radius[0], radius[1], whiteR, centerPiePaint);
}
? ? ?接下来就是最外层饼图的绘制了 , 之前想了好多方案 , 最终确定了下面的方法 ,不要太简单了 ; mAnimatorValue >= startAngle+sweepAngle? 小于当前进度的区域 , 全部绘制出来 , 当mAnimatorValue < startAngle + sweepAngle 时 , 此时的进度在当前区域中, 还未超过当前区域最大值 ,? 所以mAnimatorValue - startAngle 的意思就是 仅仅绘制超出当前区域最小值的部分?
private void drawPie(Canvas canvas) {
for (int i = 0; i < pieBeanMap.size(); i++) {
ChartPieBean bean = pieBeanMap.get(i);
float angle = bean.startAngle + bean.sweepAngle;
if (angle <= mAnimatorValue) {
canvas.drawArc(areaArc, bean.startAngle, bean.sweepAngle, true, paintMap.get(i));
} else {
float sweepAngle = mAnimatorValue - bean.startAngle;
if (sweepAngle >= 0) {
canvas.drawArc(areaArc, bean.startAngle, sweepAngle, true, paintMap.get(i));
}
}
}
}
? ? 不过这里不得不 提一下 , 为什么这里要判断 sweepAngle >= 0? 呢 ? 因为动画的取值范围是 0 ~ 360? ; 所以刚开始的时候mAnimatorValue会小于startAngle ,? 如果继续绘制 ,就是负值了 , 会反方向绘制 , 所以一定要加条件过滤下.
? ? 好了,到这里,饼状图就绘制完成了 , 不过效果图里面还有些辅助线和标记文字 , 也不是很难啦 , 就不详细介绍了 , 值得注意的是 , 标记文字绘制的时候 , 注意canvas.drawText( " ", x ,y ,paint ) ; 这里的 x 跟paint的居中方式有关 , 设置LEFT居左的话 , x值是文字的左边对齐边界值 ,? 设置为CENTER居中显示 , x才是在中心位置 , 还有个点值得注意 , 字体的行高和绘制文字坐标 y 的关系 , 瞬间感觉 android 就是个坑 , 绘制个文字都这么麻烦 ! 没办法 , 我这里是把辅助线当作了绘制文字的baseLine? , 所以上面的文字需要减去行高的1/4 , 下面的文字需要加上行高的 3/4 , 基本就对称显示了 , 这样即使是在55`的小米TV上面 ,也能够保持对称 , 关于paint绘制文字的行高问题 , 这里可以参考下博客
canvas.drawText(String.valueOf(bean.value), this.endX, stopY - height / 4, textPaint);
canvas.drawText(bean.type, this.endX, stopY + height / 4 * 3, textPaint);
? ? 至此 , canvas动态绘制饼状图就完成了 , 总体效果还是蛮不错的 , 代码里面我有将控件添加到Parent的onScrollChangeListener中 , 仅仅绘制可视区域 , 这样更节省内存