Android 扇形统计图的设计与编写

先上图,包含统计图初始化时的动画,点击环形的效果

没有数据时状态

饼状图根据效果不同,调用的APi参数略微有差异,有些同学可能不想要中间的空白直接全部展示扇形,emmmm。这种需求比你现在看到的这个样子 要简单的多,看完这种样式的,其他的实现方式也就懂了。

首先,实现思路不能落下:

1、在自定义View里面初始化的时候(构造方法里),

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

这行代码不能忘,这个是硬件加速,某些低端手机上如果不写着一行,阴影部分无法展示,而且相邻扇形图无法无缝对接,即便看起来你的代码并没有什么问题。

2、计算画布的大小以及定位我们的统计图位置

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
//View宽高
        mTotalWidth = w - getPaddingLeft() - getPaddingRight();
        mTotalHeight = h - getPaddingTop() - getPaddingBottom();
        //饼状图半径
        mRadius = WindowsUtil.dp2px(178) / 2;
        //参与计算的扇形半径(我们是画一个空心圆,例如:画圆的线的粗为2,实际需要半径为10,如果输入参数时半径不减去线粗的1/2的话 看到的环形外圈半径为11,内圈半径为9)
        calculateRadius = mRadius - mRadius / 4;
        mRectF.left = -calculateRadius;
        mRectF.top = -calculateRadius;
        mRectF.right = calculateRadius;
        mRectF.bottom = calculateRadius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
           //限定位置(原点为中心点)
        canvas.translate(mTotalWidth / 2, mTotalHeight / 2);
        //绘制饼图的每块区域
        drawPiePath(canvas);

    }

3、好了,直接说重点,看看 drawPiePath(canvas);方法里怎么操作,怎么去绘制的

 /**
     * 绘制饼图的每块区域 和文本
     *
     * @param canvas
     */
    private void drawPiePath(Canvas canvas) {
        //起始地角度
        float startAngle = starAngle;
        if (mDataList != null && mDataList.size() > 0) {

            for (int i = 0; i < mDataList.size(); i++) {
                String name = mDataList.get(i).getName() + (int) mDataList.get(i).getValue();
                float sweepAngle = mDataList.get(i).getValue() / mTotalValue * 360;//每个扇形的角度
                sweepAngle = sweepAngle * percent;
                mPaint.setColor(mDataList.get(i).getColor());

                mLinePaint.setColor(mDataList.get(i).getColor());
                mTextPaint.setColor(mDataList.get(i).getColor());

                double x1, y1;
                int addLength = 0;
                if (i != 0) {
                    if (i > 3) {
                        for (int c = 0; c < maxListNumH; c++) {
                            if (lineAndTextHorizontalAddress[i - c] > 0) {
                                addLength += WindowsUtil.dp2px(mTextPaint.measureText(name) / (maxListNumH * 2));
                            }
                        }
                    }
                    if (mDataList.get(i).getQuadrant() == 1 || mDataList.get(i).getQuadrant() == 3) {
                        x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                        y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                    } else {
                        if (mDataList.get(i - 1).getQuadrant() != mDataList.get(i).getQuadrant()) {
                            x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                            y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                        } else {
                            if (lineAndTextHorizontalAddress[i - 1] == 0) {
                                x1 = mRadius + LineLength + addLength;
                                y1 = mRadius + LineLength;
                            } else {
                                x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                                y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                            }
                        }
                    }
                } else {
                    x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                    y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                }
                //处理注释线的长度
                //确定直线的起始和结束的点的位置
                float pxs = (float) (mRadius * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
                float pys = (float) (mRadius * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
                float pxt = (float) ((x1) * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
                float pyt = (float) ((y1) * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));

                //绘制注释一线(斜线)
                canvas.drawLine(pxs, pys, pxt, pyt, mLinePaint);
                //绘制二线(水平线)和文本
                float stopX = 0, stopY = 0;

                if (mDataList.get(i).getQuadrant() == 2 || mDataList.get(i).getQuadrant() == 3) {//2 3 象限

                    float x = (pxt - horizontalLineLength - (lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name))) - addLength);
                    stopX = x;
                    stopY = pyt;
                    canvas.drawLine(pxt, pyt, x, pyt, mLinePaint);
                    canvas.drawText(name + "", stopX - mTextPaint.measureText(name), pyt, mTextPaint);
                    smallCircleRectF.set(stopX, stopY - 5, stopX + 10, stopY + 5);
                } else {
                    float x = (pxt + horizontalLineLength + (lineAndTextHorizontalAddress[i] * mTextPaint.measureText(name)) + addLength);
                    stopX = x;
                    stopY = pyt;
                    canvas.drawLine(pxt, pyt, x, pyt, mLinePaint);
                    canvas.drawText(name + "", stopX, pyt, mTextPaint);
                    smallCircleRectF.set(stopX - 10, stopY - 5, stopX, stopY + 5);
                }

                //画扇形
                setTouchView(pxt, pyt);
                //被点击状态
                if (position - 1 == i) {
                    Paint paint = new Paint(mPaint);
                    paint.setStyle(Paint.Style.STROKE);
                    paint.setStrokeWidth(mRadius / 2);
                    if (haveShadow) {
                        paint.setShadowLayer(4, 3, 3, Color.GRAY);
                    }
                    //外部展示扇形
                    canvas.drawArc(mRectFTouch, startAngle, sweepAngle, false, paint);
                } else {
                    //外部展示扇形
                    canvas.drawArc(mRectF, startAngle, sweepAngle, false, mPaint);
                }
                Paint paintCircle = new Paint(mPaint);
                paintCircle.setStrokeWidth(10);
                paintCircle.setStyle(Paint.Style.FILL);
                //线头的小圈圈
                canvas.drawArc(smallCircleRectF, 0, 360, true, paintCircle);
                angles[i] = startAngle;
                startAngle += sweepAngle;
                anglesEnds[i] = startAngle;
                float textWidth = paintCenterText.measureText(centerText);
                canvas.drawText(centerText, -textWidth / 2, -WindowsUtil.dp2px(12 / 2), paintCenterText);
                float textWidth1 = paintCenterText.measureText("" + (int) mTotalValue);
                canvas.drawText("" + (int) mTotalValue, -textWidth1 / 2, WindowsUtil.dp2px(14), paintCenterText);
            }
        } else {
            mPaint.setStrokeWidth(mRadius / 2);
            mPaint.setColor(initColor);
            canvas.drawArc(mRectF, 0, 360, false, mPaint);
            float textWidth = paintCenterText.measureText(centerText);
            canvas.drawText(centerText, -textWidth / 2, -WindowsUtil.dp2px(12 / 2), paintCenterText);
            float textWidth1 = paintCenterText.measureText("" + (int) mTotalValue);
            canvas.drawText("" + (int) mTotalValue, -textWidth1 / 2, WindowsUtil.dp2px(14), paintCenterText);
        }
    }

e m m m m m,代码有点长,啰哩啰嗦的,实际上是因为有许多我们需要去注意的效果、判断,所以看起来变量多了一些,而且,悄悄地跟你们说,,我原来计划让整个扇形转起来,想大转盘那样,并且数据不会重叠(相邻扇形角度过于小的时候,扇形注视数据文本会重叠),后来发现这个过程存在很多问题,项目也着急上线,就暂时没实现 正如代码中你看见的,我把整个座标轴分成了四个象限(初中数学),因为考虑到数据重叠,相邻象限的折现需要朝不同的方向延伸,我把每个象限又分成两个区间。设置一个基础长度x,如果出现同一个区间的扇形相邻,并且同时小于a度(一般写个15就可以),那么他们的注释线可能会重叠(我说的注释线 与折现是一个东西,并且折现分为两段,与不平行的,与扇形角度相关的 叫第一段,平行的吗,与注释文本挨着的叫第二段)

好了,概念中的名称我们知道了,基本的划分理念(八个区间)也知道了,代码思路就好理解多了

 4、我们来说一下API,说完API的使用方式,那么直接上代码,有些小伙伴比较懒,不想看原理与步骤,那就看看API的意思:

canvas.drawArc(mRectFTouch, startAngle, sweepAngle, false, paint);

这行代码是画扇形的核心代码里面各个参数大家应该知道是什么吧都,上源码来认真的复习一下

 /**
     * <p>
     * Draw the specified arc, which will be scaled to fit inside the specified oval.绘制指定的弧,将缩放到指定的椭圆内。
     * </p>
     * <p>
     * If the start angle is negative or >= 360, the start angle is treated as start angle modulo360.
    如果起始角为负或>= 360,则将起始角视为起始角模360
     * </p>
     * <p>
     * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
     * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
     * negative, the sweep angle is treated as sweep angle modulo 360
     * 如果扫描角为>= 360,则完全绘制椭圆。注意,这与SkPath::arcTo略有不同,后者处理扫角模360。如果扫描角为负,则将扫描角视为扫描角模360</p>
     * <p>
     * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0  degrees (3 o'clock on a watch.)
     *圆弧是顺时针画的。一个0度的角对应于一个0的几何角
(表上3点钟) </p>
     *
     * @param oval The bounds of oval used to define the shape and size of the arc
椭圆的边界,用来定义弧的形状和大小
     * @param startAngle Starting angle (in degrees) where the arc begins
起始角度
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
结束角度
     * @param useCenter If true, include the center of the oval in the arc, and close it if it is   being stroked. This will draw a wedge
如果为true,则将椭圆的中心包含在弧中,如果false,则将其关闭。这将画出一个楔子
(具体的结果大家可以试一下)
     * @param paint The paint used to draw the arc
画笔 没什么可说的,承载了很多责任
     */
    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
    }

RectF oval, float startAngle, float sweepAngle, boolean useCenter,

第一个参数RectF oval,限定画的扇形在座标轴上的位置与大小,我们的点击突出效果就是靠改变他的参数它来实现的

到了这里不得不说一个效果————阴影效果,给画笔设置

paint.setShadowLayer(float radius ,float dx,float dy,int color);

在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色

这行代码,就会有投影,emmmm 太简单了,想当初为了这个阴影效果,我是爬遍了各种技术帖子,都计划自己去画阴影了,幸亏灵机一动看了一下Pain 的各个参数的介绍,

说到了Pain。那么我们把另一个使用到的相关的属性也说一下(控制 画扇形还是画圆弧)

        paintCenterText.setStyle(Paint.Style.FILL);    //设置画笔为填充
        mPaint.setStyle(Paint.Style.STROKE);//不填充
        设置画笔的样式, FILL,FILL_OR_STROKE,或STROKE

这里送大家一个链接,关于Paint的方法介绍,https://blog.csdn.net/lpjishu/article/details/84716912,这位大神写的还不错

Api知道了,大致的实现思路知道了,剩下的就是代码以及计算了,

计算主要用到了余弦定理,高中数学忘了的童鞋可以回去复习一下了,因为这个忘了的话,根本没法去计算(勾股定理已经不够用了)

使用方式

 pieChart = findViewById(R.id.pc_devices);
//设置点击扇形之后的处理模式
        pieChart.setTouchStyle(PieChartConstant.TOUCH_TYPE_MOVE);

//可以自己去模拟数据
for(int i=0;i<z;i++){
//数量除了展示之外还会涉及到具体的计算(扇形比重)
  arrayList.add(new PieDataEntity(“名称”
                                , “数量”, color));
}
//arrayList 可以穿入null
  pieChart.setDataList(arrayList);
                    pieChart.startAnimation(2000);

这里我先列出所有涉及到的计算:

1、点击环形之后的偏移量

2、注释线的长度计算,文本的位置

3、扇形的中线长度位置

4、每一个扇形的的起始与中止位置

 

扇形的api大家知道怎么用了,然后点击扇形之后的突出实现原理(画突出的扇形的时候,圆心按照扇形的中心角度方向去偏移)大家也明白了,比较有难点的,或者说比较复杂的,是注释线的长度适配,错位适配法

代码链接:扇形统计图代码下载

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章