Android中的雙緩衝渲染----SurfaceView

Android中提供了View進行繪圖處理,View可以滿足大部分的繪圖需求,但是有時候,View卻顯得力不從心,所以Android提供了SurfaceView給Android開發者,以滿足更多的繪圖需求。下面就讓我們一起來了解一下SurfaceView

一、爲什麼要使用SurfaceView

我們知道View是通過刷新來重繪視圖,系統通過發出VSSYNC信號來進行屏幕的重繪,刷新的時間間隔是16ms,如果我們可以在16ms以內將繪製工作完成,則沒有任何問題,如果我們繪製過程邏輯很複雜,並且我們的界面更新還非常頻繁,這時候就會造成界面的卡頓,影響用戶體驗,爲此Android提供了SurfaceView來解決這一問題。

ViewSurfaceView的區別:

1 . View適用於主動更新的情況,而SurfaceView則適用於被動更新的情況,比如頻繁刷新界面。

2 . View在主線程中對頁面進行刷新,而SurfaceView則開啓一個子線程來對頁面進行刷新。

3 . View在繪圖時沒有實現雙緩衝機制,SurfaceView在底層機制中就實現了雙緩衝機制。

這摘錄了一段網上對於雙緩衝技術的介紹

雙緩衝技術是遊戲開發中的一個重要的技術。當一個動畫爭先顯示時,程序又在改變它,前面還沒有顯示完,程序又請求重新繪製,這樣屏幕就會不停地閃爍。而雙緩衝技術是把要處理的圖片在內存中處理好之後,再將其顯示在屏幕上。雙緩衝主要是爲了解決 反覆局部刷屏帶來的閃爍。把要畫的東西先畫到一個內存區域裏,然後整體的一次性畫出來。

二、如何使用SurfaceView

要想使用SurfaceView需要經過創建、初始化、使用三個步驟,下面我們就一步步來說說這三個步驟。

1 . 創建SurfaceView

我們需要自定義一個類繼承自SurfaceView,並且實現兩個接口以及接口定義的方法,當然,與自定義View類似,還要重寫三個構造函數。下面是代碼:

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    public SurfaceViewTemplate(Context context) {
        this(context, null);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
       //創建
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //改變
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //銷燬
    }

    @Override
    public void run() {
        //子線程
    }
}

前面三個構造函數的寫法和自定義View是相同的,接下來的三個方法分別在SurfaceView創建、改變、銷燬的時候進行調用,最後的run()方法中寫我們子線程中執行的繪圖邏輯即可。

2 . 初始化SurfaceView

這一步我們主要是定義三個成員變量以備後面繪圖時使用,然後初始化這三個成員變量並且註冊對應的回調方法。代碼如下:

private SurfaceHolder mSurfaceHolder;
//繪圖的Canvas
private Canvas mCanvas;
//子線程標誌位
private boolean mIsDrawing;

/**
 * 初始化View
 */
private void initView(){
    mSurfaceHolder = getHolder();
    //註冊回調方法
    mSurfaceHolder.addCallback(this);
    //設置一些參數方便後面繪圖
    setFocusable(true);
    setKeepScreenOn(true);
    setFocusableInTouchMode(true);
}

public SurfaceViewSinFun(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //在三個參數的構造方法中完成初始化操作
    initView();
}

上面的代碼很簡單,都有註釋,相信很容易看懂,這裏不再過多解釋。

3 . 使用SurfaceView

經過上面兩步的準備工作,下面就可以開始使用SurfaceView了。

這一步又可以分爲3步來完成:

(1) 通過lockCanvas()方法獲得Canvas對象

(2) 在子線程中使用Canvas對象進行繪製

(3) 使用unlockCanvasAndPost()方法將畫布內容進行提交

注意: lockCanvas() 方法獲得的Canvas對象仍然是上次繪製的對象,由於我們是不斷進行繪製,但是每次得到的Canvas對象都是第一次創建的Canvas對象。

SurfaceView的繪製可以使用下面的模板代碼來實現,唯一的不同就是繪製邏輯的不同,代碼如下:

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private SurfaceHolder mSurfaceHolder;
    //繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;
    public SurfaceViewTemplate(Context context) {
        this(context, null);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        //開啓子線程
        new Thread(this).start();
    }

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

    }

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

    @Override
    public void run() {
        while (mIsDrawing){
            drawSomething();
        }
    }
    //繪圖邏輯
    private void drawSomething() {
        try {
            //獲得canvas對象
            mCanvas = mSurfaceHolder.lockCanvas();
            //繪製背景
            mCanvas.drawColor(Color.WHITE);
            //繪圖
        }catch (Exception e){

        }finally {
            if (mCanvas != null){
                //釋放canvas對象並提交畫布
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }

    /**
     * 初始化View
     */
    private void initView(){
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }
}

我們在xml文件中的使用和自定義View是相同的,使用全路徑名稱即可:

<com.codekong.drawlearning.view.SurfaceViewTemplate
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

三、SurfaceView小案例

下面我們通過兩個小案例來展示SurfaceView的使用。先放上效果圖

正弦曲線

手寫板

1 . 繪製正弦曲線

大體的框架都是上面給的那個代碼模板,區別只在於初始化畫筆,和具體的繪圖邏輯,所以這裏不再贅述,直接上代碼:

public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private SurfaceHolder mSurfaceHolder;
    //繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;
    private int x = 0, y = 0;
    private Paint mPaint;
    private Path mPath;
    public SurfaceViewSinFun(Context context) {
        this(context, null);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(5);
        mPath = new Path();
        //路徑起始點(0, 100)
        mPath.moveTo(0, 100);
        initView();
    }

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

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

    }

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

    @Override
    public void run() {
        while (mIsDrawing){
            drawSomething();
            x += 1;
            y = (int)(100 * Math.sin(2 * x * Math.PI / 180) + 400);
            //加入新的座標點
            mPath.lineTo(x, y);
        }
    }

    private void drawSomething() {
        try {
            //獲得canvas對象
            mCanvas = mSurfaceHolder.lockCanvas();
            //繪製背景
            mCanvas.drawColor(Color.WHITE);
            //繪製路徑
            mCanvas.drawPath(mPath, mPaint);
        }catch (Exception e){

        }finally {
            if (mCanvas != null){
                //釋放canvas對象並提交畫布
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }

    /**
     * 初始化View
     */
    private void initView(){
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }
}

2 . 手寫板(隨手指繪製軌跡)

這個主要是涉及到觸摸事件,在手指按下時將Path的起始點移動到按下的座標點,手指移動時將移動的座標點加入Path中,其他的代碼是相同的。代碼如下:

public class SurfaceViewHandWriting extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private SurfaceHolder mSurfaceHolder;
    //繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;
    //畫筆
    private Paint mPaint;
    //路徑
    private Path mPath;
    private static final String TAG = "pyh";
    public SurfaceViewHandWriting(Context context) {
        this(context, null);
    }

    public SurfaceViewHandWriting(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewHandWriting(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setAntiAlias(true);
        mPath = new Path();
        mPath.moveTo(0, 100);
        initView();
    }

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

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

    }

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

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

    }

    @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;
    }

    /**
     * 初始化View
     */
    private void initView(){
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }

    private void drawSomething() {
        try {
            //獲得canvas對象
            mCanvas = mSurfaceHolder.lockCanvas();
            //繪製背景
            mCanvas.drawColor(Color.WHITE);
            //繪製路徑
            mCanvas.drawPath(mPath, mPaint);
        }catch (Exception e){

        }finally {
            if (mCanvas != null){
                //釋放canvas對象並提交畫布
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

上面還有一個細節,在繪製的時候,我們並沒有讓線程一直運行,而是讓它休眠一會,從而節約系統資源,一般建議判斷的閾值爲50-100之間即可保證用戶體驗同時節約系統資源。




轉載自:https://www.jianshu.com/p/b037249e6d31
 

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