本着无图言x的原则,所以先上效果:
很简单!也很常见!
所以这样子比较适合入门,因为这个是自定义单个控件,所以只会用到ondraw()、onmeasure()两个方法,所以面试的时候不要傻傻的说自定义view要重写onlayout(),要分情况的!
下面我们一步一步来:
继承view,重写构造方法,至于重写那个构造方法,要看情况咯,CircleView(Context context) 一个参数构造 一般是用于Java代码对象view的创建,CircleView(Context context, AttributeSet attrs) 两个参数构造用于布局声明,但也有1~3构造全部写的,我觉得没必要
自定义view一般都会用到画笔,毕竟图是我们自己实现的,随意我们需要在构造里初始化画笔,当然还有画笔的还需要修饰,比如:颜色、大小、样式等等
接着就是要在画布上画出我们想要的东西了,画布在我们的ondraw()里面,至于里面的逻辑都不是很难,简单的数据计算
当然这时候效果已经出来了,但还不尽人意,比如:你会发现你的布局文件设置宽高为wrap_content和match_parent没有区别,这些问题,我们接下来就会在码代码的时候讲
我先贴下代码,没有onmeasure()哦!
public class CircleView extends View {
private float sweepAngle;//圆弧经过的角度
private Paint rPaint;//矩形的画笔
private Paint progressPaint;//圆弧的画笔
private Paint textPaint;//进度画笔
private int precent = 0;//更新百分比
CircleAnim anim;//内部动画
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
anim = new CircleAnim();
}
private void init(Context context, AttributeSet attrs) {
rPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rPaint.setStyle(Paint.Style.STROKE);//不填充
rPaint.setColor(Color.GRAY);
rPaint.setStrokeWidth(15);
progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
progressPaint.setStyle(Paint.Style.STROKE);//不填充
progressPaint.setColor(Color.RED);
progressPaint.setStrokeWidth(15);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setColor(Color.BLACK);
textPaint.setFakeBoldText(true);//设置粗体
textPaint.setTextSize(40);
textPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int min = Math.min(getWidth(), getHeight());
int centre = min / 2; // 获取圆心的x坐标
int radius = centre - 15 / 2;// 半径
RectF rectF = new RectF(centre - radius, centre - radius, centre + radius, centre + radius);
canvas.drawArc(rectF, 0, 360, false, rPaint);
canvas.drawArc(rectF, 0, sweepAngle, false, progressPaint);//这里角度0对应的是三点钟方向,顺时针方向递增
canvas.drawText(precent + "%", centre, centre, textPaint);
}
public class CircleAnim extends Animation {
public CircleAnim() {
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
sweepAngle = interpolatedTime * 360;
precent = (int) (interpolatedTime * 100);
invalidate();
}
}
//设置动画时间
public void setProgressNum(int time) {
anim.setDuration(time);
this.startAnimation(anim);
}
}
ondraw()方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int min = Math.min(getWidth(), getHeight());
int centre = min / 2; // 获取圆心的x坐标
int radius = centre - 15 / 2;// 半径
RectF rectF = new RectF(centre - radius, centre - radius, centre + radius, centre + radius);
canvas.drawArc(rectF, 0, 360, false, rPaint);
canvas.drawArc(rectF, 0, sweepAngle, false, progressPaint);//这里角度0对应的是三点钟方向,顺时针方向递增
canvas.drawText(precent + "%", centre, centre, textPaint);
}
- 获取view的宽高,进而获取圆弧的的中心坐标,为什么会取宽高的最小值呢?如:w:150 h:100 看下效果图:
下面一部分怎么少了呢? 我们的圆弧半径是取值于宽高的,如果宽度大于高度,而又用宽度来做半径的取值标准,就会导致这样效果,因为宽度是150,也就是半径是75,但高度只有100,那肯定会有一部分绘制不出来的;但如果我们按最小的算就不一样了,怎么算都够,高度100作为取值标准,半径就是50,而宽度是150,所以肯定够你绘制,这就是我们取宽、高最小值的原因。
- 接着就是画出矩形、圆弧、文本了。
动画:
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
sweepAngle = interpolatedTime * 360;
precent = (int) (interpolatedTime * 100);
invalidate();
}
- 主要是这个方法第一个参数的值是0.0~1.0 ;不敢乱翻译。所以你们可以看下源码解释:
/**
* Helper for getTransformation. Subclasses should implement this to apply
* their transforms given an interpolation value. Implementations of this
* method should always replace the specified Transformation or document
* they are doing otherwise.
*
* @param interpolatedTime The value of the normalized time (0.0 to 1.0)
* after it has been run through the interpolation function.
* @param t The Transformation object to fill in with the current
* transforms.
*/
protected void applyTransformation(float interpolatedTime, Transformation t) {
}
主要是通过invalidate(); 来刷新界面(注意:invalidate()和postInvalidate()的区别,后者可以在子线程刷新ui),你启动这个动画后,applyTransformation()方法会不断的执行,直到interpolatedTime 变为1.0,同时,invalidate() 会不停的调用ondraw(),这就实现了连贯的更新动画
这样基本没什么问题了,但。。。真的没问题了吗?
我们把代码改成这样:
<com.example.administrator.kotlinapp.CircleView
android:id="@+id/circleview_100"
android:layout_marginTop="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
效果图:
纳尼???wrap_content和match_parent效果一样有没有。
所以我们就需要自己测量咯
onmeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measurewidth = 400;
int measureheight = 400;
measurewidth = resolveSize(measurewidth, widthMeasureSpec);
measureheight = resolveSize(measureheight, heightMeasureSpec);
setMeasuredDimension(measurewidth, measureheight);
}
- resolveSize()是不是很少见?但你一定经常见这样代码段:
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
没错这就是resolveSize()的源码,以前我们都是自己写这样的逻辑
- MeasureSpec.UNSPECIFIED: 不限制,一般我们会给一个默认值
- MeasureSpec.EXACTLY:有上限,不能超过限制
- MeasureSpec.AT_MOST:限制固定尺寸
ok,各种模式已经适配完了,这样就支持wrap_content、match_parent、固定值了。
最后附上 demo: