Android自定义view ---- 饼状图

前言

最近项目中遇到一个需求,需要一个饼状图,显示百分比,点击每一个扇形区域可以切换下面列表的数据。拿到这个需求后首先想到了MPAndroidChart等第三方库,这个库中包含了各种各样的图表,冷静下来一想,整个项目中就这一个地方用到,那么引入这个库必然会增大项目的体积。所以呢,还是自己搞一个算了。、

效果图

先看一下 最终的效果图:

最终效果.png
设计思路

看了效果图,是不是感觉还不错。 其实实现起来还是挺简单的,先来理清楚思路,思路理清楚了,相信你也可以的。

1.创建类集成View,实现onDraw。这里类名为MyPieChart。
2.将每个扇形封装成一个类,也可以说是对象,这里命名为PieEntry。其中包含三个元素:数值、颜色、是否被选中、起始角度、结束角度(用于点击事件)。
3.创建一个init方法来初始化Paint。并在构造方法中调用init方法
4.在onDraw方法中画图。也就是逐个的画扇形。
  • 计算总值,也就是遍历List<PieEntry>,将每个PieEntry的数值相加。
  • 获取中心点坐标和两个半径,一个是被选中的半径稍微大一点,另一个是未选中的坐标稍微小一点。
  • 再次遍历List<PieEntry> 画出每个扇形,在这个循环体内,我们需要计算出当前扇形的角度,然后累加的起始角度,作为下一个扇形的起始角度。并在这个循环体内画出扇形。然后画出外围显示的百分比。
5.重写onTouchEvent方法,拦截ACTION_DOWN状态。得到点击的坐标,判断该点是否小于半径,如果大于半径则不处理,如果小于半径则计算该点和圆心的连线与x正方形的夹角,最后再遍历List<PieEntry>判断该夹角在那个扇形区域中,由此将点击时间回掉出去。

大题思路就是这样,文字描述可能还描述的不够明白,下面看代码怎么一步一步实现。

具体代码实现

1.创建MyPieChart类和PieEntry类。
public class MyPieChart extends View {

    private List<PieEntry> pieEntries;
    private Paint paint; //画笔
    private float centerX;   //中心点 x坐标
    private float centerY;  //中心点 y坐标
    private float radius;    //未选中状态的半径
    private float sRadius; //选中状态的半径

    /**
     * 每个扇形的对象
     */
    public static class PieEntry {
        private float number;  //数值
        private int colorRes;  //颜色资源
        private boolean selected; //是否选中
        private float startC;     //对应扇形起始角度
        private float endC;       //对应扇形结束角度

        public PieEntry(int number, int colorRes, boolean selected) {
            this.number = number;
            this.colorRes = colorRes;
            this.selected = selected;
        }

        public float getStartC() {
            return startC;
        }

        public void setStartC(float startC) {
            this.startC = startC;
        }

        public float getEndC() {
            return endC;
        }

        public void setEndC(float endC) {
            this.endC = endC;
        }

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean selected) {
            this.selected = selected;
        }

        public float getNumber() {
            return number;
        }

        public void setNumber(float number) {
            this.number = number;
        }

        public int getColorRes() {
            return colorRes;
        }

        public void setColorRes(int colorRes) {
            this.colorRes = colorRes;
        }
    }
}
2. onDraw方法的具体实现。

这里是onDraw方法的具体实现,代码中有详细的注释。其中涉及到一些三角函数知识,如果看不明白的话,可以参考下面的图示。


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //计算总值
        int total = 0;
        for (int i = 0; i < pieEntries.size(); i++) {
            total += pieEntries.get(i).getNumber();
        }
        //刷新中心点 和半径
        centerX = getPivotX();
        centerY = getPivotY();
        if (sRadius == 0) {   //这里做个判断,如果没有通过setRadius方法设置半径,则半径为真个view最小边的一半
            sRadius = (getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2);
        }
        //计算出两个状态的半径,这里二者相差5dp.
        radius = sRadius - DensityUtils.dp2px(getContext(), 5);

        //其实角度设置为0,即x轴正方形
        float startC = 0;
        //遍历List<PieEntry> 开始画扇形
        for (int i = 0; i < pieEntries.size(); i++) {
            //计算当前扇形扫过的角度
            float sweep = 360 * (pieEntries.get(i).getNumber() / total);
            //设置当前扇形的颜色
            paint.setColor(getResources().getColor(pieEntries.get(i).colorRes));
            //判断当前扇形是否被选中,确定用哪个半径
            float radiusT;
            if (pieEntries.get(i).isSelected()) {
                radiusT = sRadius;
            } else {
                radiusT = radius;
            }
            //画扇形的方法
            RectF rectF = new RectF(centerX - radiusT, centerY - radiusT, centerX + radiusT, centerY + radiusT);
            canvas.drawArc(rectF, startC, sweep, true, paint);

            //下面是画扇形外围的 短线和百分数值。

            float arcCenterC = startC + sweep / 2; //当前扇形弧线的中间点和圆心的连线 与 起始角度的夹角
            float arcCenterX = 0;  //当前扇形弧线的中间点 的坐标 x  以此点作为短线的起始点
            float arcCenterY = 0;  //当前扇形弧线的中间点 的坐标 y

            float arcCenterX2 = 0; //这两个点作为短线的结束点
            float arcCenterY2 = 0;
            //百分百数字的格式
            DecimalFormat numberFormat = new DecimalFormat("00.00");
            paint.setColor(Color.BLACK);

            //分象限 利用三角函数 来求出每个短线的起始点和结束点,并画出短线和百分比。
            //具体的计算方法看下面图示介绍
            if (arcCenterC >= 0 && arcCenterC < 90) {
                arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180));
                arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
                canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
                canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint);
            } else if (arcCenterC >= 90 && arcCenterC < 180) {
                arcCenterC = 180 - arcCenterC;
                arcCenterX = (float) (centerX - radiusT * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180));
                arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
                canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
                canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint);
            } else if (arcCenterC >= 180 && arcCenterC < 270) {
                arcCenterC = 270 - arcCenterC;
                arcCenterX = (float) (centerX - radiusT * Math.sin(arcCenterC * Math.PI / 180));
                arcCenterY = (float) (centerY - radiusT * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
                arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
                canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
                canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint);
            } else if (arcCenterC >= 270 && arcCenterC < 360) {
                arcCenterC = 360 - arcCenterC;
                arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY = (float) (centerY - radiusT * Math.sin(arcCenterC * Math.PI / 180));
                arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
                arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
                canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
                canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2, paint);
            }
            //将每个扇形的起始角度 和 结束角度 放入对应的对象
            pieEntries.get(i).setStartC(startC);
            pieEntries.get(i).setEndC(startC + sweep);
            //将当前扇形的结束角度作为下一个扇形的起始角度
            startC += sweep;
        }
    }
扇形的绘画图解
扇形的绘画逻辑.png
扇形周围短线和百分比的绘制逻辑
扇形周围短线和百分比的绘制逻辑.png

3. onTouchEvent方法的具体实现。

该方法主要是监听点击事件,从获取哪个扇形被点击到了。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX;
        float touchY;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchX = event.getX(); //touch点的坐标
                touchY = event.getY();
                //判断touch点到圆心的距离 是否小于半径
                if (Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2) <= Math.pow(radius, 2)) {
                    //计算 touch点和圆心的连线 与 x轴正方向的夹角
                    float touchC = getSweep(touchX, touchY);
                    //遍历 List<PieEntry> 判断touch点在哪个扇形中
                    for (int i = 0; i < pieEntries.size(); i++) {
                        if (touchC >= pieEntries.get(i).getStartC() && touchC < pieEntries.get(i).getEndC()) {
                            pieEntries.get(i).setSelected(true);
                            if (listener != null)
                                listener.onItemClick(i); //将被点击的扇形id回调出去
                        } else {
                            pieEntries.get(i).setSelected(false);
                        }
                    }
                    invalidate();//刷新画布
                }
                break;
        }

        return super.onTouchEvent(event);
    }

    /**
     * 获取  touch点/圆心连线  与  x轴正方向 的夹角
     *
     * @param touchX
     * @param touchY
     */
    private float getSweep(float touchX, float touchY) {
        float xZ = touchX - centerX;
        float yZ = touchY - centerY;
        float a = Math.abs(xZ);
        float b = Math.abs(yZ);
        double c = Math.toDegrees(Math.atan(b / a));
        if (xZ >= 0 && yZ >= 0) {//第一象限
            return (float) c;
        } else if (xZ <= 0 && yZ >= 0) {//第二象限
            return 180 - (float) c;
        } else if (xZ <= 0 && yZ <= 0) {//第三象限
            return (float) c + 180;
        } else {//第四象限
            return 360 - (float) c;
        }
    }
touch点和圆心连线 与 x轴正方向的夹角 计算逻辑
touch点和圆心连线 与 x轴正方向的夹角.png

使用

大功告成,下面看一下怎么使用。
很简单,跟普通的view使用差不多

xml中设置控件的大小
 <com.sharker.view.MyPieChart
                    android:id="@+id/pie_chart"
                    android:layout_width="match_parent"
                    android:layout_height="190dp" />
在java代码中使用
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // TODO: add setContentView(...) invocation
        setContentView(R.layout.xxx);
        
        MyPieChart pieChart = (MyPieChart) findViewById(R.id.pie_chart);
        pieChart.setRadius(DensityUtils.dp2px(getContext(), 75));
        pieChart.setOnItemClickListener(new MyPieChart.OnItemClickListener() {
            @Override
            public void onItemClick(int position) {
                
            }
        });
        List<MyPieChart.PieEntry> pieEntries = new ArrayList<>();
        pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_orange, true));
        pieEntries.add(new MyPieChart.PieEntry(2, R.color.chart_green, false));
        pieEntries.add(new MyPieChart.PieEntry(3, R.color.chart_blue, false));
        pieEntries.add(new MyPieChart.PieEntry(4, R.color.chart_purple, false));
        pieEntries.add(new MyPieChart.PieEntry(5, R.color.chart_mblue, false));
        pieEntries.add(new MyPieChart.PieEntry(6, R.color.chart_turquoise, false));
        pieChart.setPieEntries(pieEntries);
    }

Ok,自定义的饼状图完成。有哪些做的不合适的地方,希望大家多多指点。 有需要源码的小伙伴,留下邮箱即可。

补充

demo地址:https://github.com/chaohengxing/MyPieChartDemo.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 本文是我在阅读了网络上其他作者的优秀内容之后做的摘录转载,其中有对内容的补充。原来地址:http://www.id...
    狗子王1948阅读 2,475评论 2 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,401评论 25 707
  • 继承View基类,画了这样的扇形图 直接来步骤吧 (参考了GcsSloop的教程) 1.分析 自定义View需要认...
    stefanJi阅读 16,366评论 13 25
  • 1997年的初夏,我在北京南站,一个人踏上了开往保定的列车,再辗转两趟汽车,来到了安新县的白洋淀旁边。 有怎样的风...
    张溪客阅读 327评论 0 0
  • 第七章 复用类 1. 组合语法 1)对于非基本类型的对象,必须将其引用置于新的类中,但可以定义基本类型数据。2)每...
    FreeCode阅读 574评论 0 0