Android SurfaceView使用詳解


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM