Android自定义View 饼状图,扇形图

效果图
项目需求得整个扇形统计图,觉得使用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);
    }

里面主要以大圆为参照物,其他所在圆的半径都是以大圆为参照点,下图其他所在圆
辅助圆
红色为转折点所在圆,黑色为白点所在圆,他们之间的圆半径,都受大圆半径影响

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)

感谢:参考链接

源码

### 回答1: 要判断一个点是否在扇形内,可以按照以下步骤进行: 1. 首先确定扇形心和半径,以及扇形的起始角度和终止角度。 2. 计算出点与心的距离,如果该距离大于扇形半径,则该点在扇形外。 3. 计算出点与心的夹角,并将其转换为与扇形起始角度和终止角度的比较。 4. 如果该点的夹角大于等于扇形起始角度并且小于等于扇形终止角度,则该点在扇形内。 5. 如果该点的夹角小于扇形起始角度或者大于扇形终止角度,则该点在扇形外。 下面是一个示例代码: ```javascript function isPointInSector(x, y, centerX, centerY, radius, startAngle, endAngle) { // 计算出点与心的距离 var distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); if (distance > radius) { // 如果距离大于扇形半径,则点在扇形外 return false; } // 计算出点与心的夹角 var angle = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI; // 将角度转换为与扇形起始角度和终止角度的比较 angle = (angle < 0) ? (angle + 360) : angle; startAngle = (startAngle < 0) ? (startAngle + 360) : startAngle; endAngle = (endAngle < 0) ? (endAngle + 360) : endAngle; if (startAngle > endAngle) { endAngle += 360; } // 判断点是否在扇形内 return (angle >= startAngle && angle <= endAngle); } ``` 其中,x、y为点的坐标,centerX、centerY为扇形心坐标,radius为扇形半径,startAngle为扇形起始角度,endAngle为扇形终止角度。如果该函数返回true,则表示该点在扇形内,否则在扇形外。 ### 回答2: 在canvas中判断一个点是否在扇形内可以通过以下步骤来实现: 1. 获取鼠标点击或触摸事件的坐标(x, y)。 2. 计算扇形的中心点坐标(centerX, centerY)和半径(radius)。 3. 计算鼠标点击或触摸事件的坐标与扇形中心点坐标的距离,即distance = √((x - centerX)² + (y - centerY)²)。 4. 如果distance大于半径radius,则说明点在扇形外,返回false。 5. 如果distance小于等于半径radius,则说明点可能在扇形内,需要进一步判断。 6. 计算鼠标点击或触摸事件的坐标与扇形中心点坐标的夹角,即angle = atan2(y - centerY, x - centerX)。 7. 将夹角angle转换为[0, 2π)范围内的角度,即angle = (angle + 2π) % (2π)。 8. 计算扇形的起始角度(startAngle)和终止角度(endAngle)。 9. 如果startAngle大于endAngle,则将endAngle增加2π。 10. 判断如果angle大于等于startAngle且小于等于endAngle,则点在扇形内,返回true;否则,点在扇形外,返回false。 上述步骤中,使用了距离和夹角来判断点的位置关系。计算夹角时,需要使用Math.atan2()函数,它可以根据点的坐标差值计算夹角。使用Math.atan2()函数时,需要注意传入的参数顺序。其中,startAngle和endAngle是以弧度为单位的角度值,可以通过 Math.PI * (角度 / 180) 的方式进行转换。 以上是判断点是否在扇形内的一种方法,可以根据具体需求进行调整和优化。 ### 回答3: Canvas无法直接判断一个点是否在扇形内,但我们可以使用数学算法来解决这个问题。 要判断一个点是否在扇形内,我们可以按照以下步骤进行操作: 1. 确定扇形的中心点和半径。 2. 计算点到扇形中心的距离,可以使用勾股定理:d = sqrt((x - centerX)^2 + (y - centerY)^2),其中(x,y)为点的坐标,(centerX, centerY)为扇形中心。 3. 判断点到扇形中心的距离是否小于等于扇形的半径,如果大于半径,则点肯定在扇形外;如果小于等于半径,则进一步判断点位于扇形的角度范围内。 4. 根据点的坐标和扇形中心的坐标,计算点位于扇形的极角(也称为方位角或夹角)θ。可以使用反正切函数计算:θ = atan2(y - centerY, x - centerX)。 5. 根据扇形的起始角度(一般用弧度表示)和扇形的角度范围,判断点的极角是否在这个范围内。 - 如果起始角度加上角度范围大于等于2π(360度),则说明扇形是一个形,点肯定在扇形内。 - 如果起始角度加上角度范围小于2π,那么点的极角应该落在这个范围内,才能判断点在扇形内。 通过以上步骤,我们可以使用Canvas判断一个点是否在扇形内。请注意,在具体实现这个算法时,需要将角度值转换为弧度。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值