Android繪圖機制與處理技巧(五)——View的孿生兄弟SurfaceView

SurfaceView與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的模板代碼:

/**
 * Created by Administrator on 2016/6/1.
 * 使用SurfaceView的模板代碼
 */
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    // SurfaceHolder
    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() {
        //初始化一個SurfaceHolder對象
        mHolder = getHolder();
        //註冊SurfaceHolder的回調方法
        mHolder.addCallback(this);

        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
        //mHolder.setFormat(PixelFormat.OPAQUE);
    }

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

    //SurfaceView的改變
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {

    }

    //SurfaceView的銷燬
    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        //通過循環來不停地進行繪製
        while(mIsDrawing){
            draw();
        }
    }

    private void draw() {
        try {
            /**
             * 獲取當前的Canvas繪圖對象。
             * 獲取到的Canvas對象還是繼續上次的Canvas對象,而不是一個新的對象。因此,之前的繪圖操作都將保留,
             * 如果需要擦除,則可以在繪製前,通過drawColor()方法來進行清屏操作。
             */
            mCanvas = mHolder.lockCanvas();
            //draw something
        } catch (Exception e){

        } finally {
            if(mCanvas != null)
                //將該方法放到finally代碼塊中,來保證每次都能將畫布內容進行提交
                mHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}

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

繪製效果如下圖:

這裏寫圖片描述

繪圖板

使用SurfaceView來實現一個簡單的繪圖板,繪圖的方法與在View中進行繪圖所使用的方法一樣,也是通過Path對象來記錄手指滑動的路徑來進行繪圖。代碼如下:

    @Override
    public void run() {
        /**
         * 在前面的模板代碼中,在線程中不斷地調用draw()方法來進行繪製,擔有的時候繪製也不用這麼頻繁。
         * 因此可以在子線程中進行sleep操作,儘可能地節省系統資源。
         */
        long start = System.currentTimeMillis();
        while(mIsDrawing){
            draw();
        }
        long end = System.currentTimeMillis();
        /**
         * 通過判斷draw()方法所使用的邏輯時長來確定sleep的時長,這是一個非常通用的解決方案,代碼中的100ms是
         * 一個大致的經驗值,這個值的取值一般在50ms到100ms之間。
         */
        if(end - start < 100){
            try {
                Thread.sleep(100 - (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

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

    //記錄手指滑動的路徑
    @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;
    }

繪製效果如下圖:

這裏寫圖片描述

代碼地址

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