效果图
这是今天要做的效果,没图没真相:
需求分析
首先大方向分成两个:选中/未选中状态。未选中状态很简单,静态的,画一个空心圆,一个小钩就可以了,小钩可以用Path来实现。下面主要说说动态的选中状态。
绘制弧线:这是一个动态的过程,所以是不断重绘,并且不断增大弧线扫过的角度,直至360°。
变小的白色圆:当弧线扫满360°,在一个彩色实心圆的背景下,有一个半径不断变小的白色的圆。所以实现的方式是,先绘制一个彩色实心圆,然后再绘制白色圆,当然还是通过不断重绘实现动画效果,在重绘的同时白色圆的半径不断变小。
彩色变大的圆和小钩:在白色圆的半径减小到零之后,绘制彩色变大的圆,动画效果还是通过不断重绘来实现的。在不断重绘的过程中,将彩色圆半径一点点变大。绘制圆之后,绘制小钩,小钩的实现和未选中状态一致,通过Path即可实现。
彩色变小的圆和小钩:当前面那个阶段的圆扩大到一定程度(程度由你来决定),开始绘制彩色圆缩小回初始尺寸的效果。实现方式和前一步类似,只不过把扩大的半径改为缩小的。
选中状态绘制流程设计
需求分析里面说了那么多“废话”,还是来张图更清晰些。
拆解需求,按步骤实现代码
让我们拆解需求,一步步地写出代码。
区分选中和未选中状态
首先当然是区分大方向,用一个变量来标记即可,代码如下:
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); if(isCheck){ drawChecked(canvas); }else{ drawUnChecked(canvas); } }123456789
接下来开始看看选中状态的绘制流程,也就是drawChecked()方法。
移动坐标系
由于绘制的主要是圆(包括弧线),所以觉得坐标系移到中间比较方便,所以在所有绘制的开始,先将坐标原点移动到View中央:
canvas.save(); canvas.translate(halfWidth, halfHeight);12
绘制彩色弧线
如前面的流程所述,需要绘制一个扫过角度不断变大的弧线,所以要这样子做:
// sweepAnglesCounter :已扫过角度计数器,每次加多少都可以,但是要保证要是360的约数。当然了,加的太大,视觉效果就不好。// MAX_SWEEP_ANGLES:最大角度,其实就是360°if(sweepAnglesCounter < MAX_SWEEP_ANGLES){ sweepAnglesCounter += 12; }// 绘制弧线,注意是不过圆心的弧线,所以传入了falsecanvas.drawArc(-radius, -radius, radius, radius, START_ANGLES, sweepAnglesCounter, false, checkedPaint);1234567
绘制彩色圆以及白色变小的圆
如前面的流程图所示,这里需要先绘制一个彩色的圆,再绘制一个白色的圆,且白色圆的半径逐渐变小。来看代码:
// 注意这个判断标记,说明绘制动态弧线的阶段已经过了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); }1234567891011121314
绘制彩色扩大的圆以及小钩
嗯这里我们需要一个彩色圆不断变大的半径计数,彩色圆的半径上限,还有描述小钩路径的Path对象。所以需要这样写:
// 注意这个标记位,表示“白色逐渐变小的圆”绘制阶段已经结束,所以开始进入彩色圆和小钩绘制阶段if(whiteRadiusCounter < 20){ whitePaint.setStyle(Paint.Style.STROKE); // 半径计数器小于半径的上限 if(expandRadiusCounter < maxExpandRadius){ // 绘制彩色圆变大(动态)同时绘制“小钩”(静态) expandRadiusCounter += 20; canvas.drawCircle(0, 0, expandRadiusCounter, checkedPaint); canvas.drawPath(tickPath, whitePaint); // 绘制小钩 } }1234567891011
绘制彩色变小的圆以及小钩
逻辑和前面一步差不多,只不过是计数方式反过来了,不多说了,看代码:
if(expandRadiusCounter == maxExpandRadius){ // 彩色圆半径缩小计数器仍大于等于圆初始大小 if(narrowRadiusCounter >= radius) { // 绘制彩色圆缩回变大前效果(动态)同时绘制“小钩”(静态) narrowRadiusCounter -= 20; canvas.drawCircle(0, 0, narrowRadiusCounter, checkedPaint); canvas.drawPath(tickPath, whitePaint);// 画小勾 } }123456789
恢复坐标系,重置计数器
动态绘制完成了,当然是要把东西还原回去,像这样子:
canvas.restore(); // 恢复坐标系// “绘制彩色变小的圆和小钩”阶段还没结束(或者可能还没开始),说明选中状态的动画还没结束,继续重绘// 注意这里的继续重绘,这就是动画效果实现的原因if(narrowRadiusCounter >= radius){ // 也可以改成调用postInvalidateDelayed()方法控制动画速度 invalidate(); } else { // 动态效果绘制结束立刻重置变量 // 避免窗口在onStop()-->onReStar()之后导致该View绘制异常 reset(); }1234567891011
未选中状态的静态效果
这个效果比较简单,是静态的,几行代码就搞定了:
private void drawUnChecked(Canvas canvas){ canvas.save(); canvas.translate(halfWidth, halfHeight); // 绘制一个灰色的圆圈、小钩 canvas.drawCircle(0, 0, radius, unCheckedPaint); canvas.drawPath(tickPath, unCheckedPaint); canvas.restore(); }12345678
添加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>123456789101112
然后在构造方法里读取并设置这些属性
// 获取xml属性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();123456789
暴露一些控制接口
看我们前面贴的效果图,点击按钮可以改变选中效果,所以肯定是有提供控制接口,也很简单,直接看代码:
public void setCheck(boolean check) { isCheck = check; // 记得要重置计数器,这很重要 reset(); invalidate(); }123456
关于测量——重写onMeasure()方法
主要是为了支持wrap_content属性,总不能总是占满全屏,或者迫使调用者写个固定尺寸。那么这个默认尺寸该怎么设计呢?很简单,彩色圆变大的时候,有一个上限半径,这个就可以作为默认尺寸。不过我们在xml文件里面支持了圆形扩大前的半径,如果用户设置了该怎么办呢?只要在两者之间做一个简单计算就可以了,像这样子:
// radius是来自xml里面设置的半径maxExpandRadius = radius + 60;12
那么现在onMeasure()方法就可以这样写了:
@Overrideprotected 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; }1234567891011121314151617181920
小结
总体来说是一个不复杂的自定义View,非常适合新手尝试绘制动画效果。其中主要注意两点:
区分选中和未选中状态,一个是静态效果(不需要反复绘制),一个是动态效果(需要不断重绘)。这两个效果建议写在两个不同方法里面,不要扎堆地写在onDraw()方法里面。
在重绘的过程里,通过计数器判断处于哪个绘制阶段,并且在动态效果绘制结束后,注意重置计数器值,以避免一些bug。
源码
项目地址:http://www.apkbus.com/thread-601914-1-1.html
这个项目收集了几个不同的自定义View,本文介绍的这个对应的名字叫做TickView。