iOS 扇形图 扇形图制作app_Android 自定义圆


项目需求得整个扇形统计图,觉得使用echars依赖感觉会有太多的冗余代码,可能个人对此有强迫症,保证apk安装包的大小,能自己实现的,使用率较高的就自己实现。功能点:

  • 显示百分比
  • 扇形圆环切换
  • 指定View的属性,大小

实现分析

  • 内部一个小圆遮挡构成圆环;
  • 外部大圆绘制多个扇形区域,扇形大小根据外部传入的百分比分割圆形;
  • 绘制折线,找到扇形所在弧的中心点,向外绘制线条;
  • 绘制文字;

1.确定view的宽和高,来决定圆的大小

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,height);
        }else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize,height);
        }
        //重新测量获取宽高
        width = getMeasuredWidth();
        height = getMeasuredHeight();

        dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);

        //确定圆心
        circleCenterX = width/2;
        circleCenterY = width/2;

        if(width>windowsWith){
            width = windowsWith;
        }
        height = width;

        ringOuterRadius = width/2 -paddingSize;

        ringPointRadius = ringOuterRadius*0.8f;//占最大圆的0.8


        ringInnerRadius = (width/2-paddingSize) *0.5f;//占最大圆的0.5

        //外圆折线点所在圆半径
        brokenRadius = width/2 - paddingSize+brokenMargin;

        // 外圆所在的矩形
        rectF = new RectF(paddingSize,
                paddingSize,
                width - paddingSize,
                width-paddingSize);
        // 点所在的矩形
        rectFPoint = new RectF(paddingSize+(ringOuterRadius - ringPointRadius),
                paddingSize+(ringOuterRadius - ringPointRadius),
                width-paddingSize-(ringOuterRadius - ringPointRadius),
                width-paddingSize-(ringOuterRadius - ringPointRadius));

        //外折点所在矩形
        rectFBrokenPoint=new RectF(width/2-brokenRadius,
                width/2-brokenRadius,
                width/2+brokenRadius,
                width/2+brokenRadius);
    }

里面主要以大圆为参照物,其他所在圆的半径都是以大圆为参照点,下图其他所在圆

iOS 扇形图 扇形图制作app_Android 自定义圆_02


红色为转折点所在圆,黑色为白点所在圆,他们之间的圆半径,都受大圆半径影响

2.开始绘制

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (colorList != null) {
            for (int i = 0; i < colorList.size(); i++) {
                mPaint.setColor(mRes.getColor(colorList.get(i)));
                mPaint.setStyle(Paint.Style.FILL);
                if (rateList != null) {
                    endAngle = getAngle(rateList.get(i));
                }
                //绘制扇形
                canvas.drawArc(rectF, preAngle, endAngle, true, mPaint);
                if (isShowRate) {
                    // 绘制百分比,折线
                    drawArcCenterPoint(canvas, i);
                }
                //绘制完一个,更新起始角度为上一个的结束角度
                preAngle = preAngle + endAngle;
            }
        }

        mPaint.setStyle(Paint.Style.FILL);
        if (isRing) {
            //绘制内部圆
            drawInner(canvas);
        }
    }

扇形绘制和内部圆的绘制没有什么大的难度,下面主要看看,折线的绘制和折线需要的坐标点

/**
     * // 绘制百分比,折线
     * @param canvas
     * @param position
     */
    private void drawArcCenterPoint(Canvas canvas, int position) {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dip2px(1));
        //白点集合
        dealPoint(rectFPoint, preAngle, (endAngle) / 2, pointList);
        //折线点集合
        dealPoint(rectFBrokenPoint,preAngle,(endAngle) / 2,outPointList);

        Point point = pointList.get(position);

        Point brokenPoint = outPointList.get(position);

        mPaint.setColor(mRes.getColor(R.color.white));
        //绘制白点
        canvas.drawCircle(point.x, point.y, whitePointRadius, mPaint);

        float[] floats = new float[8];
        floats[0] = point.x;
        floats[1] = point.y;
        floats[2] = brokenPoint.x;
        floats[3] = brokenPoint.y;
        floats[4] = brokenPoint.x;
        floats[5] = brokenPoint.y;

        if (point.x >= width/2) {
            mPaint.setTextAlign(Paint.Align.LEFT);//文字在右边
            floats[6] = brokenPoint.x + extendLineWidth;
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);//文字在左边
            floats[6] = brokenPoint.x - extendLineWidth;
        }
        floats[7] = brokenPoint.y;
        mPaint.setColor(mRes.getColor(colorList.get(position)));
        //绘制折线,根据坐标绘制
        canvas.drawLines(floats, mPaint);//{x1,y1,x2,y2,x3,y3,……}两两形成一条直线

        mPaint.setTextSize(showRateSize);
        mPaint.setStyle(Paint.Style.FILL);
        //绘制文字
        canvas.drawText(rateList.get(position) + "%", floats[6], floats[7] , mPaint);
    }

下面为折线点和白点集合的获取方法,参考网上的

private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
        Path path = new Path();
        //通过path,创建一个指定角度的圆弧
        path.addArc(rectF, startAngle, endAngle);
        //测量路径的长度
        PathMeasure measure = new PathMeasure(path, false);
        float[] pos = new float[]{0f, 0f};
        //利用PathMeasure分别测量出各个点的坐标值coords
        //第一个参数表示 0 到 measure.getLength() 之间的一个区间,所以这里取弧线终点坐标,保存pos数组
        measure.getPosTan(measure.getLength() /1, pos, null);
        Log.e("coords:", "x轴:" + pos[0] + " -- y轴:" + pos[1]);
        float x = pos[0];
        float y = pos[1];
        Point point = new Point(Math.round(x), Math.round(y));
        pointList.add(point);
    }

通过上面方法我们获得了所有白点和折点的所有坐标点。
绘制文字的时候,注意是在线条左边还是右边,根据下面属性判断

if (point.x >= width/2) {
            mPaint.setTextAlign(Paint.Align.LEFT);//文字在右边
            floats[6] = brokenPoint.x + extendLineWidth;
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);//文字在左边
            floats[6] = brokenPoint.x - extendLineWidth;
        }

3.全部代码

package com.szsh.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;

import androidx.annotation.Nullable;

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

public class RingView extends View {

    private int windowsWith;//屏幕宽
    private  int height = 200;//默认高
    private  int width = 200;//默认宽

    private Context mContext;
    private Paint mPaint;
    private float mPaintWidth = 0;        // 画笔的宽
    private Resources mRes;
    private float showRateSize = 14; // 展示文字的大小

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

    private float paddingSize = 90;//圆与View 的内边距,不是比例数据

    private float whitePointRadius = 2; //白点半径
    private float ringOuterRadius = 100;     // 外圆的半径
    private float ringInnerRadius = 64;     // 内圆的半径

    private float ringPointRadius = 80;    // 点所在圆的半径

    private float extendLineWidth = 20;     //点外延后  折的横线的长度
    private List<Point> pointList = new ArrayList<>(); //点的集合
    private List<Point> outPointList = new ArrayList<>();// 外折线点 的集合
    private float brokenRadius = 0;//外折线点的所在圆半径,一般大于外圆半径,小于视图with/2
    private float brokenMargin = 20;//折点距离最大圆距离
    private RectF rectF;                // 外圆所在的矩形
    private RectF rectFPoint;           // 点所在的矩形
    private RectF rectFBrokenPoint;     //外折点所在矩形

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

    private float preAngle = -135;//起始绘制位置 0度,水平顺时针开始
    private float endAngle = 0;
    private DisplayMetrics dm;


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

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

        initView(attrs);
    }

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,height);
        }else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize,height);
        }
        //重新测量获取宽高
        width = getMeasuredWidth();
        height = getMeasuredHeight();

        dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);

        //确定圆心
        circleCenterX = width/2;
        circleCenterY = width/2;

        if(width>windowsWith){
            width = windowsWith;
        }
        height = width;

        ringOuterRadius = width/2 -paddingSize;

        ringPointRadius = ringOuterRadius*0.8f;//占最大圆的0.8


        ringInnerRadius = (width/2-paddingSize) *0.5f;//占最大圆的0.5

        //外圆折线点所在圆半径
        brokenRadius = width/2 - paddingSize+brokenMargin;

        // 外圆所在的矩形
        rectF = new RectF(paddingSize,
                paddingSize,
                width - paddingSize,
                width-paddingSize);
        // 点所在的矩形
        rectFPoint = new RectF(paddingSize+(ringOuterRadius - ringPointRadius),
                paddingSize+(ringOuterRadius - ringPointRadius),
                width-paddingSize-(ringOuterRadius - ringPointRadius),
                width-paddingSize-(ringOuterRadius - ringPointRadius));

        //外折点所在矩形
        rectFBrokenPoint=new RectF(width/2-brokenRadius,
                width/2-brokenRadius,
                width/2+brokenRadius,
                width/2+brokenRadius);
    }


    private void initView(AttributeSet attrs) {
        TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.ringView);
        showRateSize =  a.getDimension(R.styleable.ringView_proTextSize,showRateSize);
        paddingSize = a.getDimension(R.styleable.ringView_paddingSize,paddingSize);
        brokenMargin = a.getDimension(R.styleable.ringView_brokenMargin,brokenMargin);
        whitePointRadius = a.getDimension(R.styleable.ringView_whitePointRadius,whitePointRadius);

        this.mRes = mContext.getResources();
        this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        DisplayMetrics dm = new DisplayMetrics();
        dm = mContext.getResources().getDisplayMetrics();
        windowsWith = dm.widthPixels;

        mPaint.setStrokeWidth(mPaintWidth);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);//抗锯齿
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (colorList != null) {
            for (int i = 0; i < colorList.size(); i++) {
                mPaint.setColor(mRes.getColor(colorList.get(i)));
                mPaint.setStyle(Paint.Style.FILL);
                if (rateList != null) {
                    endAngle = getAngle(rateList.get(i));
                }
                //绘制扇形
                canvas.drawArc(rectF, preAngle, endAngle, true, mPaint);
                if (isShowRate) {
                    // 绘制百分比,折线
                    drawArcCenterPoint(canvas, i);
                }
                //绘制完一个,更新起始角度为上一个的结束角度
                preAngle = preAngle + endAngle;
            }
        }

        mPaint.setStyle(Paint.Style.FILL);
        if (isRing) {
            //绘制内部圆
            drawInner(canvas);
        }
    }

    private void drawInner(Canvas canvas) {
        mPaint.setColor(mRes.getColor(R.color.white));
        canvas.drawCircle(circleCenterX, circleCenterY , ringInnerRadius, mPaint);

        //外圆折线点所在圆
        mPaint.setColor(mRes.getColor(R.color.main_red));
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(circleCenterX, circleCenterY , brokenRadius, mPaint);

        //白点所在圆
        mPaint.setColor(mRes.getColor(R.color.main_black));
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(circleCenterX, circleCenterY , ringPointRadius, mPaint);
    }

    /**
     * // 绘制百分比,折线
     * @param canvas
     * @param position
     */
    private void drawArcCenterPoint(Canvas canvas, int position) {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dip2px(1));
        //白点集合
        dealPoint(rectFPoint, preAngle, (endAngle) / 2, pointList);
        //折线点集合
        dealPoint(rectFBrokenPoint,preAngle,(endAngle) / 2,outPointList);

        Point point = pointList.get(position);

        Point brokenPoint = outPointList.get(position);

        mPaint.setColor(mRes.getColor(R.color.white));
        //绘制白点
        canvas.drawCircle(point.x, point.y, whitePointRadius, mPaint);

        float[] floats = new float[8];
        floats[0] = point.x;
        floats[1] = point.y;
        floats[2] = brokenPoint.x;
        floats[3] = brokenPoint.y;
        floats[4] = brokenPoint.x;
        floats[5] = brokenPoint.y;

        if (point.x >= width/2) {
            mPaint.setTextAlign(Paint.Align.LEFT);//文字在右边
            floats[6] = brokenPoint.x + extendLineWidth;
        } else {
            mPaint.setTextAlign(Paint.Align.RIGHT);//文字在左边
            floats[6] = brokenPoint.x - extendLineWidth;
        }
        floats[7] = brokenPoint.y;
        mPaint.setColor(mRes.getColor(colorList.get(position)));
        //绘制折线
        canvas.drawLines(floats, mPaint);//{x1,y1,x2,y2,x3,y3,……}两两形成一条直线

        mPaint.setTextSize(showRateSize);
        mPaint.setStyle(Paint.Style.FILL);
        //绘制文字
        canvas.drawText(rateList.get(position) + "%", floats[6], floats[7] , mPaint);
    }



    private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
        Path path = new Path();
        //通过path,创建一个指定角度的圆弧
        path.addArc(rectF, startAngle, endAngle);
        //测量路径的长度
        PathMeasure measure = new PathMeasure(path, false);
        float[] pos = new float[]{0f, 0f};
        //利用PathMeasure分别测量出各个点的坐标值coords
        //第一个参数表示 0 到 measure.getLength() 之间的一个区间,所以这里取弧线终点坐标,保存pos数组
        measure.getPosTan(measure.getLength() /1, pos, null);
        Log.e("coords:", "x轴:" + pos[0] + " -- y轴:" + pos[1]);
        float x = pos[0];
        float y = pos[1];
        Point point = new Point(Math.round(x), Math.round(y));
        pointList.add(point);
    }

    /**
     * @param percent 百分比
     * @return
     */
    private float getAngle(float percent) {
        //实际使用过程中是按照int算的百分比会有精度丢失,这里多加一度,解决绘制出现空隙的问题
        float a = 361f / 100f * 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 version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="ringView">
        <attr name="proTextSize" format="dimension"></attr><!--文字大小-->
        <attr name="paddingSize" format="dimension"></attr><!--最大圆与视图内边距-->
        <attr name="brokenMargin" format="dimension"></attr> <!--折点距离最大圆距离-->
        <attr name="whitePointRadius" format="dimension"></attr><!--白点半径-->
    </declare-styleable>


</resources>

使用

<com.szsh.myapplication.RingView
                        android:layout_width="260dp"
                        android:layout_height="260dp"
                        app:proTextSize = "12sp"
                        app:paddingSize = "68dp"
                        app:brokenMargin = "10dp"
                        app:whitePointRadius = "1dp"
                        android:id="@+id/ringView"
                        app:layout_constraintLeft_toLeftOf="parent"
                        app:layout_constraintTop_toTopOf="parent"
                        app:layout_constraintBottom_toBottomOf="parent"
                        />
// 添加的是颜色
        val colorList: MutableList<Int> = ArrayList()
        colorList.add(R.color.main_color)
        colorList.add(R.color.main_accent)
        colorList.add(R.color.main_red)

        //  添加的是百分比
        val rateList: MutableList<Float> = ArrayList()
        rateList.add(33.3f)
        rateList.add(33.3f)
        rateList.add(33.3f)
        ringView.setShow(colorList, rateList, true, true)