开源图表库Android进阶Android自定义控件

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

2016-05-10  本文已影响2872人  Kisson

效果图

饼状图

需求

  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;
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读