SurfaceView
SurfaceView和View的区别
SurfaceView和View很相似,但是也有所不同,它们的区别主要体现在以下几点:
- View适用于主动更新的情况,而SurfaceView主要适用于被动更新,例如频繁的刷新。
- View是在主线程中对界面进行刷新的,而SurfaceView通过会通过一个子线程来进行页面的刷新。
- View在绘图的时候没有采用双缓冲机制,而SurfaceView在底层实现机制中就已经实现了双缓冲机制。
网上对于双缓冲技术的介绍:1
双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。
建议阅读:android双缓冲绘图技术分析&双缓冲技术绘图&Android 绘图时实现双缓冲
为什么使用SurfaceView
Android系统中当帧率不低于60fps,也就是每一帧的绘制时间不超过16ms的时候,用户才不会感觉到卡顿出现,一帧如果在16ms内没有绘制完成,那么剩下的操作就会放到下一帧的时间去执行,导致下一帧没法绘制,这时候就出现了掉帧现象,这时候我们在32ms内看到的会是同一副画面,从而感觉到卡顿。View是通过onDraw()方法更新视图的(如果需要更新视图,必须我们主动的去调用invalidate()或者postInvalidate()方法来走一次onDraw()方法)。如果在View的onDraw()方法中执行的逻辑太多,超过了16ms,用户就会感觉到画面的卡顿。
View中的onDraw()方法是运行在主线程的,执行onDraw方法的时候会轻微阻塞主线程,对于频繁刷新的页面,onDraw方法会频繁的调用,这时候如果View的onDraw()方法比较耗时(方法中执行的逻辑太多),会导致主线程阻塞时间变长,影响程序的响应应速度,对用户事件的响应变得迟钝,降低了用户体验。比如需要频繁的刷新页面的游戏页面,onDraw方法频繁调用,再加之内部执行的操作比较多,对用户的点击事件的处理就会变得迟缓。
为了缓解和解决上述的两个问题,Android提供了SurfaceView。SurfaceView采用了双缓冲机制,提高了绘制的速度。SurfaceView可以在主线程之外的线程中向屏幕上绘图,这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。在游戏开发中多用到SurfaceView,游戏中的背景、人物、动画等等尽量在画布canvas中画出。
SurfaceView的适用场景
根据上面的分析,我们可以知道,如果我们自定义的View需要频繁的刷新,也就是onDraw方法会频繁的调用,或者刷新数据的时候处理的数据量比较大,这个时候就可以考虑使用SurfaceView来代替View了。需要注意的是SurfaceView也是View派生来的。
SurfaceView的使用
SurfaceView的使用存在的着一套模版代码,大部分的SurfaceView的绘图操作都可以使用这套模版代码来进行编写。
多数情况下可以通过一下步骤来创建一个SurfaceView的模版:
- 创建SurfaceView
创建自定义的SurfaceView继承自SurfaceView,并实现两个接口-SurfaceHolder.Callback和Runnable。代码如下所示:1
public class SimpleDraw extends SurfaceView implements SurfaceHolder.Callback, Runnable
需要实现SurfaceHolder.Callback中的方法,需要实现的方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13@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的创建,改变和销毁过程。
另外还需要实现Runnable接口的run方法。
- 初始化SurfaceView
在SurfaceView的构造方法中,对SurfaceView进行初始化操作。通常需要在SurfaceView中定义以下的三个成员变量:1
2
3private SurfaceHolder mHolder;
private Canvas mCanvas;
private boolean mIsDrawing;
初始化方法就是对SurfaceHolder进行初始化,初始化代码如下所示:1
2mHolder = getHolder();
mHolder.addCallback(this);
代码中初始化了Holder,并注册了回掉方法。
mCanvas是用来进行绘图操作的,而mIsDrawing是用来控制运行onDraw方法的子线程的。
- 使用SurfaceView
通过SurfaceHolder的lockCanvas方法获得canvas,然后利用获得的canvas对象进行绘制操作,每次通过lockCanvas方法获得的都是同一个canvas对象,之前所有的绘制操作都会被保留,如果需要清除之前的操作的话,则可以在绘制前,利用drawColor进行清除操作。
整个SurfaceView的模版代码如下所示: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
68public class SurfaceViewTemplate extends SurfaceView
implements SurfaceHolder.Callback, Runnable {
// SurfaceHolder
private SurfaceHolder mHolder;
// 用于绘图的Canvas
private Canvas mCanvas;
// 子线程标志位
private boolean mIsDrawing;
public SurfaceViewTemplate(Context context) {
super(context);
initView();
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
//mHolder.setFormat(PixelFormat.OPAQUE);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
draw();
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
// draw sth
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
在surfaceCreated中开启一个子线程,在子线程中执行一个while循环,在while循环中,不断的执行draw方法,在draw方法中中先通过lockCanvas()获得canvas方法获得canvas,然后进行绘制操作,最后通过unlockCanvasAndPost()方法,将绘制保存下来。
SurfaceView的例子
利用SurfaceView绘制正弦曲线
思路:使横纵坐标的值满足正弦曲线,然后不断改变横纵坐标的值,根据不断变动的坐标绘制曲线即可,在代码中我们可以用Path记录横纵坐标的值,然后利用canvas绘制路径的方法绘制路径。因为绘制正弦曲线的时候,需要不断的调用draw方法来绘制路径,所以我们可以选择使用SurfaceView来代替View来实现正线曲线的绘制。
完整代码如下所示: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
84public class SinView extends SurfaceView
implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
private Canvas mCanvas;
private boolean mIsDrawing;
private int x = 0;
private int y = 0;
private Path mPath;
private Paint mPaint;
public SinView(Context context) {
super(context);
initView();
}
public SinView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public SinView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
mPath.moveTo(0, 400);
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
draw();
x += 1;
y = (int) (100*Math.sin(x * Math.PI / 180) + 400);
mPath.lineTo(x, y);
if (y == 300 || y == 500) {
Log.d("zhangyan","x:"+x);
}
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
// SurfaceView背景
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
利用SurfaceView制作简单的绘图板
思路:在onTouchEvent中记录手指滑动的路径,在draw方法中绘制路径即可。
完整代码如下所示:
1 | public class SimpleDraw extends SurfaceView |
在run方法中对绘制进行了优化,为了避免频繁的绘制消耗系统资源,我们可以控制draw方法执行的频率,通过判断draw方法的执行的时间长度来决定sleep的时间,这是一个通用的做法,100ms是一个经验值,通常取50-100ms。在示例代码中进行了进一步的优化,具体实现可以阅读示例代码。此外,设置了一个清除canvas的标志位,通过设置这个标识位,可以清除canvas上之前绘制的内容,清除操作主要是下面这句代码:1
mCanvas.drawColor(0, PorterDuff.Mode.CLEAR)
示例代码:SurfaceViewPractice
参考:
《Android群英传》
Android中的卡顿现象
Android SurfaceView的基本使用
Android SurfaceView的基本介绍(一)
SurfaceView学习笔记->什么是SurfaceView
Android SurfaceView入门学习