安卓自定义饼状图

来源:互联网 发布:mac设置自动关机 编辑:程序博客网 时间:2024/06/11 01:00

闲来想自己写个饼状图,于是就动手开始画了。
主要的逻辑:1.根据比例依次旋转角度画出扇形;2.在扇形区域内设置内容数据;3.当点击某个扇形的时候,就让当前扇形脱离整体,空出一部分
看起来这个逻辑比较复杂,但是真正写下来之后就会发现其实饼状图也简单,主要就是围绕着安卓简单的自定义控件画扇形。效果如下:
这里写图片描述
接下来就开始代码实现:
首先初始化2个画笔,一个是画扇形的,一个是画扇形的边框的,还有初始化集合数据:

private void initPaint() {        mPaint = new Paint();        mPaint.setStyle(Paint.Style.FILL);        mPaint.setAntiAlias(true);        mPaintBorder = new Paint(Paint.ANTI_ALIAS_FLAG);        mPaintBorder.setStyle(Paint.Style.STROKE);        mPaintBorder.setAntiAlias(true);        mPaintBorder.setColor(Color.BLACK);        mPaintBorder.setTextSize(35); mDatas.add(new PieItemBean("测试1", 9, Color.rgb(155, 187, 90)));        mDatas.add(new PieItemBean("测试2", 3, Color.rgb(191, 79, 75)));        mDatas.add(new PieItemBean("测试3", 76f, Color.rgb(242, 167, 69)));        mDatas.add(new PieItemBean("测试4", 6, Color.rgb(60, 173, 213)));        mDatas.add(new PieItemBean("测试5", 6, Color.rgb(90, 79, 88)));}

然后先复写onMeasure方法,以便设置控件的大小:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        setMeasuredDimension(measureValue(widthMeasureSpec), measureValue(heightMeasureSpec));    }    private int measureValue(int measureSpec) {        int result = 100;//设置最小值        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if (specMode == MeasureSpec.EXACTLY) { //fill_parent或者设置了具体的宽高            result = specSize;        } else if (specMode == MeasureSpec.AT_MOST) { //wrap_content            result = Math.min(result, specSize);        }        return result;    }

接下来就是最重要的onDraw方法了.
在onDraw方法中,首先需要获得padding值和宽高,从而设置圆的半径:

        int width = getMeasuredWidth();        int height = getMeasuredHeight();        int rWidth = width - getPaddingLeft() - getPaddingRight();        int rHeight = height - getPaddingTop() - getPaddingBottom();        mRadious = Math.min(rWidth, rHeight) / 2;        //圆心坐标        cenX = mRadious + getPaddingLeft();        cenY = mRadious + getPaddingTop();

然后就是根据每个条目所占比例来绘制扇形图了:

 //用于存放当前百分比的圆心角度        float currentAngle = 0.0f;        float offsetAngle = 0f;//角度偏移量        for (int i = 0; i < mDatas.size(); i++) {            PieItemBean bean = mDatas.get(i);            currentAngle = per2Radious(totalAngle, bean.value);//得到当前角度            mPaint.setColor(bean.color);//给画笔设置颜色            if (mRectF == null) {//设置圆所需的范围                mRectF = new RectF(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), width - getPaddingRight());            }            //在饼图中显示所占比例            canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaint);            //边框            canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaintBorder);            //下次的起始角度            offsetAngle += currentAngle;        }

其中用到了2个工具方法:

 /**     * 将百分比转换为图心角角度     */    public float per2Radious(float totalAngle, float percentage) {        float angle = 0.0f;        if (percentage >= 101f || percentage < 0.0f) {            //Log.e(TAG,"输入的百分比不合规范.须在0~100之间.");        } else {            float v = percentage / 100;//先获取百分比            float itemPer = totalAngle * v;//获取对应角度的百分比            angle = round(itemPer, 2);//精确到小数点后面2位        }        return angle;    }    /**     * 四舍五入到小数点后scale位     */    public float round(float v, int scale) {        if (scale < 0)            throw new IllegalArgumentException("The scale must be a positive integer or zero");        BigDecimal bgNum1 = new BigDecimal(v);        BigDecimal bgNum2 = new BigDecimal("1");        return bgNum1.divide(bgNum2, scale, BigDecimal.ROUND_HALF_UP).floatValue();    }

然后我们就可以先运行起来了,运行后结果如下:
这里写图片描述

可以看到整个轮廓已经出来了,接下来就是第二步,在扇形区域内部显示文字.在显示文字的的时候主要是要计算文字描绘的起点位置,因为对于三角函数只对90°内的熟悉,所以计算的时候都转换为90°之内的值来计算:

 //先将度数定位到当前所在条目的一半位置            float degree = offsetAngle + currentAngle / 2;            //根据角度所在不同象限来计算出文字的起始点坐标            float dx = 0, dy = 0;             if (degree > 0 && degree <= 90f) {//在第四象限                dx = (float) (cenX + mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * degree));//注意Math.sin(x)中x为弧度值,并非数学中的角度,所以需要将角度转换为弧度                dy = (float) (cenY + mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * degree));            } else if (degree > 90f && degree <= 180f) {//在第三象限                dx = (float) (cenX - mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (180f - degree)));                dy = (float) (cenY + mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (180f - degree)));            } else if (degree > 180f && degree <= 270f) {//在第二象限                dx = (float) (cenX - mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (270f - degree)));                dy = (float) (cenY - mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (270f - degree)));            } else {                dx = (float) (cenX + mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (360f - degree)));                dy = (float) (cenY - mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (360f - degree)));            }            //文字的基本线坐标设置为半径的2.3/3位置处,起点y坐标设置为半径的2.7/3位置处            canvas.drawText(bean.value + "%", dx, dy, mPaintBorder);            //下次的起始角度            offsetAngle += currentAngle;

这里写图片描述

现在数据已经都绘制好了,剩下的就是扇形区的点击事件了
要判断点击的位置位于哪个条目,需要以下几点判断:
1.首先根据点击点的坐标来计算到圆心的距离,来判断点击位置是否在圆内部
2.如果在圆的内部,再计算出点击处与x轴所成的角度,然后根据该角度来判断在哪个数据合集内,之前的offsetAngle属性最后记录的都是当前元素在坐标系内横跨的角度,所以建立一个数组存起来,方便对比:

//下次的起始角度            offsetAngle+= currentAngle;            degrees[i] = offsetAngle;

然后就是复写onTouchEvent来计算了。在这里补充一下onTouchEvent事件的返回值说明:如果返回true,则是当前触摸事件(整个事件包括down,move,up)不处理,留着交给父控件处理或者在别的动作的时候消耗,所以在down,move时都返回true,以便将触摸事件能够交给up时处理,up处理完,当前的点击事件已经处理完成了,所以可以返回false,示意该控件已经处理完了这个事件,不需要父控件在处理了。

private long startTime;//点击的起始时间    @Override    public boolean onTouchEvent(MotionEvent event) {        float x = event.getX();        float y = event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                startTime = System.currentTimeMillis();                break;            case MotionEvent.ACTION_MOVE:                break;            case MotionEvent.ACTION_UP:                long currTime = System.currentTimeMillis();                float dx = x - cenX;                float dy = y - cenY;                if (currTime - startTime <= 500) {//按下和抬起的时间在500毫秒内认为是单击                    float degree;//被点击选中的角度                    //先判断是否在圆内部                    if (isInCircle(dx, dy, mRadious)) {//根据不同的象限来获取到x轴的角度                        if (dx > 0 && dy > 0) {//第四象限                            degree = (float) (90f - 180 * Math.atan2(dx, dy) / PI);                        } else if (dx < 0 && dy > 0) {//第三象限                            degree = (float) (180 * Math.atan2(-dx, dy) / PI + 90f);                        } else if (dx < 0 && dy < 0) {//第二象限                            degree = (float) (180 * Math.atan2(dy, dx) / PI + 360f);                        } else {//第一象限                            degree = (float) (360f - 180 * Math.atan2(-dy, dx) / PI);                        }                        //然后判断该角度在哪个数据集内                        selectedPos = judgeDegree(degree);                        invalidate();//请求重绘                    }                }                return false;        }        return true;    }    /**     * 根据坐标计算是否在圆内部     */    private boolean isInCircle(float lx, float ly, float radius) {        double v = Math.pow(Math.abs(lx), 2) + Math.pow(Math.abs(ly), 2);        double dis = Math.sqrt(v);        if (dis > radius) {            return false;        }        return true;    }    /**     * 判断当前点击的在哪个数据集合里面     */    private int judgeDegree(float degree) {        int selectedPos = 0;        for (int i = 0; i < degrees.length; i++) {            if (degree <= degrees[i]) {                selectedPos = i;                break;            }        }        return selectedPos;    }

至此,onTouchEvent事件已经处理完了,接下来就是在onDraw方法中将选中的条目剥离出来,单独处理,从而完成点击效果

 if (selectedPos == i) {//选中的偏离一点儿                canvas.save();                canvas.rotate(offsetAngle + currentAngle / 2);//先将画布x轴旋转到当前角度的一半位置,                canvas.translate(50, 0);//然后在平移50个单位,就将被选中的模块独立出来了                canvas.drawArc(mRectF, currentAngle / 2, -currentAngle, true, mPaint);                //边框                canvas.drawArc(mRectF, currentAngle / 2, -currentAngle, true, mPaintBorder);                canvas.restore();            } else {                //在饼图中显示所占比例                canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaint);                //边框                canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaintBorder);            }

这里写图片描述
最后,再加入一个初始化时候的旋转动画
利用handler和message:

private static final int MSG_INFO = 0x24;    private int num = 0;    private float perDegree = 360f / 10;//单位扩大角度    private Handler mHander = new Handler() {        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);            if (msg.what == MSG_INFO) {                if (num > 10) {                    return;                }                totalAngle = num * perDegree;                invalidate();                num++;            }        }    };

在onDraw中加入推送消息

 mHander.sendEmptyMessageDelayed(MSG_INFO,20);

附上源码下载地址:
demo地址

原创粉丝点击