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對象了。
目前OPhone還不支援GIF動畫圖片的顯示,這裡就通過一個SurfaceView來展示如何定製一個支援GIF動畫的View,同時從該樣本(注釋)中也可以看出如何使用SurfaceView。
首先建立一個GifView繼承在SurfaceView,代碼如下:
class GifView extends SurfaceView implements Callback { //GifThread是用來繪製的後台線程,一般使用SurfaceView都會使用一個 //後台線程來做繪製的工作 private GifThread mGifThread; //GifDecoder是一個Gif圖片格式的解析器,用來解析Gif圖片的每幀資料和顯示時間。 private GifDecoder mGifDecoder; public GifView(Context context,int
gifId) { super(context); //擷取Gif圖片資料 InputStream is = context.getResources().openRawResource(gifId); //解析Gif圖片資料 mGifDecoder = new GifDecoder(); mGifDecoder.read(is); try { is.close(); } catch (IOException e) { e.printStackTrace(); } is = null; //擷取SurfaceHolder
SurfaceHolder holder = getHolder(); //如果有必要可以設定合適的SurfaceType //holder.setType(SurfaceHolder.SURFACE_TYPE_HARDWARE); //設定回呼函數 holder.addCallback(this); //建立Gif繪製線程 mGifThread = new GifThread(holder,this); } public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) { //Do nothing } public void surfaceCreated(SurfaceHolder holder) { //當Surface建立成功後,啟動繪製線程 mGifThread.setRunning(true); mGifThread.start(); } public void surfaceDestroyed(SurfaceHolder holder) { //當Surface即將摧毀的時候,停止繪製線程 boolean
retry = true; mGifThread.setRunning(false); while (retry) { try { mGifThread.join(); retry = false; } catch (InterruptedException e) { // we will try it again and again... } } } } 在代碼中都有詳細的注釋,這裡就不再解釋,下面是GifThread代碼: class GifThread extends Thread { private
SurfaceHolder mSurfaceHolder; private boolean mRunning; private int mCurrentFrame;//當前繪製的幀數 private long mLastTime;//上一幀繪製的時間 private int mFrameCount;//總幀數 private Bitmap[] mBitmap;//每幀的圖片資料 private int[] mDelay;//每幀的顯示時間 public GifThread(SurfaceHolder surfaceHolder,
GifView gifView) { mSurfaceHolder = surfaceHolder; GifDecoder mGifDecoder = gifView.mGifDecoder; mLastTime = System.currentTimeMillis(); mFrameCount = mGifDecoder.getFrameCount(); mBitmap = new Bitmap[mFrameCount]; mDelay = new int[mFrameCount]; for (int i
= 0; i < mFrameCount; i++) { mBitmap[i] = mGifDecoder.getFrame(i); mDelay[i] = mGifDecoder.getDelay(i); } mGifDecoder = null; } public void setRunning(boolean running) { mRunning = running; } private void doDraw(Canvas canvas) { mLastTime = System.currentTimeMillis();;
canvas.drawBitmap(mBitmap[mCurrentFrame], new Matrix(), null); mCurrentFrame ++; } @Override public void run() { Canvas c; int delay = 0; while (mRunning) { c = null; if(mCurrentFrame != 0) { delay = mDelay[mCurrentFrame-1]; } if(mCurrentFrame == mFrameCount)
{ mCurrentFrame = 0; } long currentTime = System.currentTimeMillis(); long t = currentTime - mLastTime; //如果到下一幀繪製時間開始繪製下一幀 if(t >= delay) { try { //擷取Canvas來繪製介面 c = mSurfaceHolder.lockCanvas(null); //通過mSurfaceHolder來同步繪製操作 synchronized (mSurfaceHolder)
{ doDraw(c); } } finally { // 在finally中執行該操作,這樣當上面的代碼拋出異常的時候 //不會導致Surface出去不一致的狀態。 if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } } }
最後建立一個Activity來測試下GifView:
public class SurfaceActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new GifView(this,R.raw.pic)); } }