前言
很久之前,在简书上看到一个圆形双曲线波浪的进度条的文章,感觉很不错,结果,看一下代码,却是IOS的,嗯,其实github上也有很多android的双曲线动画,不过,我看了几个控件的源码(这个有空在分析),看不懂~原谅我把数学扔给体育老师了,里面涉及到 反余弦函数,还有Matrix的使用,好吧,这个我是真的不是很懂,然后,决定自己写一个,效果:
在文章的结尾,我会贴上代码地址。事实上,我写这篇文章的主要目的,更多是希望能描述清楚这个控件的使用,以及这个控件如何画出来。有过真实开发的同学都知道,需求千奇百怪,你永远不会知道产品下一个改动是什么。
准备
事实上,我并不准备重复我以前写过的东西,这里我只会简述一下几个API的使用:
path.quadTo() 参数为绝对位置
path.rQuadTo() 参数为相对前一个点的相对位置
上面这两个API如果不是很明白的话,可以看下我以前的一篇文章 Android 双曲线波浪动画(第一发),这篇文章里面你可以看到我很详细的讲述了这两个API的不同,当然,你还可以欣赏到我灵魂画师般的画技^-^。
path.arcTo 添加一个圆弧到path,如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
上面的解释感觉好抽象,举个实际简单的列子:
假设这是一个左上角是原点的矩形,x轴边长 = 20, y轴边长 = 10,那么如图,我想要填充如图的形状,那么问题主要就是怎么连接 B – C。
伪代码:
path.moveTo(0, 0); 从 0,0 出发
path.lineTo(5, 0); A 连接到 C
Rect rect = new Rect(0, 0, 20, 10); 整个矩形的Rect对象
path.arcTo(rect, 180, 90, true); 180 代表着开始的角度,90代表着需要绘制的角度, true的意思就是:将最后一个点移动到圆弧起点,即不连接最后一个点与圆弧起点,你可以理解为只绘制路径,不填充圆弧,填充圆弧的话,就会变成三角的形状了
至此,如果你确实理解了以上的方法,那么接下来就是一些细节了,如果还是不太明白,那么接下来的篇幅,希望能帮助你理解。
代码思路
秉承着本人的习惯,代码就是思路,并且,最清楚的方法莫过于 onDraw。
onDraw :
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(cavans_bg);
if (progress > 0 && progress < max)
drawWave(canvas);
else if (progress == max)
canvas.drawColor(front_wave_color);
drawText(canvas);
if (shape.equals(CIRCLE))
drawArcs(canvas);
}
整个绘制,我分为了三部分: drawWave ( 绘制动态波浪 ) , drawText(绘制文字), drawArcs(绘制四角的伪三角形)。
先来看看简单的部分,drawText 和 drawArcs
drawText:很简单的代码,默认情况下绘制在控件中心
// 默认情况下的值,side_lenght 是正方形的边长。
if (text_margin_top == 0)
text_margin_top = (int) (side_length / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2);
protected void drawText(Canvas canvas) {
if (text_follow_progress)
text = progress + PERCENT_CHAR;
int textLength = (int) textPaint.measureText(text);
int i = (side_length - textLength) / 2;
canvas.drawText(text, i, text_margin_top, textPaint);
}
drawArcs: 其实就是以 左上,右上,右下,左下 的顺序绘制了四个伪三角形。
protected void drawArcs(Canvas canvas) {
pathPaint.setColor(arcColor);
path.reset();
path.moveTo(half_side_length, 0);
path.arcTo(rectf, 180, 90, true);
path.lineTo(0, 0);
path.close();
canvas.drawPath(path, pathPaint);
path.reset();
path.moveTo(half_side_length, 0);
path.arcTo(rectf, 270, 90, true);
path.lineTo(side_length, 0);
path.close();
canvas.drawPath(path, pathPaint);
path.reset();
path.moveTo(side_length, 0);
path.arcTo(rectf, 0, 90, true);
path.lineTo(side_length, side_length);
path.close();
canvas.drawPath(path, pathPaint);
path.reset();
path.moveTo(half_side_length, side_length);
path.arcTo(rectf, 90, 90, true);
path.lineTo(0, side_length);
path.close();
canvas.drawPath(path, pathPaint);
}
在看完简单的代码后,就可以来看看主要的代码 drawWave,对了,在看 drawWave 之前,需要看下一些参数的含义,以及参数的计算,我将这些步骤大都放在 onSizeChanged 方法中,也就是当 onMeasure 方法完全调用完成,完成了对 View 宽高的测量之后,再来计算我需要的值。
onSizeChanged
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Log.d(TAG, "w = " + w + " : h = " + h);
// 整个控件的矩形区间对象
rectf = new RectF(0, 0, side_length, side_length);
// side_length 其实就是矩形的边长,因为这个矩形是个正方形
// 下面三行代码分别计算了 二分之一、四分之一、以及 八分之一 的边长值
half_side_length = side_length / 2;
quarter_side_length = half_side_length / 2;
eighth_side_length = quarter_side_length / 2;
//计算每一个进度的高度
percent_height = side_length / max;
// 默认情况下,波浪的振幅
if (dwave == -1)
dwave = side_length / 40 * 3;
if (text_margin_top == 0)
text_margin_top = (int) (side_length / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2);
initPaints();
}
drawWave:注释会详细的写在代码里
protected void drawWave(Canvas canvas) {
//计算当前进度的高度
int wave_height = side_length - progress * percent_height;
// baseX 是波浪起始点的 X 轴坐标,其中 dx 是用来控制变化起始点的位置的
// 之后会贴出 dx 的变化代码
int baseX = -side_length + dx;
if (baseX > 0)
baseX = 0;
//先绘制底部的波浪
path.reset();
pathPaint.setColor(behind_wave_color);
path.moveTo(baseX, wave_height);
for (int i = 0; i < 2; i++) {
path.rQuadTo(quarter_side_length, -dwave, half_side_length, 0);
path.rQuadTo(quarter_side_length, dwave, half_side_length, 0);
}
path.lineTo(side_length, side_length);
path.lineTo(0, side_length);
path.close();
canvas.drawPath(path, pathPaint);
//再绘制顶部的波浪
// 注意:这里我默认写死了起始点的 X 坐标比 底部波浪的起始坐标 向左偏离 八分之一的宽度,这样两层波浪的效果才会逼真一点,同步的话会感觉很怪异
path.reset();
pathPaint.setColor(front_wave_color);
path.moveTo(baseX - eighth_side_length, wave_height);
for (int i = 0; i < 3; i++) {
path.rQuadTo(quarter_side_length, dwave, half_side_length, 0);
path.rQuadTo(quarter_side_length, -dwave, half_side_length, 0);
}
path.lineTo(side_length, side_length);
path.lineTo(0, side_length);
path.close();
canvas.drawPath(path, pathPaint);
}
dx 的变化代码:一个简单的属性动画
public void startWaveAnimation() {
animation = true;
int value = 0;
if (side_length == 0)
value = Math.min(width, height);
else if (height == width && height == 0)
value = side_length;
else
value = Math.min(side_length, Math.min(width, height));
valueAnimator = ValueAnimator.ofInt(0, value);
valueAnimator.setDuration(wave_duration);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
dx = (int) valueAnimator.getAnimatedValue();
if (animation)
postInvalidate();
}
});
valueAnimator.start();
}
总结
到这里其实整个控件就没什么好说的了,其他的只是一些零碎的细节,以及bug 的修复代码。其实本周计划是出两篇博客的,一篇是自己写的控件,就是本篇,另一篇是对 github 上开源控件的源码解读,然而,就像开篇所述,反三角函数 和 Matrix 把我拦住了,不过,一定会写的,等我把数学捡起来,给自己定个小目标:下星期完成。