Android 自定义控件 (二) 动态绘制 饼状图

         上面的一篇博客 , 已经介绍了安卓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<ChartPieBean> 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中 , 仅仅绘制可视区域 , 这样更节省内存

    完整代码我已上传至  GitHub,感兴趣的可以参考下 , 有问题请指出 , 谢谢 ...https://github.com/good-good-study/Chart

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@Foritee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值