Android 圆环统计图(带外延折线可点击)

需求先看UI效果图吧
Android 圆环统计图(带外延折线可点击)_第1张图片
看到这肯定去找轮子,找了半天,没找到相似的,大部分搜到的都是点击外凸,而这个UI是内凸,其实外凸内凸区别还不小,没找到一样的,于是乎,和iOS说好了要不就放弃吧,然而第二天,人家夸夸夸撸完了,还是贝塞尔写的,当场晕死,我是真看不懂那公式,然后不甘心,拼命的改别人的轮子,就算不用贝塞尔,也得搞出来啊,于是乎,套两个圆环不就好了,于是,开始下手干活,下面贴出来iOS和Android的效果,左iOS右Android
Android 圆环统计图(带外延折线可点击)_第2张图片 Android 圆环统计图(带外延折线可点击)_第3张图片
虽然没有人家漂亮,但是基本也实现了,代码贴在下面了

辅助工具类GeomTool.class

import android.graphics.Point;
import android.graphics.RectF;

/**
 * 与2D屏幕有关的计算,屏幕约定为X轴向右,Y轴向下,顺时针角度增加。
 * Created by hxw on 2016/8/25.
 */
public  class GeomTool {

    /**
     * 这个方法放在这里展示了原始的计算过程
     *
     * @see #calcCirclePoint
     */
    @Deprecated
    private static Point calcCirclePoint2(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 x = 0, y = 0;

        if (angle == 0 || angle == 360) {
            // sin:0 cos: 1
            x = cx + radius;
            y = cy;
        } else if (angle > 0 && angle < 90) {
            // sin:0~1 cos: 1~0
            double dy = radius * sin;
            double dx = radius * cos;
            x = cx + dx;
            y = cy + dy;
        } else if (angle == 90) {
            // sin:1 cos: 0
            x = cx;
            y = cy + radius;
        } else if (angle > 90 && angle < 180) {
            // sin:1~0 cos: 0~-1
            double dy = radius * sin;
            double dx = radius * cos;
            x = cx + dx;
            y = cy + dy;
        } else if (angle == 180) {
            // sin:0 cos: -1
            x = cx - radius;
            y = cy;
        } else if (angle > 180 && angle < 270) {
            // sin:0~-1 cos: -1~0
            double dy = radius * sin;
            double dx = radius * cos;
            x = cx + dx;
            y = cy + dy;
        }  else if (angle == 270) {
            // sin:-1 cos: 0
            x = cx;
            y = cy - radius;
        } else if (angle > 270 && angle < 360) {
            // sin:-1~0 cos: 0~1
            double dy = radius * sin;
            double dx = radius * cos;
            x = cx + dx;
            y = cy + dy;
        }

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

    /**
     * 计算指定角度、圆心、半径时,对应圆周上的点。
     * @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;
    }

    /**
     * 计算坐标(x, y)到圆心(cx, cy)形成的角度,角度从0-360,360度就是0度,顺时针增加
     * (x轴向右,y轴向下)若2点重合返回-1;
     */
    public static int calcAngle(float x, float y, float cx, float cy) {
        double resultDegree = 0;

        double vectorX = x - cx; // 点到圆心的X轴向量,X轴向右,向量为(0, vectorX)
        double vectorY = cy - y; // 点到圆心的Y轴向量,Y轴向上,向量为(0, vectorY)
        if (vectorX == 0 && vectorY == 0) {
            // 重合?
            return -1;
        }
        // 点落在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;
    }

    /**
     * 计算指定区域中可放置的最大正方形区域。
     * @param region 指定的区域
     * @param squareRect 正方形区域,将在原区域中居中
     * @return squareRect, or new RectF if squareRect is null.
     */
    public static RectF calcMaxSquareRect(RectF region, RectF squareRect) {
        if (squareRect == null) squareRect = new RectF();
        if (region == null) return squareRect;

        float w = region.width();
        float h = region.height();
        if (w == h) {
            squareRect.set(region);
        } else if (w > h) {
            float padding = (w - h) / 2;
            squareRect.set(region);
            squareRect.inset(padding, 0);
        } else { // (w < h)
            float padding = (h - w) / 2;
            squareRect.set(region);
            squareRect.inset(0, padding);
        }
        return squareRect;
    }

    /**
     * 将角度变换为0-360度。
     * @param angle 原角度
     * @return 0-360之间的等效角度
     */
    public static int clampAngle(int angle) {
        return ((angle % 360) + 360) % 360;
    }

    /**
     * 返回给定值在区间[min, max]上的最近值。
     */
    public static float clamp(float value, float min, float max) {
        if (min > max) {
            min = min + max;
            max = min - max;
            min = min - max;
        }
        if (value < min) {
            return min;
        } else if (value > max) {
            return max;
        }
        return value;
    }

    /**
     * 计算点(x1, y1)和(x2, y2)之间的距离。
     */
    public static float calcDistance(float x1, float y1, float x2, float y2) {
        return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }

}

自定义View RingView.class


import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

import com.dq.demo.R;

import java.util.ArrayList;
import java.util.List;

public class RingView extends View {

    private Context mContext;
    private Paint mPaint;
    private int mPaintWidth = 0;        // 画笔的宽
    private int topMargin = 30;         // 上边距
    private int leftMargin = 80;        // 左边距
    private Resources mRes;
    private DisplayMetrics dm;
    private int showRateSize = 12; // 展示文字的大小

    private int circleCenterX = 96;     // 圆心点X  要与外圆半径相等
    private int circleCenterY = 96;     // 圆心点Y  要与外圆半径相等

    private int ringOuterRidus = 96;     // 外圆的半径
    private int ringPointRidus = 80;    // 点所在圆的半径

    private float rate = 0.4f;     //点的外延距离  与  点所在圆半径的长度比率
    private float extendLineWidth = 80;     //点外延后  折的横线的长度

    private RectF rectF;                // 外圆所在的矩形
    private RectF rectFPoint;           // 点所在的矩形

    private List<Integer> colorList;
    private List<Float> rateList;
    private boolean isShowRate;

    private Float maxTotal = 0F;

    private String lengthenLineColor = "#3A66AF";
    private String lengthenTextColor = "#455B72";

    public RingView(Context context) {
        super(context, null);
    }

    public RingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        initView();
    }

    public void setShow(List<Integer> colorList, List<Float> rateList) {
        setShow(colorList, rateList, false);
    }

    public void setShow(List<Integer> colorList, List<Float> rateList, boolean isShowRate) {
        this.colorList = colorList;
        this.rateList = rateList;
        this.isShowRate = isShowRate;

        angles = new float[rateList.size()];
        for (int i = 0; i < rateList.size(); i++) {
            maxTotal += rateList.get(i);
        }

        for (int j = 0; j < rateList.size(); j++) {
            angles[j] = (float) ((rateList.get(j) / maxTotal) * 360f);
        }

    }

    private void initView() {
        this.mRes = mContext.getResources();
        this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);
        int screenWidth = wm.getDefaultDisplay().getWidth();
        leftMargin = (px2dip(screenWidth) - (2 * circleCenterX)) / 2;
        mPaint.setColor(getResources().getColor(R.color.red));
        mPaint.setStrokeWidth(dip2px(mPaintWidth));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        rectF = new RectF(dip2px(mPaintWidth + leftMargin),
                dip2px(mPaintWidth + topMargin),
                dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2 + leftMargin),
                dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2 + topMargin));
        rectFPoint = new RectF(dip2px(mPaintWidth + leftMargin + (ringOuterRidus - ringPointRidus)),
                dip2px(mPaintWidth + topMargin + (ringOuterRidus - ringPointRidus)),
                dip2px(circleCenterX + ringPointRidus + mPaintWidth * 2 + leftMargin),
                dip2px(circleCenterY + ringPointRidus + mPaintWidth * 2 + topMargin));
        Log.e("矩形点:", dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2) + " --- " + dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2));

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        pointList.clear();
        if (colorList != null) {
            for (int i = 0; i < colorList.size(); i++) {
                mPaint.setColor(mRes.getColor(colorList.get(i)));
                mPaint.setStyle(Paint.Style.FILL);
                drawOuter(canvas, i);
            }
        }

        if (colorList != null) {
            for (int i = 0; i < colorList.size(); i++) {
                drawInouter(canvas, i);
            }
        }

        mPaint.setStyle(Paint.Style.FILL);

        Paint paint1 = new Paint();
        paint1.setColor(Color.parseColor("#A7A6FF"));
        canvas.drawCircle(rectF.centerX(), rectF.centerY(), 150, paint1);

    }

    private float preRate;

    private void drawArcCenterPoint(Canvas canvas, int position) {
        mPaint.setStyle(Paint.Style.FILL);
        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(Color.parseColor(lengthenLineColor));

        if (position == mCurrentItem) {
            /**
             * 折线初始圆点
             */
            canvas.drawCircle(point.x, point.y, dip2px(2), mPaint);
        }

        if (preRate / 2 + rateList.get(position) / 2 < 5) {
            extendLineWidth += 40;
            rate -= 0.05f;
        } else {
            extendLineWidth = 40;
            rate = 0.4f;
        }

        // 外延画折线
        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;
        if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
            mPaint.setTextAlign(Paint.Align.LEFT);
            floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 + dip2px(extendLineWidth);
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);
            floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 - dip2px(extendLineWidth);
        }
        floats[7] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;


        mPaint.setColor(Color.parseColor(lengthenLineColor));
        if (position == mCurrentItem) {
            /**
             * 折线线段
             */
            canvas.drawLines(floats, mPaint);
        }
        mPaint.setTextSize(dip2px(showRateSize));

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.parseColor(lengthenTextColor));

        if (position == mCurrentItem) {
            /**
             * 文字渲染
             */
            if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
                canvas.drawText(rateList.get(position) + "%", floats[6] - dip2px(extendLineWidth), floats[7] - dip2px(showRateSize) / 3, mPaint);
            } else {
                canvas.drawText(rateList.get(position) + "%", floats[6] + dip2px(extendLineWidth), floats[7] - dip2px(showRateSize) / 3, mPaint);
            }
        }

        preRate = rateList.get(position);
    }

    List<Point> pointList = new ArrayList<>();
    List<Point> pointArcCenterList = new ArrayList<>();

    private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
        Path orbit = new Path();
        //通过Path类画一个90度(180—270)的内切圆弧路径
        orbit.addArc(rectF, startAngle, endAngle);
        PathMeasure measure = new PathMeasure(orbit, false);
        Log.e("路径的测量长度:", "" + measure.getLength());
        float[] coords = new float[]{0f, 0f};
        int divisor = 1;
        measure.getPosTan(measure.getLength() / divisor, coords, null);
        Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]);
        float x = coords[0];
        float y = coords[1];
        Point point = new Point(Math.round(x), Math.round(y));
        pointList.add(point);
    }

    private void drawOuter(Canvas canvas, int position) {
        if (rateList != null) {
            endAngle = getAngle(rateList.get(position));
        }
        canvas.drawArc(rectF, preAngle, endAngle, true, mPaint);

        if (isShowRate) {
            /**
             * 绘制折线外延
             */
            drawArcCenterPoint(canvas, position);
        }

        preAngle = preAngle + endAngle;
    }

    /**
     * 绘制内圆环
     */
    private void drawInouter(Canvas canvas, int position) {
        if (rateList != null) {
            endAngle = getAngle(rateList.get(position));
        }

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(getResources().getColor(R.color.white));
        paint.setStyle(Paint.Style.FILL);

        RectF rectF1 = new RectF(dip2px(mPaintWidth + 130),                                       //左
                dip2px(mPaintWidth + 60),                                       //上
                dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2 + 70),                    //右
                dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2 + 0)); //下

        if (mCurrentItem != position) {
            canvas.drawArc(rectF1, preAngle, endAngle, true, paint);
        }

        preAngle = preAngle + endAngle;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            int item = calcClickItem(event.getX(), event.getY());
            if (item >= 0 && item < rateList.size()) {
                setCurrentItem(item);
            } else {
                mCurrentItem = -1;
                invalidate();
            }
        }
        return super.onTouchEvent(event);
    }

    private int mCurrentItem = -1;
    public float[] angles;

    private void setCurrentItem(int item) {
        Log.e(RingView.class.getSimpleName(), "Click To Position " + item);
        if (mCurrentItem != item) {
            mCurrentItem = item;
        }
        invalidate();
    }

    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;
    }

    private float preAngle = -90;
    private float endAngle = -90;

    /**
     * @param percent 百分比
     * @return
     */
    private float getAngle(float percent) {
        float a = 360f / maxTotal * percent;
        return a;
    }

    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public int dip2px(float dpValue) {
        return (int) (dpValue * dm.density + 0.5f);
    }

    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public int px2dip(float pxValue) {
        return (int) (pxValue / dm.density + 0.5f);
    }
}

xml中引入布局

<com.dq.demo.ui.view.RingView
    android:id="@+id/ringView"
    android:layout_marginTop="300dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

activity中使用

RingView ringView = (RingView) mView.findViewById(R.id.ringView);
// 添加的是颜色
List<Integer> colorList = new ArrayList<>();
colorList.add(R.color.color_ff3e60);
colorList.add(R.color.color_ffa200);
colorList.add(R.color.color_31cc64);
colorList.add(R.color.yellow_2);
colorList.add(R.color.grey_600);
colorList.add(R.color.text_top_1);
//  添加的是百分比
List<Float> rateList = new ArrayList<>();
rateList.add(10f);
rateList.add(15f);
rateList.add(25f);
rateList.add(40f);
rateList.add(30f);
rateList.add(28f);
ringView.setShow(colorList, rateList, true);

至此结束!!!
如有更好的方法,欢迎在评论区讨论。

你可能感兴趣的:(Android,android)