效果图 这是今天要做的效果,没图没真相: 需求分析 首先大方向分成两个:选中/未选中状态。未选中状态很简单,静态的,画一个空心圆,一个小钩就可以了,小钩可以用Path来实现。下面主要说说动态的选中状态。 绘制弧线:这是一个动态的过程,所以是不断重绘
效果图
这是今天要做的效果,没图没真相:

需求分析
首先大方向分成两个:选中/未选中状态。未选中状态很简单,静态的,画一个空心圆,一个小钩就可以了,小钩可以用Path来实现。下面主要说说动态的选中状态。
-
绘制弧线:这是一个动态的过程,所以是不断重绘,并且不断增大弧线扫过的角度,直至360°。
-
变小的白色圆:当弧线扫满360°,在一个彩色实心圆的背景下,有一个半径不断变小的白色的圆。所以实现的方式是,先绘制一个彩色实心圆,然后再绘制白色圆,当然还是通过不断重绘实现动画效果,在重绘的同时白色圆的半径不断变小。
-
彩色变大的圆和小钩:在白色圆的半径减小到零之后,绘制彩色变大的圆,动画效果还是通过不断重绘来实现的。在不断重绘的过程中,将彩色圆半径一点点变大。绘制圆之后,绘制小钩,小钩的实现和未选中状态一致,通过Path即可实现。
-
彩色变小的圆和小钩:当前面那个阶段的圆扩大到一定程度(程度由你来决定),开始绘制彩色圆缩小回初始尺寸的效果。实现方式和前一步类似,只不过把扩大的半径改为缩小的。
选中状态绘制流程设计
需求分析里面说了那么多“废话”,还是来张图更清晰些。

拆解需求,按步骤实现代码
让我们拆解需求,一步步地写出代码。
区分选中和未选中状态
首先当然是区分大方向,用一个变量来标记即可,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(isCheck){
drawChecked(canvas);
}else{
drawUnChecked(canvas);
}
}
接下来开始看看选中状态的绘制流程,也就是drawChecked()方法。
移动坐标系
由于绘制的主要是圆(包括弧线),所以觉得坐标系移到中间比较方便,所以在所有绘制的开始,先将坐标原点移动到View中央:
canvas.save();
canvas.translate(halfWidth, halfHeight);
绘制彩色弧线
如前面的流程所述,需要绘制一个扫过角度不断变大的弧线,所以要这样子做:
if(sweepAnglesCounter < MAX_SWEEP_ANGLES){
sweepAnglesCounter += 12;
}
canvas.drawArc(-radius, -radius, radius, radius, START_ANGLES, sweepAnglesCounter, false, checkedPaint);
绘制彩色圆以及白色变小的圆
如前面的流程图所示,这里需要先绘制一个彩色的圆,再绘制一个白色的圆,且白色圆的半径逐渐变小。来看代码:
if(sweepAnglesCounter == MAX_SWEEP_ANGLES){
checkedPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0, 0, radius, checkedPaint);
if(whiteRadiusCounter >= 20){
whiteRadiusCounter -= 20;
}
whitePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0, 0, whiteRadiusCounter, whitePaint);
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
绘制彩色扩大的圆以及小钩
嗯这里我们需要一个彩色圆不断变大的半径计数,彩色圆的半径上限,还有描述小钩路径的Path对象。所以需要这样写:
if(whiteRadiusCounter < 20){
whitePaint.setStyle(Paint.Style.STROKE);
if(expandRadiusCounter < maxExpandRadius){
expandRadiusCounter += 20;
canvas.drawCircle(0, 0, expandRadiusCounter, checkedPaint);
canvas.drawPath(tickPath, whitePaint);
}
}
绘制彩色变小的圆以及小钩
逻辑和前面一步差不多,只不过是计数方式反过来了,不多说了,看代码:
if(expandRadiusCounter == maxExpandRadius){
if(narrowRadiusCounter >= radius) {
narrowRadiusCounter -= 20;
canvas.drawCircle(0, 0, narrowRadiusCounter, checkedPaint);
canvas.drawPath(tickPath, whitePaint);
}
}
恢复坐标系,重置计数器
动态绘制完成了,当然是要把东西还原回去,像这样子:
canvas.restore();
if(narrowRadiusCounter >= radius){
invalidate();
} else {
reset();
}
未选中状态的静态效果
这个效果比较简单,是静态的,几行代码就搞定了:
private void drawUnChecked(Canvas canvas){
canvas.save();
canvas.translate(halfWidth, halfHeight);
canvas.drawCircle(0, 0, radius, unCheckedPaint);
canvas.drawPath(tickPath, unCheckedPaint);
canvas.restore();
}
添加xml属性
其实关于绘制的过程,已经讲完了,不过一个完整的自定义View,应该支持xml属性,那我们就写几个来意思一下。首先在res/values/路径下面,新建attrs.xml文件,然后写入我们想要支持的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TickView">
<attr name="checked_color" format="color"/>
<attr name="checked" format="boolean"/>
<attr name="radius" format="dimension"/>
</declare-styleable>
</resources>
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
然后在构造方法里读取并设置这些属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TickView);
checkedPaint.setColor(typedArray.getColor(R.styleable.TickView_checked_color, DEFAULT_CHECKED_COLOR));
isCheck = typedArray.getBoolean(R.styleable.TickView_checked, false);
radius = (int)typedArray.getDimension(R.styleable.TickView_radius, DEFAULT_RADIUS);
typedArray.recycle();
暴露一些控制接口
看我们前面贴的效果图,点击按钮可以改变选中效果,所以肯定是有提供控制接口,也很简单,直接看代码:
public void setCheck(boolean check) {
isCheck = check;
reset();
invalidate();
}
关于测量——重写onMeasure()方法
主要是为了支持wrap_content属性,总不能总是占满全屏,或者迫使调用者写个固定尺寸。那么这个默认尺寸该怎么设计呢?很简单,彩色圆变大的时候,有一个上限半径,这个就可以作为默认尺寸。不过我们在xml文件里面支持了圆形扩大前的半径,如果用户设置了该怎么办呢?只要在两者之间做一个简单计算就可以了,像这样子:
maxExpandRadius = radius + 60;
那么现在onMeasure()方法就可以这样写了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
}
private int getMeasureSize(int measureSpec){
int modeSpec = MeasureSpec.getMode(measureSpec);
int sizeSpec = MeasureSpec.getSize(measureSpec);
int result;
if(modeSpec == MeasureSpec.EXACTLY){
result = sizeSpec;
} else {
result = maxExpandRadius<<1;
if(modeSpec == MeasureSpec.AT_MOST){
result = Math.min(sizeSpec, result);
}
}
return result;
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
小结
总体来说是一个不复杂的自定义View,非常适合新手尝试绘制动画效果。其中主要注意两点:
-
区分选中和未选中状态,一个是静态效果(不需要反复绘制),一个是动态效果(需要不断重绘)。这两个效果建议写在两个不同方法里面,不要扎堆地写在onDraw()方法里面。
-
在重绘的过程里,通过计数器判断处于哪个绘制阶段,并且在动态效果绘制结束后,注意重置计数器值,以避免一些bug。
Android自定义View分享 打钩动画
转载https://www.codesocang.com/appboke/38740.html