漫說Android 中SurfaceView蘊含的美

相信大家對SurfaceView並不陌生,也相信大家一定有用它來做過視頻播放等功能。

但我今天要跟大夥分享的並不是如何利用SurfaceView來做視頻播放,而是想與大夥一起來談談SurfaceView所蘊含的美,一種只有程序員才能讀懂的美。

SurfaceView作爲View家族的一員,它的美是內在的,而這種內在的美又受View家族的薰陶。即繼承了View的精神,但又與時俱進,不乏創新精神,標新立異,因此SurfaceView它又是一種特殊的視圖,主要特殊在它擁有自己的層級(Layer)層級緩衝區(LayerBuffer)
因此我們可以通過下面這張圖(摘自:http://blog.csdn.net/luoshengyang/article/details/8661317/)來說明下SurfaceView的實現原理:

圖1:SurfaceView及其宿主Activity窗口的繪製表面示意圖

從圖上可以看到,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來繪製:

假設有這樣一個需求:通過人臉檢測返回的人臉關鍵點座標,在人臉上標出這些點,並加以編號

效果如圖所示:
人臉關鍵點

這裏首先我們得明白幾個問題:

  1. 這是實時人臉檢測,不是靜態圖片;
  2. 拿到人臉關鍵點數據後,如何畫出這些點,並一起出來在相機框內。

人臉檢測及人臉識別,這是技術性很強,專業很強的東西,非一般人所能駕馭。在這裏我們可以利用市面上一些比較出名的,識別率比較高的專業公司提供的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();
}

這樣幾步後,最終我們就可以在手機的相機預覽中看到上面類似冰冰的圖了。
謝謝你終於看完了。

發佈了73 篇原創文章 · 獲贊 26 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章