1. 程式人生 > >Android SurfaceView使用詳解(很好的實戰例子)

Android SurfaceView使用詳解(很好的實戰例子)

一、surfaceview 
在顯示時才會呼叫callback中的surfaceCreated。注意,是在顯示時,在初始化時不會呼叫
在隱藏時會呼叫callback中的surfaceDestroyed

二、清屏操作

public void clearDraw(SurfaceHolder holder,int color) {
    Log.w("tan","clearDraw");
    Canvas canvas = null;
    try {
        canvas = holder.lockCanvas(null);
        canvas.drawColor(color);
    }catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
    }finally {
        if(canvas != null) {
            holder.unlockCanvasAndPost(canvas);
        }
    }
}

-----------------------------------------------------------------------------------

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,原始碼如下:

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