相信大家對SurfaceView並不陌生,也相信大家一定有用它來做過視頻播放等功能。
但我今天要跟大夥分享的並不是如何利用SurfaceView來做視頻播放,而是想與大夥一起來談談SurfaceView所蘊含的美,一種只有程序員才能讀懂的美。
SurfaceView作爲View家族的一員,它的美是內在的,而這種內在的美又受View家族的薰陶。即繼承了View的精神,但又與時俱進,不乏創新精神,標新立異,因此SurfaceView它又是一種特殊的視圖,主要特殊在它擁有自己的層級(Layer)或層級緩衝區(LayerBuffer)。
因此我們可以通過下面這張圖(摘自:http://blog.csdn.net/luoshengyang/article/details/8661317/)來說明下SurfaceView的實現原理:
從圖上可以看到,DecorView作爲視圖容器,裏面可以裝載有:TextView控件及SurfaceView控件,那麼TextView是如何在窗口上來繪製自己的UI的呢?
其實從上圖我們就不難看出,它是通過SurfaceFlinger中的Layer來繪製自己的UI到宿主窗口的繪圖表面上的。而至於SurfaceFlinger是什麼,以及它與WindowManagerService有着什麼關係,我在這裏就不再贅述,不懂的,大夥可自行查找資料。總之,就是以TextView爲代表的Android普通控件,它們的UI繪製是在應用程序 的主線程中進行的。但是如果你的UI很複雜或是實時性很強的,那麼就有可能造成主線程的阻塞(因爲應用主線程除了處理UI繪製外,還要處理用戶的觸摸與輸入事件),從而導致程序出現ANR異常閃退。
就這樣,SurfaceView順應時事的出現了,繼承了TextView繪製思想,又解決了UI很複雜時,可能造成主線程阻塞的問題,從而SurfaceView很好的被用來處理視頻播放,相機預覽等等,
下面我們就來看看SurfaceView之於TextView,它的不同之至到底在哪。
首先看下Google官方文檔對SurfaceView給出的解釋:
The surface is Z ordered so that it is behind the window holding its SurfaceView;
the SurfaceView punches a hole in its window to allow its surface to be displayed.
翻譯出來大至如下(恕我英文太爛):
由於繪圖表面只是在Z軸上有序排列的,因此它在宿主窗口背後持有了SurfaceView的對象引用;正是如此,SurfaceView在Z軸上挖了個“孔”,以便於展示繪圖表面上所繪製的UI。
不知道大家有沒有看懂我的翻譯,如果沒懂,我再貼出下面一張圖,希望能幫助理解:
其實並不是SurfaceView在Z軸上真正的對宿主窗口表面挖了個洞,實際上,而只是在其宿主Activity窗口上設置了一塊透明區域罷了。明白了SurfaceView的這一實現原理,我們就可以用它來實現很多別的功能了,比如:在人臉識別的基礎上,對人臉進行虛擬妝容等,
至於SurfaceView具體的繪製過程,網上已經有大把的文章來講述了,比如老羅的【Android視圖SurfaceView的實現原理分析】,就已經講的很詳細了。那麼下面,我們就結合上面所講的,來通過一個實例看下到底如何利用SurfaceView來繪製:
假設有這樣一個需求:通過人臉檢測返回的人臉關鍵點座標,在人臉上標出這些點,並加以編號
效果如圖所示:
這裏首先我們得明白幾個問題:
- 這是實時人臉檢測,不是靜態圖片;
- 拿到人臉關鍵點數據後,如何畫出這些點,並一起出來在相機框內。
人臉檢測及人臉識別,這是技術性很強,專業很強的東西,非一般人所能駕馭。在這裏我們可以利用市面上一些比較出名的,識別率比較高的專業公司提供的SDK來實時檢測人臉(比如:Face++,商湯科技等,當然這些都是付費的)。
點座標拿到後,就是怎麼畫了,畫點有很多種方式,你可以通過Canvas畫布來畫,也可以通過OpenGLES 來畫,但後者有點殺雞用牛刀的感覺。那用Canvas怎麼畫呢?這個Canvas從哪裏來,我們如何實例化它?
正本清源,讓我們再次回到官方文檔:
Access to the underlying surface is provided via the SurfaceHolder interface,
which can be retrieved by calling getHolder().
翻譯如下:
SurfaceView 可以通過 getHolder()方法來獲取SurfaceHolder接口實例來與宿主窗口的繪圖表面surface進行通信(即在surface上進行繪製)。
The Surface will be created for you while the SurfaceView's window is visible;
you should implement surfaceCreated(SurfaceHolder) and surfaceDestroyed(SurfaceHolder) to discover when the Surface is created and destroyed as the window is shown and hidden.
翻譯如下:
當SurfaceView所在宿主窗口對用戶可見的時候,宿主窗口的繪圖表面Surface的實例也即被創建,Surface也是有生命週期的,因此你必須得讓當前宿主窗口實現SurfaceHolder接口的surfaceCreate()方法與surfaceDestroyed()方法,來判斷Surface何時創建及何時銷燬。
下面我們就來看下代碼:
第一步:實現SurfaceHolder接口
public class SurfaceViewTestActivity extends Activity implements SurfaceHolder.Callback{
private SurfaceHoloder mSurfaceViewHoloder;
private SurfaceView mSurfaceView;
private Canvas mCanvas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.surface_view_activity);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
//這個時候Surface就創建了,我們可以把holder引用賦給自定義的SurfaceHolder對象。
if(holoder != null )mSurfaceViewHoloder = holder;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//當繪製表面發生變化(比如橫豎屏切換)等時調用
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//噹噹前宿主窗口被銷燬時調用,以結束surface.
}
}
第二步:實例化SurfaceView
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.surface_view_activity);
mSurfaceView = (SurfaceView)findViewById(R.id.surfaceview);
mSurfaceView.setZOrderOnTop(true);//這就是上面說的SurfaceView在Z軸上挖洞,
//設置繪製表面Surface的PixelFormat,這裏因爲是在人臉上畫點,爲了不遮擋人臉,我們採用
//PixelFormat.TRANSLUCENT格式,即透明的
mSurfaceHoloer.setFormat(PixelFormat.TRANSLUCENT);
//假如在這裏已經返回了人臉關鍵點座標(實際不是這裏,爲了簡化流程,就在這裏假如)
//然後開始畫
startDrawPoints();
}
第三步:開始畫人臉關鍵點
/***
*注意:上面說到TextView等基礎控件是直接在應用主線程繪製UI,而SurfaceView是進化了的,它是專門用來處理
*複雜UI等的,即得單獨開一個線程來爲讓其在Layer上畫
**/
private void startDrawPoints(){
new Thread(){
@Ovrride
public void run(){
//這一步很關鍵,首先得通過SurfaceHolder來拿到Surface的Canvas
if(mSurfaceHolder != null ){
mCanvas = mSurfaceHolder.lockCanvas();
// Lock the canvas for drawing.
if (mCanvas == null) {
Log.i("WindowSurface", "Failure locking canvas");
return;
}
//這裏省略各種初始化:Paint,Path等等
mCanvas .drawPath(path, mPaint);
//畫完之後 ,記得刷新界面
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}.start();
}
這樣幾步後,最終我們就可以在手機的相機預覽中看到上面類似冰冰的圖了。
謝謝你終於看完了。