基于SurfaceView的loading动画

概述

最近看到一个bootstrap的loading框,感觉还挺好看的,决定动手写一个。首先来技术选型一下吧,其实一个loadingview在这里扯性能问题并不是很合适,但是如果一定要讨论用什么去实现的话,我认为其事SurfaceView是个不错的选择,原因在于我们是主动的更新界面,根据数据去运算,所以,这种主动更新的场景更加适合SurfaceView来实现,刚好最近写了一片关于SurfaceView的入门博客,拿来练练手挺不错的。

示例

先来看看本文最后的结果长啥样子吧。

12个旋转的球
5个花色球型的脉冲
5个同色的矩形脉冲

SurfaceView回顾

由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。总之就是夸奖SurfaceView很牛逼啦,但是牛逼的同时,也是会有坑的哦。

SurfaceView使用的几个步骤

SurfaceView使用的步骤主要包括:

  1. 首先继承自SurfaceView,实现最基本的几个构造函数,在构造函数里初始化最基本的绘图数据。
  2. 在构造函数里,获取SurfaceHolder示例,保存在一个变量里,该句柄便于后面线程中绘画使用。
  3. 为holder添加一个callback函数,用来出发SurfaceView各个阶段的操作。为了代码简洁,直接将外层类继承自SurfaceView.CallBack,并执行addCallBack。
  4. 这个Callback一共有三个方法要去覆写,分别是
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

三个方法分别会在创建,可见度改变,和不可见的时候触发。所以在使用的时候,我们要调用SurfaceView的setVisibility方法来控制其生命周期。

  1. 在创建区域surfaceCreated进行绘图相关的初始化工作,比如Paint,循环线程的开始操作。
  2. 在不可见销毁区域surfaceDestroyed关闭线程,个人认为最好的退出循环线程的方法是使用变量标志位。
  3. 在创建的线程里,一般首先调用holder.lockCanvas来获取画布,或者调用lockCanvas(RectF)来锁定脏数据区域。然后进行各种异想天开的绘画,这时候,世界就交给你了。
    基本上知道以上的步骤,就大概了解这东西怎么用了,当然也有写注意点的,如何设置背景透明,绘图清除区域颜色等,还有线程更新时间等。

矩形脉冲

基本思路

首先是订基本参数咯,我们的边框是多少,这就需要定义containerWidth,containerHeight,这个可以在surfaceCreated以后,通过api获取,然后我们的总长度是多少totalWidth,一共绘制几个count,每个之间的宽度是多少xGap,每个矩形的基本高度是多少baseHeight,矩形的振幅AMPLITUDE等。如下:

1
2
3
4
private int containerWidth, containerHeight;
private int totalWidth, itemHeight;
private int xGap;
private int count;

定义runnable

我们要循环的制造脉冲,那么需要在runnable中定义一个循环事件,解释起来太难了,直接上代码吧,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
drawingRunnable = new Runnable() {
@Override
public void run() {
containerWidth = getWidth();
containerHeight = getHeight();
startX = (containerWidth - totalWidth) / 2;
startY = (containerHeight - itemHeight) / 2;
xGap = 10;
Rect dirtyArea = new Rect(startX, startY, startX + totalWidth, startY + itemHeight);
while (exitFlag) {
canvas = holder.lockCanvas(dirtyArea);
// canvas = holder.lockCanvas();
if (canvas == null) return;
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清除画布
for (int i = 0; i < count; i++) {
int wave = calcFactor(i, factor);
canvas.drawRect(startX + (xGap + step) * i, startY - wave / 2,
startX + step + (xGap + step) * i,
startY + itemHeight + wave / 2, paint);
}
factor += SPEED;
holder.unlockCanvasAndPost(canvas);// 更新屏幕显示内容
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};

照着代码来解释可能简单些,先计算容器高宽,知道自己的绘制空间,计算起始绘制点startX和startY,计算出脏数据区域,依次绘制出count个矩形,其高度的计算是核心,绘制出这些矩形后,提交更新。并将线程睡眠10毫秒,这里取10毫秒是因为动画的最小单元就是10,我们也是在做动画,没必要搞得很快,给及其带来压力,同时也不能太慢,这样会有明显的视觉错误。

制造脉冲发生器

在脉冲发生器上,每个人可能都有自己的想法去实现,我都想了好几种,最后还是想到了去正弦线上去截数据的办法,即将0-PI/2的空间分成count份,并将小于0的部分割掉,那么脉冲就形成了啊,但是这个脉冲是比较缓的。代码如下:

1
2
3
4
5
private int calcFactor(int index, int factor) {
double diff = Math.PI / (DELAY);
int res = (int) (AMPLITUDE * Math.cos(Math.PI * factor / 360 - index * diff));
return res < 0 ? 0 : res;
}

好了,至此,我们就完成了矩形的发生器loadingView了,按照这个思路,我们可以写出各种各样的脉冲型loadingView,当然不一定是loadingView哈,现在你就是画家了,想怎么画就怎么画,期待大家的master piece啦。

矩形完整的代码

在博客里不想贴太多代码,那么就贴一个矩形的代码吧,其他的代码我做成了android library放在了github上,如果感兴趣的话,可以到我的个人空间下去下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* Created by gordon on 16/5/29.
*/
public class RectWaveLoadingView extends SurfaceView implements SurfaceHolder.Callback {
private Canvas canvas;
private Paint paint;
private SurfaceHolder holder;
private Runnable drawingRunnable;
private int containerWidth, containerHeight;
private int factor;
private int totalWidth, itemHeight;
private boolean exitFlag = true;
private int startX, startY;
private int xGap;
private int count;
/**
* set this params larger is bigger amplitude is required.
*/
private static final int AMPLITUDE = 50;
/**
* set this params larger is faster speed is required.
*/
private static final int SPEED = 12;
/**
* if equal to count, then no delay at all, set this params larger if no delay is required.
*/
private static final int DELAY = 8;
public RectWaveLoadingView(Context context) {
super(context);
init();
}
public RectWaveLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RectWaveLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setZOrderOnTop(true);
getHolder().addCallback(this);
getHolder().setFormat(PixelFormat.TRANSLUCENT);
factor = 0;
totalWidth = 120;
itemHeight = 80;
count = 5;
paint = new Paint();
paint.setColor(Color.BLACK);
final int step = (totalWidth - (count - 1) * xGap) / count;
drawingRunnable = new Runnable() {
@Override
public void run() {
containerWidth = getWidth();
containerHeight = getHeight();
startX = (containerWidth - totalWidth) / 2;
startY = (containerHeight - itemHeight) / 2;
xGap = 10;
Rect dirtyArea = new Rect(startX, startY, startX + totalWidth, startY + itemHeight);
while (exitFlag) {
canvas = holder.lockCanvas(dirtyArea);
// canvas = holder.lockCanvas();
if (canvas == null) return;
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清除画布
for (int i = 0; i < count; i++) {
int wave = calcFactor(i, factor);
canvas.drawRect(startX + (xGap + step) * i, startY - wave / 2,
startX + step + (xGap + step) * i,
startY + itemHeight + wave / 2, paint);
}
factor += SPEED;
holder.unlockCanvasAndPost(canvas);// 更新屏幕显示内容
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
private int calcFactor(int index, int factor) {
double diff = Math.PI / (DELAY);
int res = (int) (AMPLITUDE * Math.cos(Math.PI * factor / 360 - index * diff));
return res < 0 ? 0 : res;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
this.holder = holder;
new Thread(drawingRunnable).start();
Log.d("loading","created");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.d("loading","changed");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
exitFlag = false;
Log.d("loading","destroyed");
}
}

下载地址是 https://github.com/gordon-rawe/loadingview.git

总结

其实使用VectorAnimator也可以用一些xml就搞定,但是好像API有限制,接下来想做一个从一个形状编程另一个形状的动画,期待我的作品吧!