Android自定义饼状图,且能区分点击的区域

效果图

饼状图

需求

  1. 根据某种类型的数据大小占总数据大小的百分比来决定其在饼状图中圆环弧的大小。(百分比系数乘以360就是圆环弧的度数)
  2. 不同类型数据可以设置不同圆环弧颜色。
  3. 点击圆环上任意一点,可以判断其点击的是何种类型数据。

实现步骤

定义数据结构

因为需要根据所传数据绘制不同饼状图,所以首先定义其数据类型:

    //数据的类型
    private String type;
    //数据的大小
    private float value;
    //数据类型所对应圆环弧的颜色资源Id
    private int colorId;

绘制步骤

上图中主要绘制的地方有三处:
1.外圆白色的边框。
2.不同颜色的弧。
3.白色的内圆。
所以对应的我们需要三个画笔(Paint)。
第一个画笔用来绘制外圆白色的边框。
第二个画笔用来绘制不同颜色的弧。
第三个画笔用来绘制白色的内圆。

测量饼状图的大小

因为饼状图最终的呈现的是一个圆,所以其width和height是相等的,也就是外圆的直径。因此我们需要重写onMeasure方法。

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   //mRadius是外圆半径
    int length = (int)(2 * mRadius);
    setMeasuredDimension(length, length);
}

绘制动画

因为要求绘制圆环的时候带一个逐步绘制的动画,所以我们需要继承Animation类来完成动画效果。
这里只需重写Animation类的applyTransformation方法。

处理点击事件

处理点击事件相对于绘制而言,逻辑上要稍微复杂点。如上图中,黄色圆环弧、红色圆环弧、橙色圆环弧的百分比系数分别是0.5、0.25、0.25,那么对应的圆环弧度数为180°、90°、90°。
(1)响应点击的事件只能发生在圆环中间。
(2)在条件(1)的基础上,计算出圆顶点顺时针到事件点击处的角度。
如果点击是红色圆环弧中间位置,那么从圆定点顺时针到点击处走过的度数是(180°+45° = )225°,225°是小于黄色圆环弧度数加上红色圆环弧的度数。


点击事件坐标图

上图是点击事件坐标图,坐标原点就是饼状图的左上顶点。如果设外圆半径为R,内容半径是0.72*R,那么圆心坐标是(R,R)。
假设点击事件的坐标为(X,Y)。那么只要其满足:

(0.72R)²≤(X-R)² + (Y-R)²≤R²

就可以判断点击的事件发生在圆环弧上。
接着我们需要计算出圆顶点顺时针到事件点击处的角度θ。如下图,将圆分为1、2、3、4四个相等部分。

四等分圆

1.若点击事件发生在第一部分。
则θ = Math.atan2(X - R, R - X) * 180 / PI。
2.若点击事件发生在第二部分。
则θ = Math.atan2(Y - R, X - R) * 180 / PI + 90°。
3.若点击事件发生在第三部分。
则θ = Math.atan2(R - X, Y - R) * 180 / PI + 180°。
4.若点击事件发生在第四部分。
则θ = Math.atan2(R - Y, R - X) * 180 / PI + 180°。

注:PI = 3.1415

计算出角度θ后,我们可以确定点击事件发生在哪段圆环弧上了。

项目代码

最后直接上干货。

public class PieChartView extends View {
    //饼图白色轮廓画笔
    private Paint mOuterLinePaint;
    //饼状图画笔
    private Paint mPiePaint;
    //内圆画笔
    private Paint mInnerPaint;
    //饼状图外圆半径
    private float mRadius = DensityUtil.dip2px(getContext(), 60) + OUTER_LINE_WIDTH;
    //构成饼状图的数据集合
    private List<PieData> mPieDataList;
    //绘制弧形的sweep数组
    private float[] mPieSweep;
    //饼状图动画效果
    private PieChartAnimation mAnimation;
    //初始画弧所在的角度
    private static final int START_DEGREE = -90;

    private static final int PIE_ANIMATION_VALUE = 100;
    //外圆边框的宽度
    private static int OUTER_LINE_WIDTH = 3;
    //动画时间
    private static final int ANIMATION_DURATION = 800;

    private RectF mRectF = new RectF();
    //圆周率
    private static final float PI = 3.1415f;

    private static final int PART_ONE = 1;

    private static final int PART_TWO = 2;

    private static final int PART_THREE = 3;

    private static final int PART_FOUR = 4;


    public void setOnSpecialTypeClickListener(OnSpecialTypeClickListener listener) {
        this.mListener = listener;
    }

    private OnSpecialTypeClickListener mListener;

    public PieChartView(Context context) {
        super(context);
        init();
    }

    public PieChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    //初始化画笔和效果动画
    private void init() {
        mOuterLinePaint = new Paint();
        mOuterLinePaint.setAntiAlias(true);
        mOuterLinePaint.setStyle(Style.STROKE);
        mOuterLinePaint.setStrokeWidth(OUTER_LINE_WIDTH);
        mOuterLinePaint.setColor(Color.WHITE);

        mPiePaint = new Paint();
        mPiePaint.setAntiAlias(true);
        mPiePaint.setStyle(Style.FILL);
        //设置动画
        mAnimation = new PieChartAnimation();
        mAnimation.setDuration(ANIMATION_DURATION);

        mInnerPaint = new Paint();
        mInnerPaint.setColor(Color.WHITE);
        mInnerPaint.setStyle(Style.FILL);
        mInnerPaint.setAntiAlias(true);
        initRectF();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mPieDataList != null && !mPieDataList.isEmpty()) {
            //起始是从-90°位置开始画
            float pieStart = START_DEGREE;
            if (mPieSweep == null) {
                mPieSweep = new float[mPieDataList.size()];
            }
            for (int i = 0; i < mPieDataList.size(); i++) {
                //设置弧形颜色
                mPiePaint.setColor(getResources().getColor(mPieDataList.get(i).getColorId()));
                //绘制弧形区域,以构成饼状图
                float pieSweep = mPieDataList.get(i).getValue() * 360;
                canvas.drawArc(mRectF, pieStart, mPieSweep[i], true, mPiePaint);
                canvas.drawArc(mRectF, pieStart, mPieSweep[i], true, mOuterLinePaint);
                //获取下一个弧形的起点
                pieStart += pieSweep;
            }
        } else {
            //无数据时,显示灰色圆环
            mPiePaint.setColor(Color.parseColor("#dadada"));//灰色
            canvas.drawCircle(mRadius, mRadius, mRadius, mPiePaint);
        }
        drawInnerCircle(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int length = (int) (2 * mRadius);
        setMeasuredDimension(length, length);
    }

    /**
     * 设置需要绘制的数据集合
     */
    public void setPieDataList(List<PieData> pieDataList) {
        this.mPieDataList = pieDataList;
        if (mPieSweep == null) {
            mPieSweep = new float[mPieDataList.size()];
        }
        startAnimation(mAnimation);
    }

    /**
     * 设置外圆半径
     *
     * @param radius 外圆半径 dp为单位
     **/
    public void setOuterRadius(float radius) {
        this.mRadius = DensityUtil.dip2px(getContext(), radius) + OUTER_LINE_WIDTH ;
        initRectF();
    }

    /**
     * 初始化绘制弧形所在矩形的四点坐标
     **/
    private void initRectF() {
        mRectF.left = 0;
        mRectF.top = 0;
        mRectF.right = 2 * mRadius;
        mRectF.bottom = 2 * mRadius;
    }

    private class PieChartAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            mPieSweep = new float[mPieDataList.size()];
            if (interpolatedTime < 1.0f) {
                for (int i = 0; i < mPieDataList.size(); i++) {
                    mPieSweep[i] = (mPieDataList.get(i).getValue() * PIE_ANIMATION_VALUE) * interpolatedTime / PIE_ANIMATION_VALUE * 360;
                }
            } else {
                for (int i = 0; i < mPieDataList.size(); i++) {
                    mPieSweep[i] = mPieDataList.get(i).getValue() * 360;
                }
            }
            invalidate();
        }
    }

    protected void drawInnerCircle(Canvas canvas) {
        canvas.drawCircle(mRadius, mRadius, (float) (mRadius * 0.72), mInnerPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                doOnSpecialTypeClick(event);
                break;
        }
        return super.onTouchEvent(event);
    }


    private void doOnSpecialTypeClick(MotionEvent event) {
        if (mPieDataList == null || mPieDataList.isEmpty()) return;
        float eventX = event.getX();
        float eventY = event.getY();
        double alfa = 0;
        float startArc = 0;
        //点击的位置到圆心距离的平方
        double distance = Math.pow(eventX - mRadius, 2) + Math.pow(eventY - mRadius, 2);
        //判断点击的坐标是否在环内
        if (distance < Math.pow(mRadius, 2) && distance > Math.pow(0.72 * mRadius, 2)) {
            int which = touchOnWhichPart(event);
            switch (which) {
                case PART_ONE:
                    alfa = Math.atan2(eventX - mRadius, mRadius - eventY) * 180 / PI;
                    break;
                case PART_TWO:
                    alfa = Math.atan2(eventY - mRadius, eventX - mRadius) * 180 / PI + 90;
                    break;
                case PART_THREE:
                    alfa = Math.atan2(mRadius - eventX, eventY - mRadius) * 180 / PI + 180;
                    break;
                case PART_FOUR:
                    alfa = Math.atan2(mRadius - eventY, mRadius - eventX) * 180 / PI + 270;
                    break;
            }
            for (PieData data : mPieDataList) {
                startArc = startArc + data.getValue() * 360;
                if (alfa != 0 && alfa < startArc) {
                    if (mListener != null) mListener.onSpecialTypeClick(data.getType());
                    break;
                }
            }
        }
    }

    /**
     *    4 |  1
     * -----|-----
     *    3 |  2
     * 圆被分成四等份,判断点击在园的哪一部分
     */
    private int touchOnWhichPart(MotionEvent event) {
        if (event.getX() > mRadius) {
            if (event.getY() > mRadius) return PART_TWO;
            else return PART_ONE;
        } else {
            if (event.getY() > mRadius) return PART_THREE;
            else return PART_FOUR;
        }
    }

    public interface OnSpecialTypeClickListener {
        void onSpecialTypeClick(String type);
    }

    public static class PieData {

        private String type;

        private float value;

        private int colorId;

        public PieData(String type, float value, int colorId) {
            this.type = type;
            this.value = value;
            this.colorId = colorId;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public float getValue() {
            return value;
        }

        public void setValue(float value) {
            this.value = value;
        }

        public int getColorId() {
            return colorId;
        }

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

推荐阅读更多精彩内容