1. SurfaceView 游戲框架實例
實例效果:就是屏幕上的文本跟着點擊的地方移動,效果圖如下:
步驟:
新建項目“GameSurfaceView”,首先自定義一個類"MySurfaceView",此類繼承SurfaceView,並實現android.view.SurfaceHolder.Callback 接口,代碼如下

package com.example.ex4_5; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; //Callback接口用於SurfaceHolder 對SurfaceView 的狀態進行監聽 public class MySurfaceView extends SurfaceView implements Callback{ //用於控制SurfaceView 的大小、格式等,並且主要用於監聽SurfaceView 的狀態 private SurfaceHolder sfh; private Paint paint; private int textX=30,textY=30; public MySurfaceView(Context context) { super(context); //實例SurfaceView sfh = this.getHolder(); //為SurfaceView添加狀態監聽 sfh.addCallback(this); //實例一個畫筆 paint = new Paint(); //設置字體大小 paint.setTextSize(30); //設置畫筆的顏色 paint.setColor(Color.GREEN); } @Override //當SurfaceView 被創建完成后響應 public void surfaceCreated(SurfaceHolder holder) { myDraw(); } @Override //當SurfaceView 狀態發生改變時響應 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override //當SurfaceView 狀態被摧毀時響應 public void surfaceDestroyed(SurfaceHolder holder) { } //SurfaceView 是通過SurfaceHolder 來修改其數據,所以即時重寫View 的onDraw(Canvas canvas)函數,在SurfaceView 啟動時也不會執行到,因此這里自定義繪圖函數 public void myDraw() { //獲取SurfaceView 的Canvas 對象, //同時對獲取的Canvas 畫布進行加鎖,防止SurfaceView 在繪制過程中被修改、摧毀等發生的狀態改變 //另外一個lockCanvas(Rect rect)函數,其中傳入一個Rect矩形類的實例,用於得到一個自定義大小的畫布 Canvas canvas = sfh.lockCanvas(); //填充背景色,即刷屏,每次在畫布繪圖前都對畫布進行一次整體的覆蓋 canvas.drawColor(Color.BLACK); //繪制內容 canvas.drawText("This is a Text !", textX, textY, paint); //解鎖畫布和提交 sfh.unlockCanvasAndPost(canvas); } //重寫觸屏監聽事件 @Override public boolean onTouchEvent(MotionEvent event) { textX = (int)event.getX(); textY = (int)event.getY(); myDraw(); return super.onTouchEvent(event); } }
修改MainActivity 類,讓其顯示自定義的SurfaceView 視圖
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //顯示自定義的SurfaceView 視圖 setContentView(new MySurfaceView(this)); } }
配置文件中設置應用程序為全屏
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
具體說明可查看代碼。
2.SurfaceView 視圖添加線程
在游戲中,基本上不會等到用戶每次觸發了按鍵事件、觸屏事件才去重繪畫布,而是會固定一個時間去刷新畫布:比如游戲中的倒計時、動態的花草、流水等等,這些游戲元素並不會跟玩家交互,但是這些元素都是動態的。所以游戲開發中會有一個線程不停的去重繪畫布,實時的更新游戲元素的狀態。
當然游戲中除了畫布給玩家最直接的動態展現外,也會有很多邏輯需要不斷的去更新,比如怪物的AI(人工智能)、游戲中錢幣的更新等等。
下面給上面實例中的SurfaceView 視圖添加線程,用於不停的重繪畫布以及不停地執行游戲邏輯。
實例效果如下:
修改后,MySurfaceView 類代碼如下:

package com.example.ex4_5; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; //Callback接口用於SurfaceHolder 對SurfaceView 的狀態進行監聽 public class MySurfaceView extends SurfaceView implements Callback, Runnable { // 用於控制SurfaceView 的大小、格式等,並且主要用於監聽SurfaceView 的狀態 private SurfaceHolder sfh; // 聲明一個畫筆 private Paint paint; // 文本坐標 private int textX = 30, textY = 30; // 聲明一個線程 private Thread th; // 線程消亡的標識符 private boolean flag; // 聲明一個畫布 private Canvas canvas; // 聲明屏幕的寬高 private int screenW, screenH; /** * SurfaceView 初始化函數 * * @param context */ public MySurfaceView(Context context) { super(context); // 實例SurfaceView sfh = this.getHolder(); // 為SurfaceView添加狀態監聽 sfh.addCallback(this); // 實例一個畫筆 paint = new Paint(); // 設置字體大小 paint.setTextSize(20); // 設置畫筆的顏色 paint.setColor(Color.WHITE); // 設置焦點 setFocusable(true); } /** * SurfaceView 視圖創建,響應此函數 */ @Override public void surfaceCreated(SurfaceHolder holder) { screenW = this.getWidth(); screenH = this.getHeight(); flag = true; // 實例線程 th = new Thread(this); // 啟動線程 th.start(); } /** * SurfaceView 視圖狀態發生改變時,響應此函數 */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * SurfaceView 視圖消亡時,響應此函數 */ @Override public void surfaceDestroyed(SurfaceHolder holder) { flag = false; } /** * 游戲繪圖 */ public void myDraw() { try { canvas = sfh.lockCanvas(); if (canvas != null) { // ————利用繪制矩形的方式刷屏 // canvas.drawRect(0, 0, this.getWidth(), this.getHeight(), // paint); // ————利用填充畫布,刷屏 // canvas.drawColor(Color.BLACK); // ————利用填充畫布指定的顏色分量,刷屏 canvas.drawRGB(0, 0, 0); canvas.drawText("啦啦啦,德瑪西亞!", textX, textY, paint); } } catch (Exception e) { // TODO: handle exception } finally { if (canvas != null) { sfh.unlockCanvasAndPost(canvas); } } } /** * 觸屏事件監聽 */ @Override public boolean onTouchEvent(MotionEvent event) { textX = (int) event.getX(); textY = (int) event.getY(); return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return super.onKeyDown(keyCode, event); } /** * 游戲邏輯 */ private void logic() { } @Override public void run() { while (flag) { long start = System.currentTimeMillis(); myDraw(); logic(); long end = System.currentTimeMillis(); try { if (end - start < 50) { Thread.sleep(50 - (end - start)); } } catch (Exception e) { e.printStackTrace(); } } } }
代碼說明:
(1)線程標識位

在代碼中“boolean flag;”語句聲明一個布爾值,它主要用於以下兩點: ①便於消亡線程 一個線程一旦啟動,就會執行run() 函數,run() 函數執行結束后,線程也伴隨着消亡。由於游戲開發中使用的線程一般都會在run() 函數中使用一個while 死循環,在這個循環中會調用繪圖和邏輯函數,使得不斷的刷新畫布和更新邏輯;那么如果游戲暫停或者游戲結束時,為了便於銷毀線程在此設置一個標識位來控制。 ②防止重復創建線程及程序異常 為什么會重復創建線程,首先從Android 系統的手機說起。熟悉或者接觸過 Android 系統的人都知道,Android 手機上一般都有“Back(返回)”與“Home(小房子)”按鍵。不管當前手機運行了什么程序,只要單擊“Back”或者“Home”按鍵的時候,默認會將當前的程序切入到系統后台運行(程序中沒有截獲這兩個按鈕的前提下);也正因為如此,會造成MySurfaceView 視圖的狀態發生改變。 首先單擊“Back” 按鈕使當前程序切入后台,然后單擊項目重新回到程序中,SurfaceView 的狀態變化為:surfaceDestroyed -> 構造函數 -> surfaceCreated -> surfaceChanged 。 然后單擊“Home” 按鈕使當前程序切入后台,然后單擊項目重新回到程序中,SurfaceView 的狀態變化為:surfaceDestroyed -> surfaceCreated -> surfaceChanged 。 通過 SurfaceView 的狀態變化可以明顯看到,當點擊“Back” 按鈕並重新進入程序的過程要比點擊“Home” 按鈕多執行了一個構造函數。也就是說當點擊“Back” 返回按鍵時,SurfaceView 視圖會被重新加載 。 正因為這個原因,如果線程的初始化是在構造函數或者在構造函數之前,那么線程也要放在視圖構造函數中進行。 千萬不要把線程的初始化放在 surfaceCreated 視圖創建函數之前,而線程的啟動卻放在 surfaceCreated 視圖創建的函數中,否則程序一旦被玩家點擊“Home”按鍵后再重新回到游戲時,程序會拋出異常。 異常是因為線程已經啟動造成的,原因很簡單,因為程序被“Home” 鍵切入后台再從后台恢復時,會直接進入 surfaceCreated 視圖中創建函數,又執行了一遍線程啟動! 能夠想到的解決方法是,可以將線程的初始化和啟動都放在視圖的構造函數中,或者都放在視圖創建的函數中。但是這里又出現新的問題,如果將線程的初始化和啟動都放在視圖的構造函數中,那么當程序被“Back”鍵切入后台再從后台恢復時,線程的數量會增多,反復多次,就會反復多出對應的線程。 那么,如果將flag這個線程標識位在視圖摧毀時讓其值改為false ,從而使當前這個線程的run 方法執行完畢,以達到摧毀線程的目的,但是如果點擊“Home”鍵呢?當程序恢復的時候,程序就不執行線程了,也就是說重繪和邏輯函數都不再執行! 所以最完美的做法是,線程的初始化與線程的啟動都寫在視圖的surfaceCreated 創建函數中,並且將線程標識位在視圖摧毀時將其值改變為 false 。這樣既可避免“線程已啟動”的異常,還可以避免點擊Back 按鍵無限增加線程數的問題。
(2)獲取視圖的寬和高

在SurfaceView 視圖中獲取視圖的寬和高的方法: this.getWidth(); 獲取視圖寬度 this.getHeight(); 獲取視圖高度 在 SurfaceView 視圖中獲取視圖的寬高,一定要在視圖創建之后才可以獲取到,也就是在 surfaceCreated 函數之后獲取,在此函數執行之前獲取到的永遠是零,因為當前視圖還沒有創建,是沒有寬高值的。
(3)繪圖函數 try 一下

因為當 SurfaceView 不可編輯或尚未創建時,調用 lockCanvas() 函數會返回null; Canvas 進行繪圖時也會出現不可預知的問題,所以要對繪制函數中進行 try...catch 處理;既然 lockCanvas() 函數有可能獲取為 null, 那么為了避免其他使用 canvas 實例進行繪制的函數報錯,在使用 Canvas 開始繪制時,需要對其進行判定是否為 null 。
(4)提交畫布必須放在 finally 中

繪圖的時候可能會出現不可預知的 Bug, 雖然使用 try 語句包起來了,不會導致程序崩潰;但是一旦在提交畫布之前出錯,那么解鎖提交畫布函數則無法被執行到,這樣會導致下次通過 lockCanvas() 來獲取 Canvas 時程序拋出異常,原因是因為畫布上次沒有解鎖提交!所以畫布將解鎖提交的函數應放入 finally 語句塊中。 還要注意,雖然這樣保證了每次能正常提交解鎖畫布,但是提交解鎖之前要保證畫布不為空的前提,所以還需判斷 Canvas 是否為空,這樣一來就完美了。
(5)刷幀時間盡可能保持一致

雖然在線程循環中,設置了休眠時間,但是這樣並不完善!比如在當前項目中, run 的 while 循環中除了調用繪圖函數還一直調用處理游戲邏輯的 logic() 函數,雖然在當前項目的邏輯函數中並沒有寫任何的代碼,但是假設這個邏輯函數 logic()中寫了幾千行的邏輯,那么系統在處理邏輯時,時間的開銷是否和上次的相同,這是無法預料的,但是可以盡可能地讓其時間差值趨於相同。假設游戲線程的休眠時間為X毫秒,一般線程的休眠寫法為: Thread.sleep(X); 優化寫法步驟如下: 步驟1 首先通過系統函數獲取到一個時間戳: long start = System.currentTimeMillis(); //在線程中的繪圖、邏輯等函數 步驟2 處理以上所有函數之后,再次通過系統函數獲取到一個時間戳: long end = System.currentTimeMillis(); 步驟3 通過這兩個時間戳的差值,就可以知道這些函數所消耗的時間:如果(end - start) > X, 那線程就完全沒有必要去休眠;如果(end - start) < X, 那線程的休眠時間應該為 X - (end - start) 。 線程休眠應更改為以下寫法: if ((end - start) < X) { Thread.sleep(X - (end - start)); } 一般游戲中刷新時間在50~100毫秒之間,也就是每秒10~20幀左右;當然還要視具體情況和項目而定。
上面的MySurfaceView 類繼承surfaceview類,並且使用回調callback接口以及線程runnable接口。那么這里簡單的說下Callback接口和SurfaceHolder 類的作用;
callback接口:
只要繼承SurfaceView類並實現SurfaceHolder.Callback接口就可以實現一個自定義的SurfaceView了,SurfaceHolder.Callback在底層的Surface狀態發生變化的時候通知View,SurfaceHolder.Callback具有如下的接口:
surfaceCreated(SurfaceHolder holder):當Surface第一次創建后會立即調用該函數。程序可以在該函數中做些和繪制界面相關的初始化工作,一般情況下都是在另外的線程來繪制界面,所以不要在這個函數中繪制Surface。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):當Surface的狀態(大小和格式)發生變化的時候會調用該函數,在surfaceCreated調用后該函數至少會被調用一次。
SurfaceHolder 類:
它是一個用於控制surface的接口,它提供了控制surface 的大小,格式,上面的像素,即監視其改變的。
SurfaceView的getHolder()函數可以獲取SurfaceHolder對象,Surface 就在SurfaceHolder對象內。雖然Surface保存了當前窗口的像素數據,但是在使用過程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或則Canvas lockCanvas()函數來獲取Canvas對象,通過在Canvas上繪制內容來修改Surface中的數據。如果Surface不可編輯或則尚未創建調用該函數會返回null,在 unlockCanvas() 和 lockCanvas()中Surface的內容是不緩存的,所以需要完全重繪Surface的內容,為了提高效率只重繪變化的部分則可以調用lockCanvas(Rect rect)函數來指定一個rect區域,這樣該區域外的內容會緩存起來。在調用lockCanvas函數獲取Canvas后,SurfaceView會獲取Surface的一個同步鎖直到調用unlockCanvasAndPost(Canvas canvas)函數才釋放該鎖,這里的同步機制保證在Surface繪制過程中不會被改變(被摧毀、修改)。
本例沒有在該surfaceview的初始化函數中將其 ScreenW 與 ScreenH 進行賦值,這里要特別注意,如果你在初始化調用ScreenW = this.getWidth();和ScreenH = this.getHeight();那么你將得到很失望的值 全部為0;原因是和接口Callback接口機制有關,當繼承callback接口會重寫它的surfaceChanged()、surfaceCreated()、surfaceDestroyed(),這幾個函數當surfaceCreated()被執行的時候,真正的view才被創建,也就是說之前得到的值為0 ,是因為初始化會在surfaceCreated()方法執行以前執行,view沒有的時候我們去取屏幕寬高肯定是0,所以這里要注意這一點;
這里把draw的代碼都try起來,主要是為了當畫的內容中一旦拋出異常了,那么也能在finally中執行該操作。這樣當代碼拋出異常的時候不會導致Surface出去不一致的狀態。
3.View 和 SurfaceView 的區別
1.更新畫布
在 View 視圖中對於畫布的重新繪制,是通過調用 View 提供的 postInvalidate() 與 invalidate() 這兩個函數來執行的,也就是說畫布是由系統主 UI 進行更新。那么當系統主 UI 線程更新畫布時可能會引發一些問題;比如更新畫面的時間一旦過長,就會造成主 UI 線程被繪制函數阻塞,這樣一來則會引發無法響應按鍵、觸屏等消息的問題。
SurfaceView 視圖中對於畫布的重繪是由一個新的單獨線程去執行處理,所以不會出現因主 UI 線程阻塞而導致無法響應按鍵、觸屏信息等問題
2.視圖機制
Android 中的View 視圖是沒有雙緩沖機制的,而 SurfaceView 視圖卻有!也可以簡單理解為, SurfaceView 視圖就是一個由 View 拓展出來的更加適合游戲開發的視圖類。
View 與 SurfaceView 都各有其優點:
比如一款棋牌類游戲,此類型游戲畫面的更新屬於被動更新;因為畫布的重繪主要是依賴與按鍵和觸屏事件(當玩家有了操作之后畫布才需要進行更新),所以此類游戲選擇 View 視圖進行開發比較合適,而且也減少了因使用 SurfaceView 需單獨起一個新的線程來不斷更新畫布所帶來的運行開銷。
但如果是主動更新畫布的游戲類型,比如RPG、飛行射擊等類型的游戲中,很多元素都是動態的,需要不斷重繪元素狀態,這時再使用 View 顯然就不合適了。
所以到底開發游戲使用哪種視圖更加的合適,這完全取決於游戲類型、風格與需求。
總體來說, SurfaceView 更加適合游戲開發,因為它能適應更多的游戲類型。