Android UI编程进阶——使用SurfaceViewt和Canvas实现动态时钟
概述:
? ? 很多時候我們想要自己寫一些類似時鐘、羅盤的控件,卻又找不到合適的Demo。我想這時你可能索性就直接上圖片了。在Android有Canvas和Paint這么好的畫師的情況下,還是選擇使用圖片,的確是有一些尷尬了。下面我就利用一步一步實現(xiàn)自定義時鐘來對這個問題做一個講解。
錯誤示例:
? ? 這里我有一個“錯誤”的示例。這里的錯誤其實應(yīng)該是要打上雙引號的,因為它不是真的錯誤,只是在某些時候,它是不適當(dāng)?shù)?。下面就讓我們先來學(xué)習(xí)一下這個示例,了解一下這個示例中哪些是不適合使用的技術(shù)。
效果圖展示:
?
看了上面的兩張運行效果圖我們可以看到很正常的兩張運行圖,不過這不是全部。錯誤信息下面再進(jìn)行展示和分析。在這里我就來解釋一下為什么說這個示例不是全錯,只是不恰當(dāng)?shù)脑?。因為如果我們的需求是不用變化的圖形,例如一些多邊形的展示等,不需要實時去刷新界面,OK,這個示例沒有任何問題,而且使用簡單。針對這一點,我想也是有必要附上代碼來展示一下實現(xiàn)過程。
靜態(tài)畫圖代碼:
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提供的繪制圖形方法@Overrideprotected void onDraw(Canvas canvas) {mCanvas = canvas;drawCompass(mCanvas);refreshClock();}private void refreshClock() {mThread = new Thread() {@Overridepublic 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;}/*** 繪制羅盤內(nèi)側(cè)的標(biāo)簽文本* 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; // 總刻度數(shù)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); // 旋轉(zhuǎn)畫紙}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;} }
錯誤日志展示及原因分析:
說實話上面兩張圖看上去真的很棒,可是如果你下載了我的源碼并運行之后,你可能就會發(fā)現(xiàn),在你的指針走了大概20秒的時候,程序就掛了。查看日志就會發(fā)現(xiàn)如下錯誤信息:
是不是有一種又是該死的OOM問題的感覺,說實話我也是這種感覺。這可能是因為invalidate()的時候沒有清理回收資源的問題,而且這里的自定義控件是繼承View,沒有采用雙緩沖技術(shù),致使程序崩潰。而此處的資源回收我也做了一些努力,可是問題依舊存在。于是我就開始找尋另一條路徑來解決問題——SurfaceView。
----------------------------------------- Split -------------------------------------------
正確示例:
前導(dǎo)知識學(xué)習(xí)——臟矩形:
? ? 所謂臟矩形刷新,意為僅刷新有新變化的部分所在的矩形區(qū)域,而其他沒用的部分就不去刷新,以此來減少資源浪費。我們可以通過在獲取Canvas畫布時,為其指派一個參數(shù)來聲明我們需要畫布哪個局部,這樣就可以只獲得這個部分的控制權(quán).(參考來自:http://www.linuxidc.com/Linux/2012-02/54367.htm)本例中,使用的是全局刷新。
前導(dǎo)知識學(xué)習(xí)——雙緩沖:
? ? 關(guān)于雙緩沖的概念,這里引用一下百度百科的說明(點擊進(jìn)入)。
? ? 如果要按照我的理解來通俗地講一遍的話,我想應(yīng)該是這個樣子的:有一個暗房,里面有一個功能深厚的畫師,他負(fù)責(zé)繪制圖畫。暗房對外提供了一個小窗口,這個小窗口是用來展示畫師畫出來的圖畫。這個暗房里還有一個畫師的助理,他負(fù)責(zé)把畫師畫出來的圖畫以一定速度展示在這個小窗口上(這邊的一定速度肯定是比畫師繪畫的速度要慢一些)。
實例示范:
運行效果圖展示:
?
看到以上的運行效果圖是不感覺很炫?(注:上面的GIF看上去有些怪,那是因為本人截圖沒截好的原因,運行程序的時候會流暢很多)寫出來的時候,我也感覺比用圖片實現(xiàn)的要好很多。接下來就來慢慢學(xué)習(xí)一下實現(xiàn)它的過程吧。
首先要做的事
1.extends SurfaceView
2.implements?SurfaceHolder.Callback
3.自定義一個Thread
第二步:邏輯功能實現(xiàn)
基于上一個不恰當(dāng)?shù)陌姹?#xff0c;這里對上面的邏輯功能進(jìn)行一些引用。
繪制秒針:
/*** 繪制秒針* 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);它的作用是將畫布旋轉(zhuǎn)angle度,而如果我們在繪制分針的時候不對畫布作一個狀態(tài)保存,那下次在繪制時針的時候?qū)⑹切D(zhuǎn)之后所做的邏輯,為了避免這些不必要的麻煩,我們需要對其先保存后再復(fù)原處理。
繪制時針:
/*** 繪制時針* 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();} 時針的繪制和分針幾乎一致,唯一要注意的是繪制時針時角度的計算。如果你這里只按小時數(shù)來計算,那它永遠(yuǎn)都是指向大刻度。永遠(yuǎn)不會指向兩個大刻度之間的部分,為了解決這個問題,我們需要加上分鐘數(shù)一起計算。即加了n分鐘下時針又偏移了多少角度。
自定義Thread
使用SurfaceView需要用到一個鎖的機制。也就是說我這邊繪圖的時候,不允許被打擾,有一個獨占的概念。可以通過以下代碼實現(xiàn):
class DrawThread extends Thread {private SurfaceHolder holder;public boolean isRun;public DrawThread(SurfaceHolder holder) {this.holder = holder;isRun = true;}@Overridepublic 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);的代碼操作。我想你應(yīng)該是明白為什么的。對!就是清屏!如果沒有這一句代碼,上一次繪制的圖形沒有被清除,這讓整個界面感覺起來很凌亂。下面就讓我們一起來感受一下在沒有清屏且只有一根指針的情況下,Canvas動態(tài)繪制出來的圖形。 ?? 大家可以明顯看到時鐘內(nèi)側(cè)的那一行Label,白色的部分在一點一點地加深,這就有力地說明了是因為上一次圖形的殘留導(dǎo)致的。
好了,利用SurfaceView和Canvas對自定義時鐘的學(xué)習(xí)就到這里了,如果你還有一些不太明白的地方,歡迎前往我的上一篇博客《Android自定義控件前導(dǎo)基礎(chǔ)知識學(xué)習(xí)(一)——Canvas》進(jìn)行學(xué)習(xí),或以評論的方式與我進(jìn)行交流。
總結(jié)
以上是生活随笔為你收集整理的Android UI编程进阶——使用SurfaceViewt和Canvas实现动态时钟的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android自定义控件前导基础知识学习
- 下一篇: Android中对APK进行反编译