Android 进阶自定义View(5)图表统计PieChartView圆饼图的实现

简介: 今天讲图表统计中比较常用的一个,像支付宝的月账单啥的,都是用圆饼图来做数据统计的,先看一下我最终实现的效果图:image.png该效果实际上是两个实心圆叠加后的效果。

今天讲图表统计中比较常用的一个,像支付宝的月账单啥的,都是用圆饼图来做数据统计的,先看一下我最终实现的效果图:


img_8f754f5a153c8522f1cd39807ed2507d.png
image.png

该效果实际上是两个实心圆叠加后的效果。


img_7dee33498c6a5e201b5ea1826bbc88e2.png
image.png
img_c681ec9c84b2ba4e491f3ee3c8fdce1c.png
image.png
《一》View实现思路分析:

(1)根据占比集合数据,计算所需绘制的角度,动态设置画笔颜色,drawArc()绘制外圆弧
(2)drawCircle()绘制内圆
(3)确定每块圆饼的小白点的位置
(4)绘制白点的沿线和占比文字

《二》具体实现:

(1)绘制不同颜色的圆饼

   for (int i = 0; i < mRateList.size(); i++) {
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mColorList.get(i));
//            Log.e("TAG", "startAngle=" + startAngle + "--sweepAngle=" + ((int) (mRateList.get(i) * (360)) - offset));
            canvas.drawArc(rectF, startAngle, (int) (mRateList.get(i) * (360)) , true, mPaint);
            startAngle = startAngle + (int) (mRateList.get(i) * 360);
   }

(2)绘制内圆

  mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_081638));
  canvas.drawCircle(radius + centerPointRadius + (xOffset + yOffset + textRect.width()), radius + centerPointRadius + (xOffset + yOffset + textRect.height()), radius / 1.5f, mPaint);

(3)确定每块圆饼的小白点的位置,通过每段圆饼的起始角度确定该段圆弧的中心点位置。

 private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
        Path path = new Path();
        //通过Path类画一个90度(180—270)的内切圆弧路径
        path.addArc(rectF, startAngle, endAngle);

        PathMeasure measure = new PathMeasure(path, false);
//        Log.e("路径的测量长度:", "" + measure.getLength());

        float[] coords = new float[]{0f, 0f};
        //利用PathMeasure分别测量出各个点的坐标值coords
        int divisor = 1;
        measure.getPosTan(measure.getLength() / divisor, coords, null);
//        Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]);
        float x = coords[0];
        float y = coords[1];
        Point point = new Point(Math.round(x), Math.round(y));
        pointList.add(point);
    }

(4)绘制以白点为起点的折线和占比文字。有个细节需要注意一下,绘制折线和比例文字时,每部分沿线和文字的绘制规则不一样,我是按下面的规则处理的:将圆分为四部分,每块区分显示。


img_8c7ddb8ca078f1b3dc40e73d1e839ed9.png
image.png
  //折线横向长度
    private int xOffset;
    //折线偏Y方向长度
    private int yOffset;
    private void dealRateText(Canvas canvas, Point point, int position, List<Point> pointList) {
        if (position == 0) {
            lastPoint = pointList.get(0);
        } else {
            lastPoint = pointList.get(position - 1);
        }
        float[] floats = new float[8];
        floats[0] = point.x;
        floats[1] = point.y;
        //右半圆
        if (point.x >= radius + centerPointRadius + (xOffset + yOffset + textRect.width())) {
            mPaint.setTextAlign(Paint.Align.LEFT);
            floats[6] = point.x + xOffset;      
            if (point.y <= radius + centerPointRadius + (xOffset + yOffset + textRect.height())) {
                //右上角
                floats[2] = point.x + yOffset;
                floats[3] = point.y - yOffset;
                floats[4] = point.x + yOffset;
                floats[5] = point.y - yOffset;
                floats[7] = point.y - yOffset;
            } else {
                //右下角
                floats[2] = point.x + yOffset;
                floats[3] = point.y + yOffset;
                floats[4] = point.x + yOffset;
                floats[5] = point.y + yOffset;
                floats[7] = point.y + yOffset;
            }
            //左半圆
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);
            floats[6] = point.x - xOffset;
            //防止相邻的圆饼绘制的文字重叠显示
            if (point.y <= radius + centerPointRadius) {
                //左上角
                floats[2] = point.x - yOffset;
                floats[3] = point.y - yOffset;
                floats[4] = point.x - yOffset;
                floats[5] = point.y - yOffset;
                floats[7] = point.y - yOffset;
            } else {
                //左下角
                floats[2] = point.x - yOffset;
                floats[3] = point.y + yOffset;
                floats[4] = point.x - yOffset;
                floats[5] = point.y + yOffset;
                floats[7] = point.y + yOffset;
            }
        }
        //根据每块的颜色,绘制对应颜色的折线
//        mPaint.setColor(mRes.getColor(colorList.get(position)));
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_b69b4f));
        //画圆饼图每块边上的折线
        canvas.drawLines(floats, mPaint);
        mPaint.setStyle(Paint.Style.STROKE);
        //绘制显示的文字,需要根据类型显示不同的文字
        if (mRateList.size() > 0) {
            //Y轴:+ textRect.height() / 2 ,相对沿线居中显示
            canvas.drawText(getFormatPercentRate(mRateList.get(position) * 100) + "%", floats[6], floats[7] + textRect.height() / 2, mPaint);
        }
    }

完整代码:

package com.example.jojo.learn.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.View;

import com.example.jojo.learn.R;
import com.example.jojo.learn.utils.DP2PX;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by JoJo on 2018/8/6.
 * wechat:18510829974
 * description: 饼状图
 */

public class PieView extends View {
    private Context mContext;
    private Paint mPaint;
    //每块占比的绘制的颜色
    private List<Integer> mColorList = new ArrayList<>();
    //圆弧占比的集合
    private List<Float> mRateList = new ArrayList<>();
    //是否展示文字
    private boolean isShowRateText;
    //圆弧半径
    private float radius;
    private int startAngle = 0;
    //不同色块之间是否需要空隙offset
    private int offset = 0;
    //圆弧中心点小圆点的圆心半径
    private int centerPointRadius;
    private float showRateSize;
    private Rect textRect;
    //折线横向长度
    private int xOffset;
    //折线偏Y方向长度
    private int yOffset;
    private float mChangeAngle;
    private boolean isAnimation;
    private int sign = 0;

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;

        init();

        initData();
    }

    private void initData() {

        float[] rate = {30f, 40f, 15f, 15f};
        int[] colors = {Color.RED, Color.BLUE, Color.YELLOW, Color.GRAY};
        for (int i = 0; i < rate.length; i++) {
            mRateList.add(rate[i] / 100);
            mColorList.add(colors[i]);
        }

        textRect = new Rect();
        if (mRateList.size() > 0) {
            mPaint.getTextBounds((mRateList.get(0) + "%"), 0, (mRateList.get(0) + "%").length(), textRect);
        }
    }

    private void init() {
        radius = DP2PX.dip2px(mContext, 80);
        centerPointRadius = DP2PX.dip2px(mContext, 2);
        xOffset = DP2PX.dip2px(mContext, 20);
        yOffset = DP2PX.dip2px(mContext, 5);
        showRateSize = DP2PX.dip2px(mContext, 10);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(showRateSize);

        if (mRateList.size() > 0) {
            textRect = new Rect();
            mPaint.getTextBounds((mRateList.get(0) + "%"), 0, (mRateList.get(0) + "%").length(), textRect);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            //边沿线和文字所占的长度:(xOffset + yOffset + textRect.width())
            heightSize = (int) (radius * 2) + 2 * centerPointRadius + getPaddingLeft() + getPaddingRight() + (xOffset + yOffset + textRect.height()) * 2;
        }
        if (widthMode == MeasureSpec.AT_MOST) {

            widthSize = (int) (radius * 2) + 2 * centerPointRadius + getPaddingLeft() + getPaddingRight() + (xOffset + yOffset + textRect.width()) * 2;
        }
        //保存测量结果
        setMeasuredDimension(widthSize, heightSize);
    }

    private int paintPosition;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //(1)绘制圆饼
        RectF rectF = new RectF(0 + centerPointRadius + (xOffset + yOffset + textRect.width()), 0 + centerPointRadius + (xOffset + yOffset + textRect.height()), 2 * radius + centerPointRadius + (xOffset + yOffset + textRect.width()), 2 * radius + centerPointRadius + (xOffset + yOffset + textRect.height()));
        List<Point> mPointList = new ArrayList<>();

        for (int i = 0; i < mRateList.size(); i++) {
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mColorList.get(i));
//            Log.e("TAG", "startAngle=" + startAngle + "--sweepAngle=" + ((int) (mRateList.get(i) * (360)) - offset));
            canvas.drawArc(rectF, startAngle, (int) (mRateList.get(i) * (360)) - offset, true, mPaint);

            //(2)处理每块圆饼弧的中心点,绘制折线,显示对应的文字
            if (isShowRateText) {
                dealPoint(rectF, startAngle, (mRateList.get(i) * 360 - offset) / 2, mPointList);
                Point point = mPointList.get(i);
                mPaint.setColor(Color.WHITE);//点的绘制的颜色
                canvas.drawCircle(point.x, point.y, centerPointRadius, mPaint);
                dealRateText(canvas, point, i, mPointList);
            }
            startAngle = startAngle + (int) (mRateList.get(i) * 360);
        }
        //(3)绘制内部中空的圆
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_081638));
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(radius + centerPointRadius + (xOffset + yOffset + textRect.width()), radius + centerPointRadius + (xOffset + yOffset + textRect.height()), radius / 1.5f, mPaint);

    }

    private Point lastPoint;

    private void dealRateText(Canvas canvas, Point point, int position, List<Point> pointList) {
        if (position == 0) {
            lastPoint = pointList.get(0);
        } else {
            lastPoint = pointList.get(position - 1);
        }
        float[] floats = new float[8];
        floats[0] = point.x;
        floats[1] = point.y;
        //右半圆
        if (point.x >= radius + centerPointRadius + (xOffset + yOffset + textRect.width())) {
            mPaint.setTextAlign(Paint.Align.LEFT);
            floats[6] = point.x + xOffset;
            //防止相邻的圆饼绘制的文字重叠显示
//            if (lastPoint != null) {
//                int absX = Math.abs(point.x - lastPoint.x);
//                int absY = Math.abs(point.y - lastPoint.y);
//                if (absX > 0 && absX < 20 && absY > 0 && absY < 20) {
//                    floats[6] = point.x + xOffset - textRect.width() / 2;
//                    Log.e("TAG", "右半圆");
//                } else {
//                    floats[6] = point.x + xOffset;
//                }
//            } else {
//                floats[6] = point.x + xOffset;
//            }
            if (point.y <= radius + centerPointRadius + (xOffset + yOffset + textRect.height())) {
                //右上角
                floats[2] = point.x + yOffset;
                floats[3] = point.y - yOffset;
                floats[4] = point.x + yOffset;
                floats[5] = point.y - yOffset;
                floats[7] = point.y - yOffset;
            } else {
                //右下角
                floats[2] = point.x + yOffset;
                floats[3] = point.y + yOffset;
                floats[4] = point.x + yOffset;
                floats[5] = point.y + yOffset;
                floats[7] = point.y + yOffset;
            }
            //左半圆
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);
            floats[6] = point.x - xOffset;
            //防止相邻的圆饼绘制的文字重叠显示
//            if (lastPoint != null) {
//                int absX = Math.abs(point.x - lastPoint.x);
//                int absY = Math.abs(point.y - lastPoint.y);
//                if (absX > 0 && absX < 20 && absY > 0 && absY < 20) {
//                    floats[6] = point.x - xOffset - textRect.width() / 2;
//                    Log.e("TAG", "左半圆");
//                } else {
//                    floats[6] = point.x - xOffset;
//                }
//            } else {
//                floats[6] = point.x - xOffset;
//            }
            if (point.y <= radius + centerPointRadius) {
                //左上角
                floats[2] = point.x - yOffset;
                floats[3] = point.y - yOffset;
                floats[4] = point.x - yOffset;
                floats[5] = point.y - yOffset;
                floats[7] = point.y - yOffset;
            } else {
                //左下角
                floats[2] = point.x - yOffset;
                floats[3] = point.y + yOffset;
                floats[4] = point.x - yOffset;
                floats[5] = point.y + yOffset;
                floats[7] = point.y + yOffset;
            }
        }
        //根据每块的颜色,绘制对应颜色的折线
//        mPaint.setColor(mRes.getColor(colorList.get(position)));
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_b69b4f));
        //画圆饼图每块边上的折线
        canvas.drawLines(floats, mPaint);
        mPaint.setStyle(Paint.Style.STROKE);
        //绘制显示的文字,需要根据类型显示不同的文字
        if (mRateList.size() > 0) {
            //Y轴:+ textRect.height() / 2 ,相对沿线居中显示
            canvas.drawText(getFormatPercentRate(mRateList.get(position) * 100) + "%", floats[6], floats[7] + textRect.height() / 2, mPaint);
        }
    }

    private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
        Path path = new Path();
        //通过Path类画一个90度(180—270)的内切圆弧路径
        path.addArc(rectF, startAngle, endAngle);

        PathMeasure measure = new PathMeasure(path, false);
//        Log.e("路径的测量长度:", "" + measure.getLength());

        float[] coords = new float[]{0f, 0f};
        //利用PathMeasure分别测量出各个点的坐标值coords
        int divisor = 1;
        measure.getPosTan(measure.getLength() / divisor, coords, null);
//        Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]);
        float x = coords[0];
        float y = coords[1];
        Point point = new Point(Math.round(x), Math.round(y));
        pointList.add(point);
    }

    public void updateDate(List<Float> rateList, List<Integer> colorList, boolean isShowRateText) {
        this.isShowRateText = isShowRateText;
        this.mRateList = rateList;
        this.mColorList = colorList;
        init();
        invalidate();
    }

    /**
     * 获取格式化的保留两位数的数
     */
    public String getFormatPercentRate(float dataValue) {
        DecimalFormat decimalFormat = new DecimalFormat(".00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
        return decimalFormat.format(dataValue);
    }


}
相关文章
|
4天前
|
API Android开发 开发者
Android经典实战之使用ViewCompat来处理View兼容性问题
本文介绍Android中的`ViewCompat`工具类,它是AndroidX库核心部分的重要兼容性组件,确保在不同Android版本间处理视图的一致性。文章列举了设置透明度、旋转、缩放、平移等功能,并提供了背景色、动画及用户交互等实用示例。通过`ViewCompat`,开发者可轻松实现跨版本视图操作,增强应用兼容性。
24 5
|
19天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详细了,这一篇是针对View的绘制,View的绘制如果你有所了解,基本分为measure、layout、draw 过程,其中比较难理解就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,文章也比较长,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,看完这篇文章,相信你会有所收获的。
40 2
|
1月前
|
Android开发
Android面试题之自定义View注意事项
在Android开发中,自定义View主要分为四类:直接继承View重写onDraw,继承ViewGroup创建布局,扩展特定View如TextView,以及继承特定ViewGroup。实现时需注意:支持wrap_content通过onMeasure处理,支持padding需在onDraw或onMeasure/onLayout中处理。避免在View中使用Handler,使用post系列方法代替。记得在onDetachedFromWindow时停止线程和动画以防止内存泄漏。处理滑动嵌套时解决滑动冲突,并避免在onDraw中大量创建临时对象。
24 4
|
1月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
27 1
|
20天前
|
机器学习/深度学习 人工智能 算法
探索AI在医疗影像分析中的应用探索安卓开发中的自定义View组件
【7月更文挑战第31天】随着人工智能技术的飞速发展,其在医疗健康领域的应用日益广泛。本文将聚焦于AI技术在医疗影像分析中的运用,探讨其如何通过深度学习模型提高诊断的准确性和效率。我们将介绍一些关键的深度学习算法,并通过实际代码示例展示这些算法是如何应用于医学影像的处理和分析中。文章旨在为读者提供对AI在医疗领域应用的深刻理解和实用知识。
22 0
|
1月前
|
前端开发 API Android开发
Android自定义View之Canvas一文搞定
这篇文章介绍了Android自定义View中如何使用Canvas和Paint来绘制图形。Canvas可理解为画布,用于绘制各种形状如文字、点、线、矩形、圆角矩形、圆和弧。常见API包括`drawText()`、`drawPoint()`、`drawLine()`、`drawRect()`等。文章还提到了Canvas的保存、恢复、平移和旋转方法,通过绘制钟表盘的例子展示了如何实际应用。总结关键点:Canvas与Paint结合用于图像绘制,掌握Canvas的基本绘图函数及坐标变换操作是自定义View的关键。
23 0
Android自定义View之Canvas一文搞定
|
27天前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
27 0
|
Web App开发 Android开发
[Android]使用Dagger 2依赖注入 - 图表创建的性能(翻译)
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5098943.html 使用Dagger 2依赖注入 - 图表创建的性能 原文:http://frogermcs.github.io/dagger-graph-creation-performance/ #PerfMatters - 最近非常流行标签,尤其在Android世界中。
865 0
|
2天前
|
JavaScript 前端开发 Java
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
IT寒冬使APP开发门槛提升,安卓程序员需转型。选项包括:深化Android开发,跟进Google新技术如Kotlin、Jetpack、Flutter及Compose;研究Android底层框架,掌握AOSP;转型Java后端开发,学习Spring Boot等框架;拓展大前端技能,掌握JavaScript、Node.js、Vue.js及特定框架如微信小程序、HarmonyOS;或转向C/C++底层开发,通过音视频项目如FFmpeg积累经验。每条路径都有相应的书籍和技术栈推荐,助你顺利过渡。
13 3
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
|
1天前
|
搜索推荐 Android开发 iOS开发
探索安卓与iOS开发的差异性与互补性
【8月更文挑战第19天】在移动应用开发的广阔天地中,安卓与iOS两大平台各据一方,引领着行业的潮流。本文将深入探讨这两个平台在开发过程中的不同之处以及它们之间的互补关系,旨在为开发者提供一个全面的视角,帮助他们更好地把握市场动态,优化开发策略。通过分析各自的开发环境、编程语言、用户界面设计、性能考量及市场分布等方面,我们将揭示安卓与iOS开发的独特魅力和挑战,同时指出如何在这两者之间找到平衡点,实现跨平台的成功。