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工程,應該就可以看到一個手持寶劍的武士在沿着屏幕不停的走了。
