1. 程式人生 > >繪圖(四,view之繪圖雙緩衝)

繪圖(四,view之繪圖雙緩衝)

前言

以下雙緩衝的一些定義均是引用其他作者,不好意思,因為自己還沒想出比較好的定義去描述雙緩衝,同時也會引用一下其他作者的程式碼。關鍵最重要的是,我不認為,寫別人已經寫過的技術部落格,是沒有用的,也許對別人已經掌握了的,確實沒有太大作用,但是對於我本人來說,我也是剛剛吸收,也有自己的想法,感覺其他作者寫得不夠詳細,於是決定寫一篇雙緩衝的部落格,不喜勿噴,謝謝支援。

這一篇文章我會接在上一篇文章《繪圖(三,進階之繪製錶盤)》繼續深入講解關於雙緩衝的好處,當然如果沒看《繪圖(三,進階之繪製錶盤)》這篇文章的不要緊,我也會單獨抽出關於雙緩衝的技術使用,以及注意點。

1.雙緩衝的使用場景

先看看《繪圖(三,進階之繪製錶盤)》這篇文章的效果圖。
這裡寫圖片描述
為了做一個文字跟隨錶盤移動的動畫,所以設計成了上述動畫效果。但在很多實際應用外面紅色弧長和錶盤刻度是靜止不變的。
效果如下:
這裡寫圖片描述

好了,那麼先看一下上一節的原始碼,由於以下的程式碼對上一節原始碼,稍微重構了一下,不過這次寫得比上一節更加詳細了。

先看建構函式

public DoubleCacaheView(Context context, AttributeSet attrs) {
    super(context, attrs);
    WindowManager wm = (WindowManager) context
            .getSystemService(Context.WINDOW_SERVICE);
    //獲取螢幕寬度
width = getScreemWidth(wm); //獲取圓弧半徑,用於計算刻度使用 r = getRadius(width,mMargin,mMarginZhiZheng); //獲取錶盤最外圈紅色弧長繪畫範圍 mRectFPanBiaoArc = getBiaoPanArcRectF(width,mMargin); //獲取錶盤刻度繪畫範圍 mRectFPanBiaoKeDu = getBiaoPanKeDu(width,mMargin,strokeWidth); //初始化畫筆 initPaint(); //紅色弧長路徑
mPathPanBiaoArc = new Path(); //錶盤刻度路徑 mPathBiaoPanKeDu = new Path(); }

如下幾個方法的具體實現,其實就是在上一節基礎上對其進行一下重構

//繪製在Path上的文字,也是就紅色弧長的繪畫
drawTextOnPath(canvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc,mSweepAnlge);
//繪製在圓弧中心的小圓點
drawCircleInCenter(canvas,mPaint);
//繪製錶盤上的刻度
drawBianPanKeDu(canvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
//繪製指標
drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);

onDraw具體實現

protected void onDraw(Canvas canvas) {
    // TODO Auto-generated method stub
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    //繪製在Path上的文字
    drawTextOnPath(canvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc,mSweepAnlge);
    //繪製在圓弧中心的小圓點
    drawCircleInCenter(canvas,mPaint);
    //繪製錶盤上的刻度
    drawBianPanKeDu(canvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
    //繪製指標
    drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);
    mPaint.setTextSize(50);
    mPaint.setTextAlign(Align.RIGHT);
    mPaint.setStyle(Paint.Style.FILL);
    if (mFlag) {
        // 如果掃描角度小於180度,將會發生重繪
        if (mSweepAnlge <= 180) {
            canvas.drawTextOnPath("檔案" + (int) mSweepAnlge 
            + "   ", mPathPanBiaoArc,60, -60, mPaint);
            mSweepAnlge += 2;
            invalidate();
        } else { // 否則繪畫完成,停止繪畫
            mFlag = false;
            canvas.drawTextOnPath("掃面完成           "
            , mPathPanBiaoArc, 60, -60, mPaint);
            mSweepAnlge = 0;
        }
    } else {
        mPaint.setTextSize(70);
        mPaint.setStrokeWidth(1);
        mPaint.setTextAlign(Align.CENTER);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawText("當前測度:" + (int) mSweepAnlge, 
        width / 2,width / 2 + 100, mPaint);
    }
}

不知大家看出來了沒有,要想做到第二種效果,只有指標轉動,而錶盤和錶盤刻度是不隨使用者動作而發生改變的,那麼這段程式碼的執行效率並不是蠻高,對於靜止不變的繪畫能不能僅繪製一次呢?大家看這段程式碼

if (mSweepAnlge <= 180) {
    canvas.drawTextOnPath("檔案" + (int) mSweepAnlge 
    + "   ", mPathPanBiaoArc,60, -60, mPaint);
    mSweepAnlge += 2;
    invalidate();
}

當mSweepAnlge <= 180的時候都會呼叫invalidate()通知元件重繪,也就是重新執行onDraw方法,也就是說,我們每次改變的僅僅是指標的轉動,但是繪畫每次都重新繪製了錶盤最外圈的紅色弧長和錶盤刻度,當試想一下如果,如果刻度比較複雜,計算比較耗時時,那麼就會出現螢幕閃爍,非常不美觀,當然此時的效果並沒有出現螢幕閃爍,等一下我會舉例說明的,於是就引出了雙緩衝技術。

閃爍的原因

注:以下解釋基於MFC的繪畫原理,我們知道繪畫底層引擎使用的都是OpenGL,所以關於不管是在哪個平臺,繪畫原理應該是差不多的。

因為窗體在重新整理時,總要有一個擦除原來圖象的過程,它利用背景色填充窗體繪圖區,然後在呼叫新的繪圖程式碼進行重繪,這樣一擦一寫造成了圖象顏色的反差。當WM_PAINT的響應很頻繁的時候,這種反差也就越發明顯。於是我們就看到了閃爍現象。(當視窗由於任何原因需要重繪時,總是先用背景色將顯示區清除,然後才呼叫OnPaint,而背景色往往與繪圖內容反差很大,這樣在短時間內背景色與顯示圖形的交替出現,使得顯示視窗看起來在閃。如果將背景刷設定成NULL,這樣無論怎樣重繪圖形都不會閃了。當然,這樣做會使得視窗的顯示亂成一團,因為重繪時沒有背景色對原來繪製的圖形進行清除,而又疊加上了新的圖形。)

重繪的原理

重繪的原理:程式根據時間來重新整理螢幕,這個時間由機器效能決定。

雙緩衝技術

如果有一幀圖形還沒有完全繪製結束,程式就開始重新整理螢幕這樣就會造成瞬間螢幕閃爍畫面很不美觀,所以雙緩衝的技術就誕生了。

那麼在Android中怎麼使用雙緩衝技術呢?其實在最開頭就已經說明了。

先通過setBitmap方法將要繪製的所有的圖形會知道一個Bitmap上,然後再來呼叫drawBitmap方法繪製出這個Bitmap,顯示在螢幕上。

我還是先舉一個小例子來怎麼使用雙緩衝技術,然後再看如何把它運用到我們的錶盤應用專案裡來。

雙緩衝舉例

個人覺得下面一個例子非常好,我自己也有看《Android瘋狂講義》,這是上面的一個例子。
程式碼不多直接上整個原始碼了。

public class DrawView extends View {

    /**
     * 記錄手觸碰螢幕的X座標
     */
    private float preX;
    /**
     * 記錄手觸碰螢幕的Y座標
     */
    private float preY;
    /**
     * 繪製路徑
     */
    private Path mPath;
    /**
     * 畫筆
     */
    private Paint mPaint;
    /**
     * 新建立的畫布
     */
    private Canvas cacheCanvas;
    /**
     * 和cacheCanvas一起使用,將新建立畫布上的繪畫儲存在cacheBitmap物件中
     */
    private Bitmap cacheBitmap;

    public DrawView(Context context, AttributeSet attrs) {
        super(context, attrs);
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);

        //建立和螢幕一樣大小的繪畫區域
        cacheBitmap = Bitmap.createBitmap(outMetrics.widthPixels,
                outMetrics.heightPixels, Config.ARGB_8888);
        cacheCanvas = new Canvas();
        //將繪畫物件和新建立的畫布關聯起來,於是在螢幕上的繪畫將全部會知道cacaheBitmap物件中
        cacheCanvas.setBitmap(cacheBitmap);
        mPath = new Path();
        mPaint = new Paint(Paint.DITHER_FLAG); //防止抖動
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(1);
        mPaint.setStyle(Style.STROKE);
        mPaint.setDither(true);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPath.moveTo(x, y);
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            mPath.quadTo(preX, preY, x, y); //使線條更加平滑,內部運用“貝塞爾曲線”
//          mPath.lineTo( x, y);
            preX = x;
            preY = y;
            cacheCanvas.drawPath(mPath, mPaint); 
            break;
        case MotionEvent.ACTION_UP:
            cacheCanvas.drawPath(mPath, mPaint);   
            mPath.reset();
            break;
        default:
            break;
        }
        invalidate();
        return true;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        Paint bmpPaint = new Paint();
        //將cacaheBitmap繪製到該View的元件上
        canvas.drawBitmap(cacheBitmap, 0, 0, bmpPaint);
    }
}

程式碼其實很簡單就是一個繪圖的小demo,執行流程如下onTouchEvent->invalidate()(通知UI發生重繪)->onDraw。
我們所有的繪畫操作內容都儲存到了cacheBitmap物件中了,而onDraw要做的只是將bitmap物件顯示出來即可。

試想一下如果要是不使用雙緩衝的情況下,那麼每次會話的路徑都要使用path把他儲存下來,然後呼叫invalidate通知UI重繪,將path裡面的內容都繪製出來,當繪畫路徑越來越多的時候就會發現繪製速度越來越慢了,說不定會出現閃爍情況,時間再久一點而且容易造成記憶體溢位,因為path在不斷add,要儲存每一條繪製路,所以當出現這種情況時首要考慮雙緩衝技術。

**最後總結一下雙緩衝實現步驟:
  1、在記憶體中建立與畫布一致的緩衝區
  2、在緩衝區畫圖
  3、將緩衝區點陣圖拷貝到當前畫布上
  4、釋放記憶體緩衝區**

好吧,還是看看效果吧
這裡寫圖片描述

錶盤繪製優化

ok,終於可以對上一節的程式碼進行效率優化了,那麼看看優化的程式碼部分吧

新增變數

/**
 * 指標到錶盤的距離
 */
private float mMarginZhiZheng = 100.0f;

/**
 * 儲存繪畫物件
 */
private Bitmap mBitmap ;
/**
 * 先建立的畫布
 */
private Canvas cacheCanvas ;
/**
 * 螢幕高度
 */
private float height;

建構函式

public UseDoubleCacaheView(Context context, AttributeSet attrs) {
    super(context, attrs);
    WindowManager wm = (WindowManager) context
            .getSystemService(Context.WINDOW_SERVICE);
    //獲取螢幕寬度
    width = getScreemWidth(wm);
    //獲取螢幕高度
    height = getScreemHeght(wm);
    //獲取圓弧半徑,用於計算刻度使用
    r = getRadius(width,mMargin,mMarginZhiZheng);
    //獲取錶盤最外圈紅色弧長繪畫範圍
    mRectFPanBiaoArc = getBiaoPanArcRectF(width,mMargin);
    //獲取錶盤刻度繪畫範圍
    mRectFPanBiaoKeDu = getBiaoPanKeDu(width,mMargin,strokeWidth);
    //初始化畫筆
    initPaint();
    //紅色弧長路徑
    mPathPanBiaoArc = new Path();
    //錶盤刻度路徑
    mPathBiaoPanKeDu = new Path();

    /**
     * 儲存繪畫物件
     */
    mBitmap = Bitmap.createBitmap((int)width, (int)height, Bitmap.Config.ARGB_8888);
    cacheCanvas = new Canvas();
    cacheCanvas.setBitmap(mBitmap);

    //講一下不變的部分一次性繪到mBitmap物件中
    //繪製錶盤
    drawBiaoPan(cacheCanvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc);
    //繪製在圓弧中心的小圓點
    drawCircleInCenter(cacheCanvas,mPaint);
    //繪製錶盤上的刻度
    drawBianPanKeDu(cacheCanvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
}

onDraw

@Override
protected void onDraw(Canvas canvas) {
    // TODO Auto-generated method stub
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);

    canvas.drawBitmap(mBitmap,0,0,mPaint);

    //繪製指標
    drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);
    mPaint.setTextSize(50);
    mPaint.setTextAlign(Align.RIGHT);
    mPaint.setStyle(Paint.Style.FILL);
    if (mFlag) {
        // 如果掃描角度小於180度,將會發生重繪
        if (mSweepAnlge <= 180) {
            canvas.drawTextOnPath("檔案" + (int) mSweepAnlge + "   ", mPathPanBiaoArc,
                    60, -60, mPaint);
            mSweepAnlge += 2;
            invalidate();
        } else { // 否則繪畫完成,停止繪畫
            mFlag = false;
            canvas.drawTextOnPath("掃面完成           ", mPathPanBiaoArc, 60, -60, mPaint);
            mSweepAnlge = 0;
        }
    } else {
        mPaint.setTextSize(70);
        mPaint.setStrokeWidth(1);
        mPaint.setTextAlign(Align.CENTER);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawText("當前測度:" + (int) mSweepAnlge, width / 2,
                width / 2 + 100, mPaint);
    }
}

說明,如果要看到兩種效果的不同,僅在MainActivity中任意一行程式碼即可

//setContentView(R.layout.activity_no_use); //沒有使用雙緩衝
setContentView(R.layout.activity_use); //使用了雙緩衝技術