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
3
private SurfaceHolder mHolder;
private Canvas mCanvas;
private boolean mIsDrawing;

初始化方法就是对SurfaceHolder进行初始化,初始化代码如下所示:

1
2
mHolder = 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
68
public 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
84
public 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
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
public class SimpleDraw extends SurfaceView
implements SurfaceHolder.Callback, Runnable {

private SurfaceHolder mHolder;
private Canvas mCanvas;
private boolean mIsDrawing;
private boolean mIsClearScreen;
private Path mPath;
private Paint mPaint;

public SimpleDraw(Context context) {
super(context);
initView();
}

public SimpleDraw(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}

public SimpleDraw(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();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(40);
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
Log.d("zhangyan","surfaceCreated");

}

@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
Log.d("zhangyan","surfaceDestroy");
}

@Override
public void run() {

while (mIsDrawing) {
long start = System.currentTimeMillis();
draw();
long end = System.currentTimeMillis();
// 50 - 100
if (end - start < 100) {
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.d("zhangyan", "draw");

}

}

private void draw() {
try {
mCanvas = mHolder.lockCanvas();
//清除canvas的操作
while (mIsClearScreen) {
mCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
mPath.close();//关闭路径,因为路径中还保存着数据,依然可以绘制
mPath = new Path();//重新获得路径
mIsClearScreen = false;
}
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(x, y);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}


public void setClearScreen(boolean clearScreen) {
mIsClearScreen = clearScreen;
}
}

在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入门学习

android双缓冲绘图技术分析
双缓冲技术绘图
Android 绘图时实现双缓冲

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器