1. 简介和效果分析

一直都觉得有很多loading动画挺炫酷的,然后自己看过一些之后也想实现一个,加强一下对绘制view的练习,能力有限,很多地方的实现的有欠考虑和逻辑优化,不管怎么样画了两天还是把效果做的还可以,如果大家有什么意见或者建议可以给我留言,望斧正。先看一下效果图吧。这个view有两种状态,一是成功的状态,打上一个大勾,另外一个就是失败的状态,会显示一个红叉表示失败。

这个是下载或者说加载成功的动画可以看到外圈有一个片进度加载过程,每转一小圈就是加载成功一部分(在这里我是把100%的进度分为了10次,也就是说如果成功应该是转了十圈的),圆圈的中间会掉下一个小圆(强行是一个水滴。。),然后水涨船高,就加载成功了。然后再看一下加载失败的图。


加载的前部分就不说了,不同的就是在加载过程中如果失败的话就是打叉。效果我们也看完了,来看看怎么实现的吧。我们按过程来拆解。

2. 实现过程

外圈背景和进度弧的绘制


2.1 先看背景圆圈吧

colorArc = Color.argb(255, 0, 150, 136);
colorsoild = Color.argb(255, 173, 216, 230);
colorFork = Color.argb(255, 238, 0, 0);
arcPaint = new Paint();
arcPaint.setAntiAlias(true);
arcPaint.setColor(colorArc);
arcPaint.setStrokeWidth(strokeWidth);
arcPaint.setStyle(Paint.Style.STROKE);
circlePaint = new Paint(arcPaint);
circlePaint.setColor(colorsoild);

我们先是定义了paint,那是必须的,然后初始化,然后就在ondraw里面进行绘制了。不过还有一个重要的东西,就是圆弧的动态的啊,那是怎么做的呢,我在这是使用了属性动画,我们来看:

//圆圈旋转动画
mRotateAnimation = ValueAnimator.ofFloat(0f, 1f);
mRotateAnimation.setDuration(2000);
mRotateAnimation.setRepeatCount(100);
mRotateAnimation.setStartDelay(0);
mRotateAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
mRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
percent = (float) animation.getAnimatedValue();
downLength = (float) animation.getAnimatedValue() * (2f * radius + 2 * dotRadius);
if (downLength / waveHeight > 1.01) {
if (currentProgress < maxProgress) {
if (isRise) {
currentProgress += 10;
isRise = false;
}
} else {
if ((float) animation.getAnimatedValue() > 0.99)
mRotateAnimation.cancel();
}
} else {
isRise = true;
}
invalidate();
}
});

我们先在只用看percent这个参数就行了,也就是说通过属性动画不停的改变percent的值来进行动态的绘制。改变值之后我们调用invalidate这个方法,让ondraw不断调用。然后就是绘制了,我们来看ondraw里的这两行:

canvas.drawArc(rectF, 0, 360, false, circlePaint);
canvas.drawArc(rectF, -90 - 360 * percent, -(20 + percent * 344), false, arcPaint);

这里的restF是一个绘制区域,就是定义的两个图形绘制的区域可以看到,先是绘制了一个圆圈,然后重点是后面这行,percent不断改变,圆弧两个顶点的绘制起始和快慢是不一样的,然后重点都是 - 90度方向,也就有了加载一圈的效果。

2.2 水波上升的效果


在讲水波绘制之前,先简单了解一下贝塞尔曲线和使用,在path的方法中有一个这样的方法,quadTo:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
isSimplePath = false;
native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}

四个参数分别是中间点和终点的坐标值。没图说个**,那我们来看一个图


现在我们可以看到通过三个点和绘制的过程,这个方法可以绘制出如图的红色线(二阶贝塞尔曲线),通过这样我们就可以绘制一条类似于正弦的上下震动的波浪线了。

还有一个就是绘制的时候我重新建了一块画布,然后画出底部圆形上面波浪的图形,怎么绘制呢,用到了这个属性

//交集上层
progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

也就是去绘制两个图像的上面部分,相信大家应该了解,如果不了解的话可以去搜索一下,这里就不介绍了。然后绘制曲线。

//波的数量
int count = (int) ((int) (bitmapWidth / 2 + 1) * 2 / space);
//决定上升的高度
waveHeight = (1 - (float) currentProgress / maxProgress) * bitmapHeight;
path.moveTo(-bitmapWidth + waveHeight, waveHeight);
//决定 曲线的弯曲程度
float d = (1 - (float) currentProgress / maxProgress) * space;
for (int i = 0; i < count; i++) {
path.rQuadTo(space, -d, space * 2, 0);
path.rQuadTo(space, d, space * 2, 0);
}

先把起点移动移动到我们计算好的点,这里的space是一个我自己定的值,可以更改,这里的moveTo相当于先移动到上图的P0点,这里的rQuadTo和QuadTo的区别就是前者是相对起点的坐标,而不是相对整个画布的坐标,然后通过rQuadTo绘制第二个点也就是P1点,同理P2,现在的效果差不多就是这样的,图中参数对应变量意义。


然后不断循环这个过程知道把计算的count数量的整个波画完

3. 小球下落(强行是水滴 0.0)

水滴一直下落,所以要计算下落的距离

private float downLength;
downLength = (float) animation.getAnimatedValue() * (2f * radius + 2 * dotRadius);

这里下落的时候我当做和水上升当做同步上升了,后面才发现这样并不好,但是这里水下落的最大长达是固定的,怎么样才能在水滴下落到水面的时候就让水面上升呢? 那就要看一下下面这几行代码了

if (downLength / waveHeight > 1.01) {
if (currentProgress < maxProgress) {
if (isRise) {
currentProgress += 10;
isRise = false;
}
} else {
if ((float) animation.getAnimatedValue() > 0.99)
mRotateAnimation.cancel();
}
} else {
isRise = true;
}

如在水滴下落的距离和水面上升到的距离作比较,如果基本是同一个位置的话,直接控制水波上升,然后水波进入下一个上升等待状态。

4. 动画打勾(如果是成功的结果)

首先,打勾需要一个路径,这里路径我们可以自己定义,

tickPath = new Path();
tickPath.moveTo(width / 2 - 0.5f * radius, height / 2);
tickPath.lineTo(width / 2 - 0.5f * radius + 0.4f * radius, height / 2 + 0.3f * radius);
tickPath.lineTo(width / 2 - 0.5f * radius + radius, height / 2 - 0.3f * radius);
tickMeasure = new PathMeasure(tickPath, false);

这里的代码就不过所解释了,然后就是我们的绘制过程了,我还是使用的属性动画来动态改变绘制的过程中的值。

//打钩动画
tickAnimation = ValueAnimator.ofFloat(0f, 1f);
tickAnimation.setStartDelay(200);
tickAnimation.setDuration(500);
tickAnimation.setInterpolator(new AccelerateInterpolator());
tickAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tickPercent = (float) animation.getAnimatedValue();
invalidate();
}
});

然后动态绘制的过程,这里又用到了PathMeasure,不太懂得同学可以去搜索了解一下

canvas.drawArc(rectF, 0, 360, false, solidCirclePaint);
canvas.drawArc(rectF, 0, 360, false, arcPaint);
Path path = new Path();
tickMeasure.getSegment(0, tickPercent * tickMeasure.getLength(), path, true);
path.rLineTo(0, 0);
canvas.drawPath(path, arcPaint);

效果就是这样的了