无论是自定义一个View还是ViewGroup我们必须得先为其制定在不同MeasureSpecMode下的大小,我这里就不讲解什么绘制源码了什么的,我们就直接实战。
我们在onMeasure中需要调用setMeasuredDimension(width,height)来为这个View确定宽高,调用这个方法确认完,我们就可以通过getMeasuredWidth和getMeasuredHeight来获取设置的这两个值了,而我们看过相关文章或者源码的童鞋们都知道,View的onMeasure默认实现是会处理 MeasureSpec.EXACTLY这种模式的,因为这种模式下代表宽高在XML或者代码中明确设置过了,或者是父View的大小。所以我们只需处理MeasureSpec.AT_MOST这种模式时,宽高是多少,因为这个模式代表了我们在XML设置了wrap_content,而包裹内容是多大,我们不知道,必须得自己明确的设置,你设置多大就是多大。一般的话,我们会根据绘制内容会绘制多大来确认这个View的大小,但是我们这种图表类的绘制内容要绘制的时候又需要根据控制的大小去设置一些变量,所以这样就产生了矛盾,所以我们这里的这个自定义View必须得先设置一个合适的宽高,我在这里就设置它为父View的2/3。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int resultWidth = widthSize;
int resultHeight = heightSize;
if(widthMode == MeasureSpec.AT_MOST){ //wrap_content的时候取父宽度的2/3
resultWidth = widthSize * 2/3;
}
if(heightMode == MeasureSpec.AT_MOST){//wrap_content的时候取父高度的2/3
resultHeight = heightSize * 2/3;
}
setMeasuredDimension(resultWidth,resultHeight);
initAfterMeasure(); //设置完宽高之后去为绘制的时候需要的变量赋值
}
接着去初始化绘制时需要用的一些变量
private void initAfterMeasure() {
//rectContent这个Rect对象,代表了我们自定义View时要绘制的区域,我们创建的时候需要在宽高的继承上减去padding,因为
//我们需要自己处理padding,父view不会帮我们处理的。padding之内才可以绘制,这样的话padding才会看起来是生效的
rectContent = new RectF(getPaddingLeft(),getPaddingTop(),getMeasuredWidth() - getPaddingRight(),getMeasuredHeight() - getPaddingBottom());
//要画的圆形的中心店
centerPoint = new PointF(rectContent.right/2,rectContent.bottom/2);
//完成度对应的角度,per是这个完成度,小数,由外部设置,比如说0.6f就是百分之60
sweepAngle = per * 360f;
//选择宽高中较小的一边作为圆的半径
chartRaduis = rectContent.right < rectContent.bottom ? rectContent.right : rectContent.bottom;
chartRaduis = chartRaduis/2;
//圆环的宽度为半径的五分之一
ringWidth = (int) (chartRaduis * 1 / 5);
//圆环中间字体的高度
allHeight = 0;
//计算字体的高度,这样才能让几行字居中显示
for (int i = 0; i < labelList.size(); i++) {
ChartLabel chartLabel = labelList.get(i);
paintText.setTextSize(chartLabel.getTextSize());
float fontHeight = FontUtil.getFontHeight(paintText);
allHeight += (fontHeight + labelSpace);
}
allHeight -= labelSpace;
}
因为的文章里,只写实战,不会去太多的讲解什么原理,或者API怎么使用,所以就不过多解释其他的了。
因为在上面 我们已经确认了View的大小,并且根据大小我们已经确认了中心点,圆形的半径,文字的起始位置,所以我们就可以愉快的在onDraw方法中进行绘制了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制背景
drawDefualt(canvas);
//绘制图表
if(!startDraw && startAnimator){ //如果是第一次onDraw就去启动动画,通过动画不断的重绘View
startDraw = true;
startAnimation();
}else { //实际去绘制View
drawChart(canvas);
}
}
我们看动态图应该可以发现,在进度环逐渐变多的时候,背景有一个灰色的环是不动的,一直存在的,所以我们先来绘制这个背景环:
private void drawDefualt(Canvas canvas) {
//设置圆环颜色,并且画一个chartRaduis半径的圆
paintChart.setColor(colorRingDef);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis,paintChart);
//设置为View原本的背景色(可以通过特殊方法获取,这里我们就使用白色),画一个 chartRaduis - ringWidth 半径的圆,
//这样子的话,中间就被盖住,只有灰色的ringWidth这么宽的圆环了。
paintChart.setColor(backColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis - ringWidth,paintChart);
}
去看一下启动动画:
private void startAnimation(){
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f); //从0到1 意思是从没有到原本设置的那个值
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animPro = (float) animation.getAnimatedValue(); //原来值的完成度
invalidate(); //重绘
}
});
valueAnimator.setDuration(animDuration);
valueAnimator.start();
}
这是个属性动画,从0到1 意味着完成度,假如原来设置的完成率是0.6f,那么动画就会导致他的值 从0.6f* 0 ~ 0.6f * 1,就是从无到有,不理解的同学,也可以直接将动画值设置成从0到设置的完成度。
来看一下实际的绘制内容:
private void drawChart(Canvas canvas) {
if(per == 0){//如果所占进度为0,则不用绘制彩色的圆弧了,只绘制中间的文字部分。
drawCenterText(canvas);
return;
}
if(!startAnimator){ // 如果不用开启动画,那么就直接使动画完成度为1,这样就是直接画设置的那个值
animPro = 1.0f;
}
//rectF是要画圆弧的那个矩形,我们知道要画一个圆形 必须现有一个矩形。
RectF rectF = new RectF(centerPoint.x - chartRaduis, centerPoint.y - chartRaduis, centerPoint.x + chartRaduis, centerPoint.y + chartRaduis);
paintChart.setColor(colorRing);
//画圆弧startAngle = -90,就是12点的位置,转过的角度是sweepAngle * 动画完成度。
canvas.drawArc(rectF,startAngle,sweepAngle*animPro,true,paintChart);
paintChart.setColor(backColor);
//画中心的白色圆,这样才能盖住所画的圆弧,呈现出圆环的效果
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaduis - ringWidth,paintChart);
//画中心的文字
drawCenterText(canvas);
}
通过上述的努力,我们就把圆环画完了,接着我们去画中间的文字:
private void drawCenterText(Canvas canvas) {
//算出文字的其实Y坐标
float top = centerPoint.y - allHeight/2;
//循环画每一行label
for (int i = 0; i < labelList.size(); i++) {
//chartLabel就是封装了每一行文字的颜色和字体大小,由外部来设置
ChartLabel chartLabel = labelList.get(i);
paintText.setColor(chartLabel.getTextColor());
paintText.setTextSize(chartLabel.getTextSize());
//将这个设置成center,我们就不需要求文字的x坐标了,直接用圆形的中点当做文字的中点
paintText.setTextAlign(Paint.Align.CENTER);
//如何求得文字的高,也有很多文章讲,不多说,只是封装成工具类
float textHeight = FontUtil.getFontHeight(paintText);
top += i * (textHeight + labelSpace);
//文字的y坐标是个难点,应该是top + Math.abs(paintText.getFontMetrics().top),至于为什么,也有文字讲,我们这里只讲实战
canvas.drawText(chartLabel.getText(),centerPoint.x,top + Math.abs(paintText.getFontMetrics().top),paintText);
}
}
这样的话,一个圆形进度条就画完了,至于如何去自定义属性,值的赋值等等这些无关紧要的大家自己看代码就可以了。
总结一下画这么一个圆形进度环用到了什么知识:
可以看到要画一个圆环是如何的简单,而确确实实的只用到了三个绘图的API,画圆形,画圆弧,画文字,所以我们不要再郁闷每次看文章学习了很多自定义View的API,却还是不知道能做什么,自定义不出好看的View。
源码:https://github.com/Ade-rui/ChartCodes/tree/master/progresschart
如果大家对上述用到的API不是很了解,可以去看如下的两篇大神写的系列文章:
启舰的自定义View系列
hencoder大神的自定义View系列
有这两个的系列文章就可以完全掌握自定义了。