为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈
GitHub 地址:https://github.com/leavesC/CustomView
也可以下载 Apk 来体验下:https://www.pgyer.com/CustomView
先看下效果图:
一、抽象概念
假设每个扇形所代表的数据的数据都是 float 类型的,这些数据需要由外部传入给 View,View 内部再来根据数据总量来计算各项数据的占比,各个扇形的角度就是以此来决定
为了简单起见,各个扇形的颜色值由 View 内部来决定,外部只需传入数据大小即可,将此概念抽象为 PercentageModel 类
/**
* 作者:leavesC
* 时间:2019/4/10 14:28
* 描述:
*/
public class PercentageModel {
private float value;
private float angle;
private int color;
}
二、确定宽高、初始化画笔
public PercentageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setDither(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = dp2px(DEFAULT_SIZE);
int width = getSize(widthMeasureSpec, defaultSize);
int height = getSize(heightMeasureSpec, defaultSize);
width = height = Math.min(width, height);
setMeasuredDimension(width, height);
Log.e(TAG, "onMeasure");
}
三、传入数据源
外部传入的数据只包含数据量 value 这个参数而已,因此还需要在 View 内部计算数据占比,并为数据项按照顺序赋予颜色值。为了避免精度损失,还需要在最后判断占比总和是否就是 360 度,不是的话则需要将损失值赋予最后一项数据
private List<PercentageModel> percentageModelList;
private static final int[] COLORS = {0xff2f7e76, 0xff1ff749, 0xfff42872, 0xff4643f4, 0xe51581da, 0xff8527e4, 0xfff1b00d, 0xff26020f};
public void setData(List<PercentageModel> percentageModelList) {
this.percentageModelList = percentageModelList;
initData(percentageModelList);
invalidate();
}
private void initData(List<PercentageModel> percentageModelList) {
if (percentageModelList == null || percentageModelList.size() == 0) {
return;
}
float sumValue = 0;
for (int i = 0; i < percentageModelList.size(); i++) {
PercentageModel percentageModel = percentageModelList.get(i);
sumValue += percentageModel.getValue();
percentageModel.setColor(COLORS[i % COLORS.length]);
}
float sumAngle = 0;
for (PercentageModel percentageModel : percentageModelList) {
float per = percentageModel.getValue() / sumValue;
percentageModel.setAngle(per * 360);
sumAngle += percentageModel.getAngle();
}
//计算百分比时可能有一些精度损失,此处需要判断是否需要把差值补回来
if (sumAngle < 360) {
for (PercentageModel percentageModel : percentageModelList) {
if (percentageModel.getAngle() != 0) {
percentageModel.setAngle(360 - sumAngle + percentageModel.getAngle());
break;
}
}
}
}
四、绘制
private RectF rect = new RectF();
@Override
protected void onDraw(Canvas canvas) {
if (percentageModelList == null || percentageModelList.size() == 0) {
return;
}
float currentStartAngle = startAngle;
canvas.translate(getWidth() / 2, getHeight() / 2);
float r = (float) (Math.min(getWidth(), getHeight()) / 2 * 0.95);
rect.left = -r;
rect.top = -r;
rect.right = r;
rect.bottom = r;
for (PercentageModel percentageModel : percentageModelList) {
paint.setColor(percentageModel.getColor());
canvas.drawArc(rect, currentStartAngle, percentageModel.getAngle(), true, paint);
currentStartAngle += percentageModel.getAngle();
}
}
通过全局变量 startAngle 来指定第一个扇形的起始角度,并将其 set 方法开放给外部,并在 set 方法内主动刷新 View
private float startAngle;
public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
invalidate();
}