1. SurfaceView的定义
前面已经介绍过View了,下面来简单介绍一下SurfaceView,参考SDK文档和网络资料:SurfaceView是View的子类,它内嵌了一个专门用于绘制的Surface,你可以控制这个Surface的格式和尺寸,Surfaceview控制这个Surface的绘制位置。surface是纵深排序(Z-ordered)的,说明它总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内的surface内容才可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面有透明控件,那么每次surface变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
SurfaceView默认使用双缓冲技术的,它支持在子线程中绘制图像,这样就不会阻塞主线程了,所以它更适合于游戏的开发。
2. SurfaceView的使用
首先继承SurfaceView,并实现SurfaceHolder.Callback接口,实现它的三个方法:surfaceCreated,surfaceChanged,surfaceDestroyed。
surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。
surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
还需要获得SurfaceHolder,并添加回调函数,这样这三个方法才会执行。
3. SurfaceView实战
下面通过一个小demo来学习SurfaceView在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:
首先创建核心类GameView.java,源码如下:
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
public
class
GameView
extends
SurfaceView
implements
SurfaceHolder.Callback {
//屏幕宽高
public
static
int
SCREEN_WIDTH;
public
static
int
SCREEN_HEIGHT;
private
Context mContext;
private
SurfaceHolder mHolder;
//最大帧数 (1000 / 30)
private
static
final
int
DRAW_INTERVAL =
30
;
private
DrawThread mDrawThread;
private
FrameAnimation []spriteAnimations;
private
Sprite mSprite;
private
int
spriteWidth =
0
;
private
int
spriteHeight =
0
;
private
float
spriteSpeed = (
float
)((
500
* SCREEN_WIDTH /
480
) *
0.001
);
private
int
row =
4
;
private
int
col =
4
;
public
GameSurfaceView(Context context) {
super
(context);
this
.mContext = context;
mHolder =
this
.getHolder();
mHolder.addCallback(
this
);
initResources();
mSprite =
new
Sprite(spriteAnimations,
0
,
0
,spriteWidth,spriteHeight,spriteSpeed);
}
private
void
initResources() {
Bitmap[][] spriteImgs = generateBitmapArray(mContext, R.drawable.sprite, row, col);
spriteAnimations =
new
FrameAnimation[row];
for
(
int
i =
0
; i < row; i ++) {
Bitmap []spriteImg = spriteImgs[i];
FrameAnimation spriteAnimation =
new
FrameAnimation(spriteImg,
new
int
[]{
150
,
150
,
150
,
150
},
true
);
spriteAnimations[i] = spriteAnimation;
}
}
public
Bitmap decodeBitmapFromRes(Context context,
int
resourseId) {
BitmapFactory.Options opt =
new
BitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable =
true
;
opt.inInputShareable =
true
;
InputStream is = context.getResources().openRawResource(resourseId);
return
BitmapFactory.decodeStream(is,
null
, opt);
}
public
Bitmap createBitmap(Context context, Bitmap source,
int
row,
int
col,
int
rowTotal,
int
colTotal) {
Bitmap bitmap = Bitmap.createBitmap(source,
(col -
1
) * source.getWidth() / colTotal,
(row -
1
) * source.getHeight() / rowTotal, source.getWidth()
/ colTotal, source.getHeight() / rowTotal);
return
bitmap;
}
public
Bitmap[][] generateBitmapArray(Context context,
int
resourseId,
int
row,
int
col) {
Bitmap bitmaps[][] =
new
Bitmap[row][col];
Bitmap source = decodeBitmapFromRes(context, resourseId);
this
.spriteWidth = source.getWidth() / col;
this
.spriteHeight = source.getHeight() / row;
for
(
int
i =
1
; i <= row; i++) {
for
(
int
j =
1
; j <= col; j++) {
bitmaps[i -
1
][j -
1
] = createBitmap(context, source, i, j,
row, col);
}
}
if
(source !=
null
&& !source.isRecycled()) {
source.recycle();
source =
null
;
}
return
bitmaps;
}
public
void
surfaceChanged(SurfaceHolder holder,
int
format,
int
width,
int
height) {
}
public
void
surfaceCreated(SurfaceHolder holder) {
if
(
null
== mDrawThread) {
mDrawThread =
new
DrawThread();
mDrawThread.start();
}
}
public
void
surfaceDestroyed(SurfaceHolder holder) {
if
(
null
!= mDrawThread) {
mDrawThread.stopThread();
}
}
private
class
DrawThread
extends
Thread {
public
boolean
isRunning =
false
;
public
DrawThread() {
isRunning =
true
;
}
public
void
stopThread() {
isRunning =
false
;
boolean
workIsNotFinish =
true
;
while
(workIsNotFinish) {
try
{
this
.join();
// 保证run方法执行完毕
}
catch
(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
workIsNotFinish =
false
;
}
}
public
void
run() {
long
deltaTime =
0
;
long
tickTime =
0
;
tickTime = System.currentTimeMillis();
while
(isRunning) {
Canvas canvas =
null
;
try
{
synchronized
(mHolder) {
canvas = mHolder.lockCanvas();
//设置方向
mSprite.setDirection();
//更新精灵位置
mSprite.updatePosition(deltaTime);
drawSprite(canvas);
}
}
catch
(Exception e) {
e.printStackTrace();
}
finally
{
if
(
null
!= mHolder) {
mHolder.unlockCanvasAndPost(canvas);
}
}
deltaTime = System.currentTimeMillis() - tickTime;
if
(deltaTime < DRAW_INTERVAL) {
try
{
Thread.sleep(DRAW_INTERVAL - deltaTime);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
tickTime = System.currentTimeMillis();
}
}
}
private
void
drawSprite(Canvas canvas) {
//清屏操作
canvas.drawColor(Color.BLACK);
mSprite.draw(canvas);
}
}
|
GameView.java中包含了一个绘图线程DrawThread,在线程的run方法中锁定Canvas、绘制精灵、更新精灵位置、释放Canvas等操作。因为精灵素材是一张大图,所以这里进行了裁剪生成一个二维数组。使用这个二维数组初始化了精灵四个方向的动画,下面看Sprite.java的源码。
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
|
public
class
Sprite {
public
static
final
int
DOWN =
0
;
public
static
final
int
LEFT =
1
;
public
static
final
int
RIGHT =
2
;
public
static
final
int
UP =
3
;
public
float
x;
public
float
y;
public
int
width;
public
int
height;
//精灵行走速度
public
double
speed;
//精灵当前行走方向
public
int
direction;
//精灵四个方向的动画
public
FrameAnimation[] frameAnimations;
public
Sprite(FrameAnimation[] frameAnimations,
int
positionX,
int
positionY,
int
width,
int
height,
float
speed) {
this
.frameAnimations = frameAnimations;
this
.x = positionX;
this
.y = positionY;
this
.width = width;
this
.height = height;
this
.speed = speed;
}
public
void
updatePosition(
long
deltaTime) {
switch
(direction) {
case
LEFT:
//让物体的移动速度不受机器性能的影响,每帧精灵需要移动的距离为:移动速度*时间间隔
this
.x =
this
.x - (
float
) (
this
.speed * deltaTime);
break
;
case
DOWN:
this
.y =
this
.y + (
float
) (
this
.speed * deltaTime);
break
;
case
RIGHT:
this
.x =
this
.x + (
float
) (
this
.speed * deltaTime);
break
;
case
UP:
this
.y =
this
.y - (
float
) (
this
.speed * deltaTime);
break
;
}
}
/**
* 根据精灵的当前位置判断是否改变行走方向
*/
public
void
setDirection() {
if
(
this
.x <=
0
&& (
this
.y +
this
.height) < GameSurfaceView.SCREEN_HEIGHT) {
if
(
this
.x <
0
)
this
.x =
0
;
this
.direction = Sprite.DOWN;
}
else
if
((
this
.y +
this
.height) >= GameSurfaceView.SCREEN_HEIGHT
&& (
this
.x +
this
.width) < GameSurfaceView.SCREEN_WIDTH) {
if
((
this
.y +
this
.height) > GameSurfaceView.SCREEN_HEIGHT)
this
.y = GameSurfaceView.SCREEN_HEIGHT -
this
.height;
this
.direction = Sprite.RIGHT;
}
else
if
((
this
.x +
this
.width) >= GameSurfaceView.SCREEN_WIDTH
&&
this
.y >
0
) {
if
((
this
.x +
this
.width) > GameSurfaceView.SCREEN_WIDTH)
this
.x = GameSurfaceView.SCREEN_WIDTH -
this
.width;
this
.direction = Sprite.UP;
}
else
{
if
(
this
.y <
0
)
this
.y =
0
;
this
.direction = Sprite.LEFT;
}
}
public
void
draw(Canvas canvas) {
FrameAnimation frameAnimation = frameAnimations[
this
.direction];
Bitmap bitmap = frameAnimation.nextFrame();
if
(
null
!= bitmap) {
canvas.drawBitmap(bitmap, x, y,
null
);
}
}
}
|
精灵类主要是根据当前位置判断行走的方向,然后根据行走的方向更新精灵的位置,再绘制自身的动画。由于精灵的动画是一帧一帧的播放图片,所以这里封装了FrameAnimation.java,源码如下:
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
|
public
class
FrameAnimation{
/**动画显示的需要的资源 */
private
Bitmap[] bitmaps;
/**动画每帧显示的时间 */
private
int
[] duration;
/**动画上一帧显示的时间 */
protected
Long lastBitmapTime;
/**动画显示的索引值,防止数组越界 */
protected
int
step;
/**动画是否重复播放 */
protected
boolean
repeat;
/**动画重复播放的次数*/
protected
int
repeatCount;
/**
* @param bitmap:显示的图片<br/>
* @param duration:图片显示的时间<br/>
* @param repeat:是否重复动画过程<br/>
*/
public
FrameAnimation(Bitmap[] bitmaps,
int
duration[],
boolean
repeat) {
this
.bitmaps = bitmaps;
this
.duration = duration;
this
.repeat = repeat;
lastBitmapTime =
null
;
step =
0
;
}
public
Bitmap nextFrame() {
// 判断step是否越界
if
(step >= bitmaps.length) {
//如果不无限循环
if
( !repeat ) {
return
null
;
}
else
{
lastBitmapTime =
null
;
}
}
if
(
null
== lastBitmapTime) {
// 第一次执行
lastBitmapTime = System.currentTimeMillis();
return
bitmaps[step =
0
];
}
// 第X次执行
long
nowTime = System.currentTimeMillis();
if
(nowTime - lastBitmapTime <= duration[step]) {
// 如果还在duration的时间段内,则继续返回当前Bitmap
// 如果duration的值小于0,则表明永远不失效,一般用于背景
return
bitmaps[step];
}
lastBitmapTime = nowTime;
return
bitmaps[step++];
// 返回下一Bitmap
}
}
|
FrameAnimation根据每一帧的显示时间返回当前的图片帧,若没有超过指定的时间则继续返回当前帧,否则返回下一帧。
接下来需要做的是让Activty显示的View为我们之前创建的GameView,然后设置全屏显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
DisplayMetrics outMetrics =
new
DisplayMetrics();
this
.getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
GameSurfaceView.SCREEN_WIDTH = outMetrics.widthPixels;
GameSurfaceView.SCREEN_HEIGHT = outMetrics.heightPixels;
GameSurfaceView gameView =
new
GameSurfaceView(
this
);
setContentView(gameView);
}
|
现在运行Android工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。