當前位置:才華齋>設計>動畫設計>

AndroidApp中使用SurfaceView製作多執行緒動畫的例項講解

動畫設計 閱讀(1.63W)

1. SurfaceView的定義

AndroidApp中使用SurfaceView製作多執行緒動畫的例項講解

通常情況程式的View和使用者響應都是在同一個執行緒中處理的,這也是為什麼處理長時間事件(例如訪問網路)需要放到另外的執行緒中去(防止阻塞當前UI執行緒的操作和繪製)。但是在其他執行緒中卻不能修改UI元素,例如用後臺執行緒更新自定義View(呼叫View的在自定義View中的onDraw函式)是不允許的。

如果需要在另外的執行緒繪製介面、需要迅速的更新介面或則渲染UI介面需要較長的時間,這種情況就要使用SurfaceView了。SurfaceView中包含一個Surface物件,而Surface是可以在後臺執行緒中繪製的。SurfaceView的性質決定了其比較適合一些場景:需要介面迅速更新、對幀率要求較高的情況。使用SurfaceView需要注意以下幾點情況:

SurfaceView和back函式都從當前SurfaceView視窗執行緒中呼叫(一般而言就是程式的主執行緒)。有關資源狀態要注意和繪製執行緒之間的同步

在繪製執行緒中必須先合法的獲取Surface才能開始繪製內容,在aceCreated() 和aceDestroyed()之間的狀態為合法的,另外在Surface型別為SURFACE_TYPE_PUSH_BUFFERS時候是不合法的。

額外的繪製執行緒會消耗系統的資源,在使用SurfaceView的時候要注意這點。

2. SurfaceView的使用

首先繼承SurfaceView,並實現back介面,實現它的三個方法:surfaceCreated,surfaceChanged,surfaceDestroyed。

(1)surfaceCreated(SurfaceHolder holder):surface建立的時候呼叫,一般在該方法中啟動繪圖的執行緒。

(2)surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸發生改變的時候呼叫,如橫豎屏切換。

(3)surfaceDestroyed(SurfaceHolder holder) :surface被銷燬的時候呼叫,如退出遊戲畫面,一般在該方法中停止繪圖執行緒。

還需要獲得SurfaceHolder,並添加回調函式,這樣這三個方法才會執行。

只要繼承SurfaceView類並實現back介面就可以實現一個自定義的SurfaceView了,back在底層的Surface狀態發生變化的時候通知View,back具有如下的介面:

(1)surfaceCreated(SurfaceHolder holder):當Surface第一次建立後會立即呼叫該函式。程式可以在該函式中做些和繪製介面相關的初始化工作,一般情況下都是在另外的執行緒來繪製介面,所以不要在這個函式中繪製Surface。

(2)surfaceChanged(SurfaceHolder holder, int format, int width,int height):當Surface的狀態(大小和格式)發生變化的時候會呼叫該函式,在surfaceCreated呼叫後該函式至少會被呼叫一次。

(3)surfaceDestroyed(SurfaceHolder holder):當Surface被摧毀前會呼叫該函式,該函式被呼叫後就不能繼續使用Surface了,一般在該函式中來清理使用的資源。

通過SurfaceView的getHolder()函式可以獲取SurfaceHolder物件,Surface 就在SurfaceHolder物件內。雖然Surface儲存了當前視窗的畫素資料,但是在使用過程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或則Canvas lockCanvas(Rect dirty)函式來獲取Canvas物件,通過在Canvas上繪製內容來修改Surface中的資料。如果Surface不可編輯或則尚未建立呼叫該函式會返回null,在 unlockCanvas() 和 lockCanvas()中Surface的內容是不快取的,所以需要完全重繪Surface的內容,為了提高效率只重繪變化的部分則可以呼叫lockCanvas(Rect dirty)函式來指定一個dirty區域,這樣該區域外的內容會快取起來。在呼叫lockCanvas函式獲取Canvas後,SurfaceView會獲取Surface的一個同步鎖直到呼叫unlockCanvasAndPost(Canvas canvas)函式才釋放該鎖,這裡的同步機制保證在Surface繪製過程中不會被改變(被摧毀、修改)。

當在Canvas中繪製完成後,呼叫函式unlockCanvasAndPost(Canvas canvas)來通知系統Surface已經繪製完成,這樣系統會把繪製完的內容顯示出來。為了充分利用不同平臺的資源,發揮平臺的.最優效果可以通過SurfaceHolder的setType函式來設定繪製的型別,目前接收如下的引數:

(1)SURFACE_TYPE_NORMAL:用RAM快取原生資料的普通Surface

(2)SURFACE_TYPE_HARDWARE:適用於DMA(Direct memory access )引擎和硬體加速的Surface

(3)SURFACE_TYPE_GPU:適用於GPU加速的Surface

(4)SURFACE_TYPE_PUSH_BUFFERS:表明該Surface不包含原生資料,Surface用到的資料由其他物件提供,在Camera影象預覽中就使用該型別的Surface,有Camera負責提供給預覽Surface資料,這樣影象預覽會比較流暢。如果設定這種型別則就不能呼叫lockCanvas來獲取Canvas物件了。

訪問SurfaceView的底層圖形是通過SurfaceHolder介面來實現的,通過getHolder()方法可以得到這個SurfaceHolder物件。你應該實現surfaceCreated(SurfaceHolder)和surfaceDestroyed(SurfaceHolder)方法來知道在這個Surface在視窗的顯示和隱藏過程中是什麼時候建立和銷燬的。

注意:一個SurfaceView只在aceCreated() 和 aceDestroyed()呼叫之間是可用的,其他時間是得不到它的Canvas物件的(null)。

3. SurfaceView實戰

下面通過一個小demo來學習SurfaceView在實際專案中的使用,繪製一個精靈,該精靈有四個方向的行走動畫,讓精靈沿著螢幕四周不停的行走。遊戲中精靈素材和最終實現的效果圖:

首先建立核心類,原始碼如下:

public class GameView extends SurfaceView implements back { //螢幕寬高 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); text = context; mHolder = older(); allback(this); initResources(); mSprite = new Sprite(spriteAnimations,0,0,spriteWidth,spriteHeight,spriteSpeed); } private void initResources() { Bitmap[][] spriteImgs = generateBitmapArray(mContext, te, 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) { ons opt = new ons(); eferredConfig = _565; rgeable = true; putShareable = true; InputStream is = esources()RawResource(resourseId); return deStream(is, null, opt); } public Bitmap createBitmap(Context context, Bitmap source, int row, int col, int rowTotal, int colTotal) { Bitmap bitmap = teBitmap(source, (col - 1) * idth() / colTotal, (row - 1) * eight() / rowTotal, idth() / colTotal, eight() / 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); teWidth = idth() / col; teHeight = eight() / 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 && !cycled()) { cle(); 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(); t(); } } public void surfaceDestroyed(SurfaceHolder holder) { if(null != mDrawThread) { Thread(); } } private class DrawThread extends Thread { public boolean isRunning = false; public DrawThread() { isRunning = true; } public void stopThread() { isRunning = false; boolean workIsNotFinish = true; while (workIsNotFinish) { try { ();// 保證run方法執行完畢 } catch (InterruptedException e) { // TODO Auto-generated catch block tStackTrace(); } workIsNotFinish = false; } } public void run() { long deltaTime = 0; long tickTime = 0; tickTime = entTimeMillis(); while (isRunning) { Canvas canvas = null; try { synchronized (mHolder) { canvas = Canvas(); //設定方向 irection(); //更新精靈位置 tePosition(deltaTime); drawSprite(canvas); } } catch (Exception e) { tStackTrace(); } finally { if (null != mHolder) { ckCanvasAndPost(canvas); } } deltaTime = entTimeMillis() - tickTime; if(deltaTime < DRAW_INTERVAL) { try { p(DRAW_INTERVAL - deltaTime); } catch (InterruptedException e) { tStackTrace(); } } tickTime = entTimeMillis(); } } } private void drawSprite(Canvas canvas) { //清屏操作 Color(K); (canvas); } }

中包含了一個繪圖執行緒DrawThread,線上程的run方法中鎖定Canvas、繪製精靈、更新精靈位置、釋放Canvas等操作。因為精靈素材是一張大圖,所以這裡進行了裁剪生成一個二維陣列。使用這個二維陣列初始化了精靈四個方向的動畫,下面看的原始碼。

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) { eAnimations = frameAnimations; this.x = positionX; this.y = positionY; h = width; ht = height; d = speed; } public void updatePosition(long deltaTime) { switch (direction) { case LEFT: //讓物體的移動速度不受機器效能的影響,每幀精靈需要移動的距離為:移動速度*時間間隔 this.x = this.x - (float) (d * deltaTime); break; case DOWN: this.y = this.y + (float) (d * deltaTime); break; case RIGHT: this.x = this.x + (float) (d * deltaTime); break; case UP: this.y = this.y - (float) (d * deltaTime); break; } } /** * 根據精靈的當前位置判斷是否改變行走方向 */ public void setDirection() { if (this.x <= 0 && (this.y + ht) < EN_HEIGHT) { if (this.x < 0) this.x = 0; ction = ; } else if ((this.y + ht) >= EN_HEIGHT && (this.x + h) < EN_WIDTH) { if ((this.y + ht) > EN_HEIGHT) this.y = EN_HEIGHT - ht; ction = T; } else if ((this.x + h) >= EN_WIDTH && this.y > 0) { if ((this.x + h) > EN_WIDTH) this.x = EN_WIDTH - h; ction = ; } else { if (this.y < 0) this.y = 0; ction = ; } } public void draw(Canvas canvas) { FrameAnimation frameAnimation = frameAnimations[ction]; Bitmap bitmap = Frame(); if (null != bitmap) { Bitmap(bitmap, x, y, null); } }}

精靈類主要是根據當前位置判斷行走的方向,然後根據行走的方向更新精靈的位置,再繪製自身的動畫。由於精靈的動畫是一幀一幀的播放圖片,所以這裡封裝了,原始碼如下:

public class FrameAnimation{ /**動畫顯示的需要的資源 */ private Bitmap[] bitmaps; /**動畫每幀顯示的時間 */ private int[] duration; /**動畫上一幀顯示的時間 */ protected Long lastBitmapTime; /**動畫顯示的索引值,防止陣列越界 */ protected int step; /**動畫是否重複播放 */ protected boolean repeat; /**動畫重複播放的次數*/ protected int repeatCount; /** * @param bitmap:顯示的圖片
* @param duration:圖片顯示的時間
* @param repeat:是否重複動畫過程
*/ public FrameAnimation(Bitmap[] bitmaps, int duration[], boolean repeat) { aps = bitmaps; tion = duration; at = repeat; lastBitmapTime = null; step = 0; } public Bitmap nextFrame() { // 判斷step是否越界 if (step >= th) { //如果不無限迴圈 if( !repeat ) { return null; } else { lastBitmapTime = null; } } if (null == lastBitmapTime) { // 第一次執行 lastBitmapTime = entTimeMillis(); return bitmaps[step = 0]; } // 第X次執行 long nowTime = entTimeMillis(); if (nowTime - lastBitmapTime <= duration[step]) { // 如果還在duration的時間段內,則繼續返回當前Bitmap // 如果duration的值小於0,則表明永遠不失效,一般用於背景 return bitmaps[step]; } lastBitmapTime = nowTime; return bitmaps[step++];// 返回下一Bitmap } }

FrameAnimation根據每一幀的顯示時間返回當前的圖片幀,若沒有超過指定的時間則繼續返回當前幀,否則返回下一幀。

接下來需要做的是讓Activty顯示的View為我們之前建立的GameView,然後設定全屏顯示。

public void onCreate(Bundle savedInstanceState) { eate(savedInstanceState); getWindow()lags(_FULLSCREEN, _FULLSCREEN); requestWindowFeature(URE_NO_TITLE); getWindow()lags(_KEEP_SCREEN_ON, _KEEP_SCREEN_ON); DisplayMetrics outMetrics = new DisplayMetrics(); indowManager()efaultDisplay()etrics(outMetrics); EN_WIDTH = hPixels; EN_HEIGHT = htPixels; GameSurfaceView gameView = new GameSurfaceView(this); setContentView(gameView); }

現在執行Android工程,應該就可以看到一個手持寶劍的武士在沿著螢幕不停的走了。