Android UI編程進階——使用SurfaceViewt和Canvas實現動態時鐘,androidcanvas
概述:
很多時候我們想要自己寫一些類似時鐘、羅盤的控制項,卻又找不到合適的Demo。我想這時你可能索性就直接片了。在Android有Canvas和Paint這麼好的畫師的情況下,還是選擇使用圖片,的確是有一些尷尬了。下面我就利用一步一步實現自訂時鐘來對這個問題做一個講解。
錯誤樣本:
這裡我有一個“錯誤”的樣本。這裡的錯誤其實應該是要打上雙引號的,因為它不是真的錯誤,只是在某些時候,它是不適當的。下面就讓我們先來學習一下這個樣本,瞭解一下這個樣本中哪些是不適合使用的技術。
展示:
看了上面的兩張運行我們可以看到很正常的兩張運行圖,不過這不是全部。錯誤資訊下面再進行展示和分析。在這裡我就來解釋一下為什麼說這個樣本不是全錯,只是不恰當的原因。因為如果我們的需求是不用變化的圖形,例如一些多邊形的展示等,不需要即時去重新整理介面,OK,這個樣本沒有任何問題,而且使用簡單。針對這一點,我想也是有必要附上代碼來展示一下實現過程。
靜態畫圖代碼:
public class CustomCanvasView extends View { private static final String TAG = CustomCanvasView.class.getName(); private Paint paint; private int mRadius; private Canvas mCanvas; private int mHours; private int mMinutes; private int mSeconds; private Thread mThread; public CustomCanvasView(Context context, int radius) { super(context); paint = new Paint(); paint.setColor(Color.RED); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeWidth(5); mRadius = radius; } // 在這裡我們將測試canvas提供的繪製圖形方法 @Override protected void onDraw(Canvas canvas) { mCanvas = canvas; drawCompass(mCanvas); refreshClock(); } private void refreshClock() { mThread = new Thread() { @Override public void run() { try { while (true) { handler.sendEmptyMessage(0x123); sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } }; mThread.start(); } Handler handler = new Handler() { public void handleMessage(Message msg) { Calendar c = Calendar.getInstance(); mHours = c.getTime().getHours(); mMinutes = c.getTime().getMinutes(); mSeconds = c.getTime().getSeconds(); invalidate(); c = null; }; }; /** * 繪製羅盤 * 2015-2-3 */ private void drawCompass(Canvas canvas) { paint.setAntiAlias(true); paint.setStyle(Style.STROKE); canvas.translate(canvas.getWidth() / 2, mRadius + 300); // 平移羅盤 canvas.drawCircle(0, 0, mRadius, paint); // 畫圓圈 // 使用path繪製路徑文字 canvas.save(); drawLabel(canvas); canvas.restore(); drawDividing(canvas); drawMinuteHand(canvas, 0); canvas = null; } /** * 繪製羅盤內側的標籤文本 * 2015-2-4 */ private void drawLabel(Canvas canvas) { canvas.translate(-155, -155); Path path = new Path(); path.addArc(new RectF(0, 0, mRadius + 100, mRadius + 100), -180, 180); Paint citePaint = new Paint(paint); citePaint.setTextSize(30); citePaint.setStrokeWidth(1); canvas.drawTextOnPath("http://blog.csdn.net/lemon_tree", path, 35, 0, citePaint); path = null; citePaint = null; canvas = null; } /** * 繪製刻度 * 2015-2-4 */ private void drawDividing(Canvas canvas) { Paint divdPaint = new Paint(paint); // 小刻度畫筆對象 divdPaint.setStrokeWidth(1); divdPaint.setTextSize(20); float y = mRadius; int count = 60; // 總刻度數 canvas.rotate(35 * 360 / count, 0f, 0f); for (int i = 0; i < count; i++) { if (i % 5 == 0) { canvas.drawLine(0f, y, 0, y + 20f, paint); canvas.drawText(String.valueOf(i / 5 + 1), -4f, y + 55f, divdPaint); } else { canvas.drawLine(0f, y, 0f, y + 15f, divdPaint); } canvas.rotate(360 / count, 0f, 0f); // 旋轉畫紙 } divdPaint = null; canvas = null; } /** * 繪製分針 * 2015-2-4 */ private void drawMinuteHand(Canvas canvas, int second) { Paint tmpPaint = new Paint(paint); tmpPaint.setStrokeWidth(2); tmpPaint.setTextSize(30); tmpPaint.setColor(Color.GRAY); tmpPaint.setStrokeWidth(4); canvas.drawCircle(0, 0, 10, tmpPaint); tmpPaint.setStyle(Style.FILL); tmpPaint.setColor(Color.YELLOW); canvas.drawCircle(0, 0, 5, tmpPaint); canvas.rotate(mSeconds * 6, 0f, 0f); canvas.drawLine(0, 20, 0, -135, paint); tmpPaint = null; canvas = null; }}
錯誤記錄檔展示及原因分析:
說實話上面兩張圖看上去真的很棒,可是如果你下載了我的源碼並運行之後,你可能就會發現,在你的指標走了大概20秒的時候,程式就掛了。查看日誌就會發現如下錯誤資訊:
是不是有一種又是該死的OOM問題的感覺,說實話我也是這種感覺。這可能是因為invalidate()的時候沒有清理回收資源的問題,而且這裡的自訂控制項是繼承View,沒有採用雙緩衝技術,致使程式崩潰。而此處的資源回收我也做了一些努力,可是問題依舊存在。於是我就開始找尋另一條路徑來解決問題——SurfaceView。
----------------------------------------- Split -------------------------------------------
正確樣本:前置知識學習——髒矩形:
所謂髒矩形重新整理,意為僅重新整理有新變化的部分所在的矩形地區,而其他沒用的部分就不去重新整理,以此來減少資源浪費。我們可以通過在擷取Canvas畫布時,為其指派一個參數來聲明我們需要畫布哪個局部,這樣就可以只獲得這個部分的控制權.(參考來自:http://www.linuxidc.com/Linux/2012-02/54367.htm)本例中,使用的是全域重新整理。
前置知識學習——雙緩衝:
關於雙緩衝的概念,這裡引用一下百度百科的說明(點擊進入)。
如果要按照我的理解來通俗地講一遍的話,我想應該是這個樣子的:有一個暗房,裡面有一個功能深厚的畫師,他負責繪製圖畫。暗房對外提供了一個小視窗,這個小視窗是用來展示畫師畫出來的圖畫。這個暗房裡還有一個畫師的助理,他負責把畫師畫出來的圖畫以一定速度展示在這個小視窗上(這邊的一定速度肯定是比畫師繪畫的速度要慢一些)。
執行個體示範:運行展示:
看到以上的運行是不感覺很炫?(註:上面的GIF看上去有些怪,那是因為本人沒截好的原因,運行程式的時候會流暢很多)寫出來的時候,我也感覺比用圖片實現的要好很多。接下來就來慢慢學習一下實現它的過程吧。
首先要做的事
1.extends SurfaceView
2.implements SurfaceHolder.Callback
3.自訂一個Thread
第二步:邏輯功能實現
基於上一個不恰當的版本,這裡對上面的邏輯功能進行一些引用。
繪製秒針:
/** * 繪製秒針 * 2015-2-4 */ private void drawSecondHand(Canvas canvas) { Paint handPaint = new Paint(mPaint); handPaint.setStrokeWidth(2); handPaint.setStyle(Style.FILL); int angle = (mSeconds + 25) * 6; // 計算角度 canvas.rotate(angle, 0f, 0f); canvas.drawLine(0, 20, 0, -135, mPaint); }
繪製分針:
/** * 繪製分針 * 2015-2-4 */ private void drawMinuteHand(Canvas canvas) { Paint handPaint = new Paint(mPaint); handPaint.setStrokeWidth(2); handPaint.setStyle(Style.FILL); canvas.save(); int angle = (mMinutes + 25) * 6; // 計算角度 canvas.rotate(angle, 0f, 0f); canvas.drawLine(0, 20, 0, -110, mPaint); canvas.restore(); }從秒針到分針代碼明顯多了幾行,而這多出來的幾行代碼有什麼作用呢?
在繪製分針的時候我們可以看到這樣一句:canvas.rotate(angle, 0f, 0f);它的作用是將畫布旋轉angle度,而如果我們在繪製分針的時候不對畫布作一個狀態儲存,那下次在繪製時針的時候將是旋轉之後所做的邏輯,為了避免這些不必要的麻煩,我們需要對其先儲存後再複原處理。
繪製時針:
/** * 繪製時針 * 2015-2-4 */ private void drawHourHand(Canvas canvas) { Paint handPaint = new Paint(mPaint); handPaint.setStyle(Style.FILL); handPaint.setStrokeWidth(8); canvas.save(); int angle = (((mHours % 12) * 5 + 25) * 6) + (mMinutes * 6 * 5 / 60); // 計算角度 canvas.rotate(angle, 0f, 0f); canvas.drawLine(0, 20, 0, -90, handPaint); canvas.restore(); }時針的繪製和分針幾乎一致,唯一要注意的是繪製時針時角度的計算。如果你這裡只按小時數來計算,那它永遠都是指向大刻度。永遠不會指向兩個大刻度之間的部分,為瞭解決這個問題,我們需要加上分鐘數一起計算。即加了n分鐘下時針又位移了多少角度。
自訂Thread
使用SurfaceView需要用到一個鎖的機制。也就是說我這邊繪圖的時候,不允許被打擾,有一個獨佔的概念。可以通過以下代碼實現:
class DrawThread extends Thread { private SurfaceHolder holder; public boolean isRun; public DrawThread(SurfaceHolder holder) { this.holder = holder; isRun = true; } @Override public void run() { while (isRun) { Canvas canvas = null; try { synchronized (holder) { canvas = holder.lockCanvas(null); canvas.drawColor(Color.BLACK); drawClock(canvas); holder.unlockCanvasAndPost(canvas); // 解鎖畫布,提交畫好的映像 Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } }大家可以看到在我上完鎖之後,對畫布有一行canvas.drawColor(Color.BLACK);的代碼操作。我想你應該是明白為什麼的。對!就是清屏!如果沒有這一句代碼,上一次繪製的圖形沒有被清除,這讓整個介面感覺起來很淩亂。下面就讓我們一起來感受一下在沒有清屏且只有一根指標的情況下,Canvas動態繪製出來的圖形。
大家可以明顯看到時鐘內側的那一行Label,白色的部分在一點一點地加深,這就有力地說明了是因為上一次圖形的殘留導致的。
好了,利用SurfaceView和Canvas對自訂時鐘的學習就到這裡了,如果你還有一些不太明白的地方,歡迎前往我的上一篇部落格《Android自訂控制項前置基礎知識學習(一)——Canvas》進行學習,或以評論的方式與我進行交流。