自定义View实现 android圆形统计图 带动画可以点击

通常app中可能的数据展示控件有柱状图,折线图,饼状图等,如果需要一个包含多种View控件的库,那么 MPAndroidChart 是不错的选择,如果只是需要一个简单的独立的饼状图控件,希望RingView满足你的要求。

控件介绍

效果图如下

在这里插入图片描述

控件功能

展示一组数据 绘制圆环,展示对应模块文本信息, 点击对应模块进行放大处理

实现过程

绘制圆环

圆环的基本绘制

圆环的绘制实际就是通过先后绘制两个半径不同的圆实现,圆就是360度的扇形,canvas.drawArc提供了这个功能:

public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint)

需要先绘制有颜色的外圆对应的各个扇形,之后再“覆盖”绘制内圆对应的各个扇形。

绘制圆环的时候需要考虑开始角度mStartAngle和当前的旋转mRotation。这里设计了一个方法drawPieFromEnd用来在(start, end)的角度范围内绘制“被显示”的那些扇形。这里的角度是扇形数组的形成的0-360的连续角度范围。

为了绘制的简单,方法选择从最后一个扇形开始绘制,相当于从end绘制到start,这样的好处是不用去计算实际上start对应的是哪个扇形了,而根据传递的角度范围,当下一个绘制的扇形的起始角度大于start时,结束绘制:

/**
 * 从尾部开始绘制圆环,只绘制endAngle到startAngle之间的,不一定绘制所有圆环。
 *
 * @param canvas
 * @param startAngle
 * @param endAngle
 */
  private void drawPieFromEnd(Canvas canvas, float startAngle, float endAngle1) {
        if (angles == null) return;
        for (int i = angles.length - 1; i >= 0; i--) {
            float itemAngle = angles[i] + 0.5f;
            float sweepStart = endAngle1 - itemAngle;
            mPaint.setColor((colorList.get(i)));
            if (sweepStart >= startAngle) {
                canvas.drawArc(rectF, sweepStart, itemAngle, true, mPaint);

            } else {
                itemAngle = endAngle1 - startAngle;

                canvas.drawArc(rectF, startAngle, itemAngle, true, mPaint);
                break;
            }
            endAngle1 -= itemAngle;
        }
    }

动画
当前控件交互过程中总共有一个动画:开始时候绘制动画 showOut

所有动画通过Animation实现,这里只是使用Animation完成动画时间和进度的控制。
重写applyTransformation方法来记录当前动画的进度progress,然后invalidate通知onDraw的执行。
开始动画执行时将当前动画模式字段int mAnimMode设置为不同的ANIM_MODE_xxx常量,然后onDraw中会根据当前的mAnimMode值,选择对应动画的绘制方法去执行。

代码结构如下:

public class PieGraphView extends View {
  private static final int ANIM_MODE_NONE = 0;
  private static final int ANIM_MODE_ROTATE = 1;
  ...

 private void initAnims() {
        mAnimShowOut = new Animation() {
            @Override
            protected void applyTransformation(final float interpolatedTime, final Transformation t) {
                mShowOutProgress = interpolatedTime;
                invalidate();
                if (interpolatedTime >= 1.0f) {
                    cancel();
                    mAnimMode = ANIM_MODE_FINISHED;
//                    mCurrentItem=0;
//                    invalidate();
                    // mAnimMode = ANIM_MODE_NONE;
                    // 目前动画都是通过Animation完成的,而不是在onDraw中递归invalidate实现,所以为了
                    // 避免两个连续的动画产生“跳跃”,将下一个旋转动画放到下个UI循环中
                    post(new Runnable() {
                        @Override
                        public void run() {
                            int item = Math.max(0, angles.length - 1);
                            setCurrentItem(0, false);
                        }
                    });
                }
            }
        };
        mAnimShowOut.setDuration(mShowOutDuration);

    }
    ...
  }

  ...

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        pointList.clear();
        mPaint.setStyle(Paint.Style.FILL);
        if (rateList == null)
            return;

//        drawArcAndText(canvas);

        switch (mAnimMode) {

            case ANIM_MODE_SHOW_OUT:
                animDrawProceed(canvas, mShowOutProgress);
                if (isRing) {
                    drawInner(canvas);
                }
                if (isShowCenterPoint) {
                    drawCenterPoint(canvas);
                }

//                canvas.drawArc(rectF, 0, 360, true, mPaint);
                break;

            case ANIM_MODE_NONE:
//                drawGrownPie(canvas);
                break;
            case ANIM_MODE_FINISHED:
                //动画完成后绘制折现 文本
//                drawableText(canvas);
                drawArcAndText(canvas);


        }
    }


initAnims()方法中对动画进行初始化。执行runAnimRotate()来开启动画。onDraw方法中根据动画模式选择执行不同的绘制方法。

方法calcClickItem完成了点击事件的不同处理:点击圆环内部和外部不进行处理,点击圆环对点击模块进行放大处理。

 private int calcClickItem(float x, float y) {
        if (rateList == null) return -1;
        final float centerX = rectF.centerX();
        final float centerY = rectF.centerY();
        float outerRadius = rectF.width() / 2;
        float innerRadius = 80;

        // 计算点击的坐标(x, y)和圆中心点形成的角度,角度从0-360,顺时针增加
        int clickedDegree = GeomTool.calcAngle(x, y, centerX, centerY);
        double clickRadius = GeomTool.calcDistance(x, y, centerX, centerY);


        if (clickRadius < innerRadius) {
            // 点击发生在小圆内部,也就是点击到标题区域
//            return -1;
        } else if (clickRadius > outerRadius) {
            // 点击发生在大圆环外
            return -2;
        }

        // 计算出来的clickedDegree是整个View原始的,被点击item需要考虑startAngle。
        int startAngle = -90;
        int angleStart = startAngle;
        for (int i = 0; i < angles.length; i++) {
            int itemStart = (angleStart + 360) % 360;
            float end = itemStart + angles[i];
            if (end >= 360f) {
                if (clickedDegree >= itemStart && clickedDegree < 360) return i;
                if (clickedDegree >= 0 && clickedDegree < (end - 360)) return i;
            } else {
                if (clickedDegree >= itemStart && clickedDegree < end) {
                    return i;
                }
            }

            angleStart += angles[i];
        }

        return -3;
    }

    // 计算点击的坐标(x, y)和圆中心点形成的角度,角度从0-360,顺时针增加
    int clickedDegree = GeomTool.calcAngle(x, y, centerX, centerY);

计算点击的角度
根据点击的坐标(x, y)和圆心(centerX, centerY)可以计算出点击的点相对圆心的角度。下面方法calcAngle完成此任务。

代码如下:

/**
 * 计算坐标(x1, y1)和(x2, y2)形成的角度,角度从0-360,顺时针增加
 * (x轴向右,y轴向下)
 */
public static int calcAngle(float x1, float y1, float x2, float y2) {
    double resultDegree = 0;

    double vectorX = x1 - x2; // 点到圆心的X轴向量,X轴向右,向量为(0, vectorX)
    double vectorY = y2 - y1; // 点到圆心的Y轴向量,Y轴向上,向量为(0, vectorY)
    // 点落在X,Y轴的情况这里就排除
    if (vectorX == 0) {
        // 点击的点在Y轴上,Y不会为0的
        if (vectorY > 0) {
            resultDegree = 90;
        } else {
            resultDegree = 270;
        }
    } else if (vectorY == 0) {
        // 点击的点在X轴上,X不会为0的
        if (vectorX > 0) {
            resultDegree = 0;
        } else {
            resultDegree = 180;
        }
    } else {
        // 根据形成的正切值算角度
        double tanXY = vectorY / vectorX;
        double arc = Math.atan(tanXY);
        // degree是正数,相当于正切在四个象限的角度的绝对值
        double degree = Math.abs(arc / Math.PI * 180);
        // 将degree换算为对应x正轴开始的0-360的角度
        if (vectorY < 0 && vectorX > 0) {
            // 右下 0-90
            resultDegree = degree;
        } else if (vectorY < 0 && vectorX < 0) {
            // 左下 90-180
            resultDegree = 180 - degree;
        } else if (vectorY > 0 && vectorX < 0) {
            // 左上 180-270
            resultDegree = 180 + degree;
        } else {
            // 右上 270-360
            resultDegree = 360 - degree;
        }
    }

    return (int) resultDegree;
}

上面的方法calcClickItem根据此角度,结合当前圆环的mStartAngle、mRotation就可以确定点击落在的扇形区域了。

计算扇形中心
绘制扇形过程中,可以得到扇形的中间角度middleAngle,而中心的半径就是圆环外半径减去一半圆环宽度,使用GeomTool.calcCirclePoint工具方法,可以根据“圆心、半径、角度”计算出扇形中心点的坐标。

代码如下:

/**
 * 计算指定角度、圆心、半径时,对应圆周上的点。
 * @param angle 角度,0-360度,X正轴开始,顺时针增加。
 * @param radius 圆的半径
 * @param cx 圆心X
 * @param cy 圆心Y
 * @param resultOut 计算的结果(x, y) ,方便对象的重用。
 * @return resultOut, or new Point if resultOut is null.
 */
public static Point calcCirclePoint(int angle, float radius, float cx, float cy, Point resultOut) {
    if (resultOut == null) resultOut = new Point();

    // 将angle控制在0-360,注意这里的angle是从X正轴顺时针增加。而sin,cos等的计算是X正轴开始逆时针增加
    angle = clampAngle(angle);
    double radians = angle / 180f * Math.PI;
    double sin = Math.sin(radians);
    double cos = Math.cos(radians);

    double dy = radius * sin;
    double dx = radius * cos;
    double x = cx + dx;
    double y = cy + dy;

    resultOut.set((int) x, (int) y);
    return resultOut;
}

绘制描述文本和折现

 private void drawArcCenterPoint(Canvas canvas, int position) {
        if (rateList!=null){
            preAngle=getPreAngle(position);
        }

        if (rateList != null) {
            endAngle = getAngle(rateList.get(position));
        }

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mRes.getColor(R.color.transparent));
        mPaint.setStrokeWidth(dip2px(1));
        canvas.drawArc(rectFPoint, preAngle, (endAngle) / 2, true, mPaint);
        dealPoint(rectFPoint, preAngle, (endAngle) / 2, pointArcCenterList);
        Point point = pointArcCenterList.get(position);
        mPaint.setColor(mRes.getColor(R.color.color_D8D8D8));
//        canvas.drawCircle(point.x, point.y, dip2px(2), mPaint);

        if (preRate / 2 + rateList.get(position) / 2 < 5) {
            extendLineWidth = 17;
            rate = 0.3f;
        } else {
            extendLineWidth = 17;
            rate = 0.3f;
        }

        rate = 15f / ringPointRidus;

        // 外延画折线
        float lineXPoint1 = (point.x - dip2px(leftMargin + ringOuterRidus)) * (1 + rate);
        float lineYPoint1 = (point.y - dip2px(topMargin + ringOuterRidus)) * (1 + rate);

        float[] floats = new float[8];
        floats[0] = point.x;
        floats[1] = point.y;
        floats[2] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
        floats[3] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
        floats[4] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
        floats[5] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;

        float bgrectLeft;
        float bgRectRight;
        float textleftPos;
        //1. 粗略计算文字宽度
        float textWidth=mPaint.measureText(rateList.get(position) + "%");

        if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
            mPaint.setTextAlign(Paint.Align.LEFT);
            floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 + dip2px(extendLineWidth);

            bgrectLeft = floats[6] + mPaint.getStrokeWidth();
            bgRectRight = floats[6] +textWidth+ dip2px(14) + mPaint.getStrokeWidth();
            textleftPos = bgrectLeft + dip2px(7);
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);
            floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 - dip2px(extendLineWidth);

            bgRectRight = floats[6] - mPaint.getStrokeWidth();
            bgrectLeft = floats[6] - textWidth- dip2px(14) - mPaint.getStrokeWidth();

            textleftPos =bgRectRight - dip2px(7);
        }
        floats[7] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.parseColor("#C5C5C5"));
        mPaint.setStrokeWidth(dip2px(1));
        canvas.drawRoundRect(new RectF(bgrectLeft-dip2px(1), floats[7] - dip2px(showRateSize) / 2 - dip2px(5),
                bgRectRight+dip2px(1), floats[7] + dip2px(showRateSize) / 2 + dip2px(5)), dip2px(2), dip2px(2), mPaint);

        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(Color.parseColor("#4C4C4C"));

        canvas.drawRoundRect(new RectF(bgrectLeft, floats[7] - dip2px(showRateSize) / 2 - dip2px(4),
                bgRectRight, floats[7] + dip2px(showRateSize) / 2 + dip2px(4)), dip2px(2), dip2px(2), mPaint);
        mPaint.setColor(Color.parseColor("#D8D8D8"));
        canvas.drawLines(floats, mPaint);
        mPaint.setTextSize(dip2px(showRateSize));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mRes.getColor(R.color.white));
        canvas.drawText(rateList.get(position) + "%", textleftPos, floats[7] + dip2px(showRateSize) / 3, mPaint);

        preRate = rateList.get(position);
//        addView(getTextView(rateList.get(position)+""));

    }
    

参考文章:https://www.cnblogs.com/everhad/p/5809982.html
代码下载地址 https://download.csdn.net/download/muranfei/20929757

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值