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