繪圖(四,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); //使用了雙緩衝技術