天天看點

Android開發之SurfaceView詳解

SurfaceView介紹

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

如果需要在另外的線程繪制界面、需要迅速的更新界面或則渲染UI界面需要較長的時間,這種情況就要使用SurfaceView了。SurfaceView中包含一個Surface對象,而Surface是可以在背景線程中繪制的。Surface屬于

OPhone底層顯示系統,關于這方面的介紹請參考附錄中的資料[1]。SurfaceView的性質決定了其比較适合一些場景:需要界面迅速更新、對幀率要求較高的情況。使用SurfaceView需要注意以下幾點情況:

SurfaceView和SurfaceHolder.Callback函數都從目前SurfaceView視窗線程中調用(一般而言就是程式的主線程)。有關資源狀态要注意和繪制線程之間的同步。 

在繪制線程中必須先合法的擷取Surface才能開始繪制内容,在SurfaceHolder.Callback.surfaceCreated() 和SurfaceHolder.Callback.surfaceDestroyed()之間的狀态為合法的,另外在Surface類型為SURFACE_TYPE_PUSH_BUFFERS時候是不合法的。 

額外的繪制線程會消耗系統的資源,在使用SurfaceView的時候要注意這點。 

使用SurfaceView

隻要繼承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調用後該函數至少會被調用一次。 

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函數來設定繪制的類型,目前接收如下的參數:

SURFACE_TYPE_NORMAL:用RAM緩存原生資料的普通Surface 

SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬體加速的Surface 

SURFACE_TYPE_GPU:适用于GPU加速的Surface 

SURFACE_TYPE_PUSH_BUFFERS:表明該Surface不包含原生資料,Surface用到的資料由其他對象提供,在Camera圖像預覽中就使用該類型的Surface,有Camera負責提供給預覽Surface資料,這樣圖像預覽會比較流暢。如果設定這種類型則就不能調用lockCanvas來擷取Canvas對象了。 

使用的SurfaceView的時候,一般情況下還要對其進行建立,銷毀,改變時的情況進行監視,這就要用到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調用後該函數至少會被調用一次。 

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函數來設定繪制的類型,目前接收如下的參數:

SURFACE_TYPE_NORMAL:用RAM緩存原生資料的普通Surface 

SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬體加速的Surface 

SURFACE_TYPE_GPU:适用于GPU加速的Surface 

SURFACE_TYPE_PUSH_BUFFERS:表明該Surface不包含原生資料,Surface用到的資料由其他對象提供,在Camera圖像預覽中就使用該類型的Surface,有Camera負責提供給預覽Surface資料,這樣圖像預覽會比較流暢。如果設定這種類型則就不能調用lockCanvas來擷取Canvas對象了。

通路SurfaceView的底層圖形是通過SurfaceHolder接口來實作的,通過getHolder()方法可以得到這個SurfaceHolder對象。你應該實作surfaceCreated(SurfaceHolder)和surfaceDestroyed(SurfaceHolder)方法來知道在這個Surface在視窗的顯示和隐藏過程中是什麼時候建立和銷毀的。

注意:一個SurfaceView隻在SurfaceHolder.Callback.surfaceCreated() 和 SurfaceHolder.Callback.surfaceDestroyed()調用之間是可用的,其他時間是得不到它的Canvas對象的(null)。

附:SurfaceView和View最本質的差別

SurfaceView和View最本質的差別在于,surfaceView是在一個新起的單獨線程中可以重新繪制畫面而View必須在UI的主線程中更新畫面。

那麼在UI的主線程中更新畫面 可能會引發問題,比如你更新畫面的時間過長,那麼你的主UI線程會被你正在畫的函數阻塞。那麼将無法響應按鍵,觸屏等消息。

當使用surfaceView 由于是在新的線程中更新畫面是以不會阻塞你的UI主線程。但這也帶來了另外一個問題,就是事件同步。比如你觸屏了一下,你需要surfaceView中thread處理,一般就需要有一個event queue的設計來儲存touch event,這會稍稍複雜一點,因為涉及到線程同步。

是以基于以上,根據遊戲特點,一般分成兩類。

1 被動更新畫面的。比如棋類,這種用view就好了。因為畫面的更新是依賴于 onTouch 來更新,可以直接使用 invalidate。 因為這種情況下,這一次Touch和下一次的Touch需要的時間比較長些,不會産生影響。

2 主動更新。比如一個人在一直跑動。這就需要一個單獨的thread不停的重繪人的狀态,避免阻塞main UI thread。是以顯然view不合适,需要surfaceView來控制。

周末看《精通Android遊戲開發》(Pro Android Games),裡面講到遊戲的架構,其中一個重要的概念surfaceview,覺得不是很了解,于是花了一點時間研究了下,寫下自己的心得。

surface,這個單詞的意思是浮在表面的,那麼surfaceview就是浮在表面的view了。如果真的這樣解釋,估計有人要拍磚了。然而,話雖不能這麼說,取這個名兒,多少還是有點關系的。surface是一個可見區域。

我們在螢幕上看到的這些view,在螢幕上看到的就是畫面,在記憶體中就是一塊記憶體區。繪圖的時候,就是顯示的硬體如顯示卡将記憶體區的這塊圖形資料繪制到螢幕上。是以,從記憶體的角度去看這些東西,會比較好了解。

surface是surfaceview中的一個可見部分。我們知道,我們看到的螢幕上的圖形,是二維的,我們看到的就是長和寬,其實,在内部實際上是三維的,另一個次元,就是層layer。我們用visio繪圖,都會看到這種情況,一個圖形會将另個圖形遮住,是因為這個圖形在上層。如果有同AutoCAD的經驗,對這個更容易了解。我們看到的圖形實際上是很多圖形一層層的疊加在一起的,這些圖形元素完全不可見,或者部分可見部分不可見,或者完全可見。

這樣看來,surface就可以這樣了解:它是記憶體中一塊區域,它是surfaceview可見不那個部分,繪圖操作作用于它,然後它就會被顯示卡之類的顯示控制器繪制到螢幕上。

surface是個啥,大概已經有了些概念了。因為它對應了一個記憶體區,大家都知道,記憶體區的對象是有生命周期的,可以動态的申請建立和銷毀,當然也可能會更新。于是,就有了作用于這個記憶體區的操作,這些操作就是surfaceCreated/Changed/Destroyed。三個操作放在一起,就是callback,

是以在很多例子裡看到,會有callback。

callback,是回調,意思是自己能幹一些活,不過自己不去主動做,而是到别人那裡去登記一下,别人需要的時候,就會叫我去做。就這個例子而言,可以打這個比方,某建築勞工隊伍A,能蓋房子,能裝修,也能拆房子。可是他自己不去主動的做這些事情,而是去向開發商B去登記這三種能力,當然了,他把這三種能力打了包,叫做A的能力。啥時候,B有活幹了,就叫A去做,或者是蓋房子,或者是裝修,或者是拆建築。在這個例子中,能力包就是callback.

說了這個例子,其實就解釋了 surface相關的一些東西,callback已經說過了,下面來說說其他的。假設surface就是一棟房子,那麼surface擁有surfaceHolder,誰呢?在這個例子中好比建築隊A。蓋房子對應的就是surfaceCreated, 拆房子就對應了surfaceDestroyed,裝修就對應了surfaceChanged.

surface有生存期,好比房子有生存期,在建造以後就存在,在拆了之後就沒有了。裝修必須發生在這之間。同樣的,surface的change必須發生在created和destroyed之間。

surfaceview知道surface的holder是誰,在surfaceview生成的時候,會調用getHolder得到holder,然後holder會調用addCallback将三個callback函數注冊。

holder擁有對于surface的控制權。在很多程式中,會在surfaceCreated的函數實作中建立另一個線程。是以在這裡有兩個線程,一個是UI線程,另一個負責畫圖的線程。畫圖線程由UI線程調用surfaceCreated的時候建立,在surfaceDestroyed調用的時候放回到線程池。在這中間,畫圖線程負責圖形的繪制。

在這種模型下,UI線程和畫圖線程各司其職,前者主要負責和使用者的互動,而後者,在負責繪制圖形。這樣,繪制圖形的時候如果時間較長,不會阻塞使用者的輸入。

我們知道,線程共享記憶體資料,是以, surface對于兩個線程是共享的。是以,為了避免在畫圖的時候,UI線程也對surface進行操作,在畫圖前,需要對surface加鎖。這個工作是有holder幹的,holder會先鎖住surface中的一塊holder.lockCanvas,我們叫canvas,然後,在上面繪畫,畫完之後,會解鎖unlockCanvasAndPost。

在 應用中,畫圖可以是一次性的,也可以是由定時器觸發的定時的畫。實作的都是runnable類中的run方法。關于runnable類,這個Java中定義的,并不是android獨有的,可以參考Java的referrence.

這個模型最大的好處就是,畫圖不依賴于UI線程,不會阻塞UI線程。

而單純的view是依賴于UI線程畫圖的。對于完全依賴于使用者的輸入進行圖像顯示的更新的,用view是可以的,但是如果能夠自動的進行繪圖,而不需等待使用者的輸入,surfaceview無疑是更好的選擇。

有圖有真相,請看圖

Android開發之SurfaceView詳解

在android中開發遊戲,一般來說,或想寫一個複雜一點的遊戲,是必須用到SurfaceView來開發的。

經過這一陣子對android的學習,我找到了自已在android中遊戲開發的誤區,不要老想着用Layout和view去實作,不要将某個遊戲

中的對象做成一個元件來處理。應該盡量想着在Canvas(畫布)中畫出遊戲戲中的背景、人物、動畫等...

SurfaceView提供直接通路一個可畫圖的界面,可以控制在界面頂部的子視圖層。SurfaceView是提供給需要直接畫像素而不是使用

窗體部件的應用使用的。Android圖形系統中一個重要的概念和線索是surface。View及其子類(如TextView, Button)

要畫在surface上。每個surface建立一個Canvas對象(但屬性時常改變),用來管理view在surface上的繪圖操作,如畫點畫線。

還要注意的是,使用它的時候,一般都是出現在最頂層的:The view hierarchy will take care of correctly compositing

with the Surface any siblings of the SurfaceView that would normally appear on top of it.

使用的SurfaceView的時候,一般情況下還要對其進行建立,銷毀,改變時的情況進行監視,這就要用到SurfaceHolder.Callback.

class BBatt extends SurfaceView implements SurfaceHolder.Callback {

public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}

//看其名知其義,在surface的大小發生改變時激發

public void surfaceCreated(SurfaceHolder holder){}

//同上,在建立時激發,一般在這裡調用畫圖的線程。

public void surfaceDestroyed(SurfaceHolder holder) {}

//同上,銷毀時激發,一般在這裡将畫圖的線程停止、釋放。

}

例子:

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

public class BBatt extends SurfaceView implements

SurfaceHolder.Callback, OnKeyListener {

private BFairy bFairy;

private DrawThread drawThread;

public BBatt(Context context) {

super

(context);

this

.setLayoutParams(

new

ViewGroup.LayoutParams(

Global.battlefieldWidth, Global.battlefieldHeight));

this

.getHolder().addCallback(

this

);

this

.setFocusable(

true

);

this

.setOnKeyListener(

this

);

bFairy =

new

BFairy(

this

.getContext());

}

public void surfaceChanged(SurfaceHolder holder,

int format,int width,int height) {

drawThread =

new

DrawThread(holder);

drawThread.start();

}

public void surfaceDestroyed(SurfaceHolder holder) {

if

( drawThread !=

null

) {

drawThread.doStop();

while

(

true

)

try

{

drawThread.join();

break

;

}

catch

(Exception ex) {}

}

}

public boolean onKey(View view, int keyCode, KeyEvent event) {}

}

執行個體2:用線程畫一個藍色的長方形

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

package com.g3.test;

import android.app.Activity;

import android.content.Context;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.graphics.RectF;

import android.os.Bundle;

import android.view.SurfaceHolder;

import android.view.SurfaceView;

public class Test extends Activity {

public void onCreate(Bundle savedInstanceState) {

super

.onCreate(savedInstanceState);

setContentView(

new

MyView(

this

));

}

//内部類

class MyView extends SurfaceView implements SurfaceHolder.Callback{

SurfaceHolder holder;

public MyView(Context context) {

super

(context);

holder =

this

.getHolder();

//擷取holder

holder.addCallback(

this

);

//setFocusable(true);

}

@Override

public void surfaceChanged(SurfaceHolder holder, int format, int width,

int height) {

}

@Override

public void surfaceCreated(SurfaceHolder holder) {

new

Thread(

new

MyThread()).start();

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

}

//内部類的内部類

class MyThread implements Runnable{

@Override

public void run() {

Canvas canvas = holder.lockCanvas(

null

);

//擷取畫布

Paint mPaint =

new

Paint();

mPaint.setColor(Color.BLUE);

canvas.drawRect(

new

RectF(40,60,80,80), mPaint);

holder.unlockCanvasAndPost(canvas);

//解鎖畫布,送出畫好的圖像

}

}

}

}

通路SurfaceView的底層圖形是通過SurfaceHolder接口來實作的,通過getHolder()方法可以得到這個SurfaceHolder對象。你應該實作surfaceCreated(SurfaceHolder)和surfaceDestroyed(SurfaceHolder)方法來知道在這個Surface在視窗的顯示和隐藏過程中是什麼時候建立和銷毀的。

SurfaceView可以在多線程中被通路。

注意:一個SurfaceView隻在SurfaceHolder.Callback.surfaceCreated() 和 SurfaceHolder.Callback.surfaceDestroyed()調用之間是可用的,其他時間是得不到它的Canvas對象的(null)。

我的通路過程:

建立一個SurfaceView的子類,實作SurfaceHolder.Callback接口。

得到這個SurfaceView的SurfaceHolder對象holder。

holder.addCallback(callback),也就是實作SurfaceHolder.Callback接口的類對象。

在SurfaceHolder.Callback.surfaceCreated()調用過後holder.lockCanvas()對象就可以得到SurfaceView對象對應的Canvas對象canvas了。

用canvas對象畫圖。

畫圖結束後調用holder.unlockCanvasAndPost()就把圖畫在視窗中了。

SurfaceView可以多線程通路,在多線程中畫圖。

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

import android.content.Context;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.util.Log;

import android.view.SurfaceHolder;

import android.view.SurfaceView;

public class MySurfaceView extends SurfaceView implements

SurfaceHolder.Callback {

private Context mContext;

private SurfaceHolder mHolder;

public TouchScreenAdjusterSurfaceView(Context context,) {

super

(context);

mContext = context;

mHolder = TouchScreenAdjusterSurfaceView.

this

.getHolder();

mHolder.addCallback(TouchScreenAdjusterSurfaceView.

this

);

this

.setFocusableInTouchMode(

true

);

// to make sure that we can get

// touch events and key events,and

// "setFocusable()" to make sure we

// can get key events

}

@Override

public void surfaceChanged(SurfaceHolder holder, int format, int width,

int height) {

// TODO Auto-generated method stub

}

@Override

public void surfaceCreated(SurfaceHolder holder) {

//now you can get the Canvas and draw something here

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

// TODO Auto-generated method stub

}

public void drawMyShape(PointPostion ps) {

mCanvas = mHolder.lockCanvas();

// draw anything you like

mHolder.unlockCanvasAndPost(mCanvas);

}

如何讓 SurfaceView 響應事件

當然建立你自己的類時,你還是得extends SurfaceView and implements Callback接口,

然後在構造函數裡設定一個屬性

this.setLongClickable(true);//這裡很重要,它是讓你的裝置支援長按效果的屬性,如果它為false 的時候MotionEvent 隻能監聽到ACTION_DOWN這個事件。

上面準備好後就可以重寫 onTouchEvent方法了!!

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

@Override

public boolean onTouchEvent(MotionEvent event)

{

switch

(event.getAction()){

case

MotionEvent.ACTION_DOWN:

Log.d(

"MotionEvent"

,

"ACTION_DOWN"

);

break

;

case

MotionEvent.ACTION_UP:

Log.d(

"MotionEvent"

,

"ACTION_UP"

);

break

;

case

MotionEvent.ACTION_MOVE:

Log.d(

"MotionEvent"

,

"ACTION_MOVE"

);

break

;

}

return

super

.onTouchEvent(event);

}