Android View 之孿生兄弟——SurfaceView

▶ SurfaceView與View的區別

Android系統提供了View進行繪圖處理,View可以滿足大家大部分的繪圖需求,但在某些時候,缺也有些心有餘而力不足,特別是在進行一些開發說的時候。我們知道,View通過刷新來重繪視圖,Android系統通過發出VSYNC心裏來進行屏幕的重繪,刷新的時隔時間爲16ms。如果在16ms內View完成了你所需要執行的所有操作,那麼用戶在視覺上,就不會產生卡頓的感覺;而如果執行的操作邏輯太多,特別是需要頻繁刷新的界面上,例如遊戲界面,那麼就會不到阻塞主線程,從而導致畫面卡頓。很多時候,在自定義View的Log經常會看到如此所示警告。

“Skipped 47 frames! The application may be doing too much work on its main thread”

這些警告的產生,很對情況就是因爲在繪製過程中,處理邏輯太多造成的。

爲了避免這一問題的產生,Android系統提供了SurfaceView組件類解決這個問題。SurfaceView可以說是View的孿生兄弟,但它與View還有有所不同的,它們的區別主要體現在一下幾點。

● View主要適用於主動更新的情況下,而SurfaceView主要適用於被動更新,例如頻繁的刷新。

● View在主線程中對界面進行刷新,而SurfaceView通常會通過一個子線程來進行頁面的刷新。

● View在繪圖時沒有使用雙緩存機制,而SurfaceView在底層實現機制中就已經實現了雙緩存機制,

總結成一句話就是,如果你的自動以View需要頻繁的刷新,或者刷新時數據處理量比較大,那麼你就可以考慮使用SurfaceView來取代View了。

▶ SurfaceView的使用

SurfaceView的使用雖然比Viwe複雜,但是SurfaceView在使用時,有一套使用模板代碼,大部分的SurfaceView繪圖操作都可以套用這樣的模板代碼進行編寫。因此,其實SurfaceView的使用更加簡單。

通常情況下,使用以下步驟來創建一個SurfaceView的模板。

● 創建SurfaceView

創建自定義的SurfaceView繼承自SurfaceView,並實現兩個接口——SurfaceHolder.Callback和Runnable,代碼以下所示。

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback,Runnable

通過實現這兩個接口,就需要在自定義的SurfaceView中實現接口的方法,對於SurfaceHolder.Callback方法,需要實現如下方法。

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int heighe) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }

分別對應SurfaceView的創建,改變和銷燬過程。

對於Runnable接口,需要實現run()方法,代碼如下所示。

    @Override
    public void run() {

    }

● 初始化SurfaceView

在自定義SurfaceView的構造方法中,需要對SurfaceViwe進行初始化。在自定義SurfaceView中,通常需要定義以下三個成員變量,代碼如下所示。

    //SurfaceHoder
    private SurfaceHolder mHolder;
    //用於繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;

初始化方法就是對SurfaceHolder進行初始化,通過以下代碼來初始化一個SurfaceHolder對象,並註冊SurfaceHolder的回調方法。

        mHolder = getHolder();
        mHolder.addCallback(this);

另外兩個成員比變量——Canvas和標誌位。對Canvas我們已經非常熟悉了,與在View的onDraw()方法中使用Canvas繪圖一樣,在SurfaceView中,我們也要使用Canvas來進行繪圖,而另一個標誌位,則是用來控制子線程的,前面已經說了,ShrfaceView通常會起一個子線程來進行繪製,而這個標誌位就是可以控制的子線程。

● 使用SurfaceView

通過SurfaceView對象的lockCanvas()方法,就可以獲得當前的Canvas繪圖對象。接下來,就可以與在View中進行的繪製操作一樣進行繪製了。不過這裏有一點需要注意的是,獲取到的Canvas對象還是繼續上次的Canvas對象,而不是一個新的對象。因此。之前的繪圖操作將都保留,如果需要擦除,則可以在繪製前,通過drawColor()方法來進行清屏操作。

繪製的時候,充分利用SurfaceView的是三個回調方法,在surfaceCreated()方法中開啓子線程進行繪製,而子線程使用一個while(mIsDrawing)的循環來不停地進行繪製,而在繪製的具體邏輯中,通過lockCanvas()方法獲得的Canvas對象進行繪製,並通過unlockCanvasAndpost(mCanvas)方法對畫布內容進行提交。整個SurfaceView的模板代碼如下所示。

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback,Runnable {
    //SurfaceHoder
    private SurfaceHolder mHolder;
    //用於繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;

    public SurfaceViewTemplate(Context context) {
        super(context);
        initView();
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    private void initView() {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        this.setKeepScreenOn(true);
//        mHolder.setFormat(PixelFormat.OPAQUE);
    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        mIsDrawing = true;
        new Thread(this).start();

    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int heighe) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        mIsDrawing = false;

    }

    @Override
    public void run() {
        while (mIsDrawing){
            draw();
        }

    }
    private void draw(){
        try {
            mCanvas = mHolder.lockCanvas();
            // draw something
        }catch (Exception e){
            
        }finally {
            if (mCanvas !=null){
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

以上代碼基本上可以滿足大部分的SurfaceView繪圖需求,唯一需要注意的是在繪製方法中,將mHolder.unlockCanvasAndPost(mCanvas)方法放到finally代碼塊中,來保證每次都能將內容提交。

▶ SurfaceView實例

下面我們通過兩個實例來看看如何使用SurfaceView進行頻繁刷新的繪圖方法。

● 正弦曲線

首先看一個類似示波器的例子,在界面上不斷繪製一個正弦曲線,類似示波器、心電圖、股票走勢圖等。當然,這樣一個視圖使用View繪製也同樣可以實現,而使用SurfaceView的具體原因前面已經講過了,這裏不再贅述。

要繪製一個正弦曲線,只需要不斷改變橫縱座標的值,並讓它們滿足正弦函數即可。因此,使用一個Path對象來保存正弦函數上的座標點,在子線程的while循環中,不斷改變縱橫座標值,代碼如下所示。

 @Override
    public void run() {
        while (mIsDrawing){
            draw();
            x += 1;
            y = (int)(100*Math.sin(x*2*Math.PI/180)+400);
            mPath.lineTo(x,y);
        }

    }
    private void draw(){
        try {
            mCanvas = mHolder.lockCanvas();
            // SurfaceView背景
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath,mPaint);
        }catch (Exception e){

        }finally {
            if (mCanvas !=null){
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }

繪圖效果如圖(1)所示。

                                                                                       圖(1)繪製正弦曲線

● 繪圖板

下面這例子,展示瞭如何使用SurfaceView來實現一個簡單的繪圖板,繪圖的方法與在View中進行繪圖所使用的方法一樣,也是通過Path對心理啊來記錄手指的滑動的路徑來進行繪圖。在SurfaceView中onTouchEvent()中來記錄Path的路徑,代碼如下所示。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mPath.moveTo(x,y);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(x,y);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

並在draw()方法中進行繪製,代碼如下所示。

    private void draw(){
        try {
            mCanvas = mHolder.lockCanvas();
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath,mPaint);
        }catch (Exception e){

        }finally {
            if (mCanvas !=null){
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }

一直到這裏爲止,這個實例與之前的實例都沒有太大區別,不過現在需要在子線程的循環中進行優化。在前面的模板代碼中,我們在線程中不斷地調用draw()方法來進行繪製,但有時候繪製也不用這麼頻繁。因此我們可以在子線程中,進行sleep操作,儘可能地節省系統資源,代碼如下所示。


    @Override
    public void run() {
        long start = System.currentTimeMillis();
        while (mIsDrawing){
            draw();
        }
        long end = System.currentTimeMillis();
        //50-100
        if (end-start<100){
            try {
                Thread.sleep(100-(end-start));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

    }

通過判斷draw()所使用的邏輯時長來確定sleep的時長,這是一個非常通用的解決方法,代碼中的100ms是一個大致的經驗值,這個值的取值一般在50ms到100ms左右。繪圖效果如圖(2)所示。

                                                                                    圖(2)繪圖實例

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章