安卓自定义饼状图

闲来想自己写个饼状图,于是就动手开始画了。
主要的逻辑: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地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是一种常用的数据可视化表,在Android开发中,可以使用MPAndroidChart库来实现的绘制。MPAndroidChart是一个功能强大且易于使用的开源表库,提供了丰富的表类型和自定义选项。 要在Android项目中使用MPAndroidChart库,首先需要在项目的build.gradle文件中添加依赖: ``` dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' } ``` 接下来,在布局文件中添加一个`PieChart`视: ```xml <com.github.mikephil.charting.charts.PieChart android:id="@+id/pie_chart" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 然后,在代码中获取`PieChart`实例,并设置数据和样式: ```java PieChart pieChart = findViewById(R.id.pie_chart); // 创建数据集 ArrayList<PieEntry> entries = new ArrayList<>(); entries.add(new PieEntry(40f, "Label 1")); entries.add(new PieEntry(30f, "Label 2")); entries.add(new PieEntry(20f, "Label 3")); entries.add(new PieEntry(10f, "Label 4")); PieDataSet dataSet = new PieDataSet(entries, "Pie Chart"); // 设置每个数据块的颜色 dataSet.setColors(ColorTemplate.COLORFUL_COLORS); PieData data = new PieData(dataSet); pieChart.setData(data); // 设置的样式和交互选项 pieChart.setDrawHoleEnabled(true); pieChart.setHoleColor(Color.WHITE); pieChart.setTransparentCircleColor(Color.WHITE); pieChart.setTransparentCircleAlpha(110); pieChart.setHoleRadius(58f); pieChart.setTransparentCircleRadius(61f); pieChart.setDrawCenterText(true); pieChart.setRotationEnabled(true); pieChart.setHighlightPerTapEnabled(true); pieChart.animateY(1400, Easing.EaseInOutQuad); // 更新视 pieChart.invalidate(); ``` 这样就可以在Android应用中绘制一个简单的了。你可以根据需要调整样式和交互选项,以满足具体需求。更多关于MPAndroidChart的使用和自定义选项,请参考官方文档和示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值