這篇文章是基于以下兩篇文章的實(shí)踐:
1.自定義View - 基礎(chǔ)
2.自定義View - Canvas - 圖形繪制
3.自定義 View - Canvas - 畫布操作和快照

時(shí)鐘的大致效果如上,用到的主要有圖形的繪制,畫布操作和快照。可能還是有點(diǎn)丑,唉,我發(fā)誓最后會(huì)做一個(gè) B 格高的東西。
下面來說說這個(gè)自定義 View。
需要實(shí)現(xiàn)一個(gè)視圖之前,我們首先要將它拆分成各個(gè)模塊,這樣能方便我們構(gòu)思各個(gè)模塊的實(shí)現(xiàn)方式。
而這個(gè)時(shí)鐘我把它分為兩部分:表盤和時(shí)針。其中,表盤分為三部分,圓、刻度、數(shù)字;指針有三個(gè),時(shí)、分、秒。
1.初始化數(shù)據(jù)
繪制時(shí)鐘我們需要的主要有這樣一些數(shù)據(jù):
- 畫筆
- 時(shí)分秒
- 刻度
- 表盤
- 數(shù)字
- 數(shù)據(jù)
- 表盤大小
- 刻度長度
- 指針長度
- 指針多余長度
- 當(dāng)前時(shí)間
這里我做了簡單的初始化:
private void init() {
initBasePaint();
initClockPaint();
initHourPaint();
initMinPaint();
initSecPaint();
initTextPaint();
//表盤半徑
mRadio = 400;
//刻度長度
mSmallTick = 20;
mMidTick = 40;
mBigTick = 60;
//指針長度
mHourLen = 210;
mMinLen = 280;
mSecLen = 350;
//指針多余長度
mHourBegin = 70;
mMinBegin = 70;
mSecBegin = 70;
//時(shí)間
mHour = 0;
mMin = 0;
mSec = 0;
}
2.繪制表盤
1)繪制圓
我們可以看到整個(gè)時(shí)鐘的中心始終在畫布正中央,因此第一步,我們先將畫布移到中間。為了獲取移動(dòng)到畫布正中間需要的像素?cái)?shù),我們要先從 onSizeChange 方法中,獲取圖形大小,然后移動(dòng)到畫布中間繪制圓。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//移動(dòng)到中心
canvas.translate(mWidth / 2, mHeight / 2);
//表盤
canvas.drawCircle(0, 0, mRadio, mClockPaint);
}
2)繪制刻度
顯然,我們不能通過三角函數(shù)來確定每個(gè)刻度的位置,那樣問題也變得困難。這里我們借助畫布的旋轉(zhuǎn)來實(shí)現(xiàn)。計(jì)算出最小格的角度為 6 度,因此,我們只要旋轉(zhuǎn) 60 次。由于特殊位置需要有長度不同的線段標(biāo)志,因此這里也需要加入判斷。另外,刻度的繪制是旋轉(zhuǎn) 360 度的,因此并不需要畫布快照來保留當(dāng)前狀態(tài)。
//刻度
for (int i = 0; i < 60; i++) {
int tick = i % 15 == 0 ? // 3 6 9 12
mBigTick :
(i % 5 == 0 ? // 1 2 4 5 7 8 10 11
mMidTick : mSmallTick); // 分
canvas.drawLine(0, mRadio, 0, mRadio - tick, mClockPaint);
canvas.drawLine(0, -mRadio, 0, -mRadio + tick, mClockPaint);
canvas.rotate(6);
}
3)繪制指針
由于時(shí)分秒的“0”刻度在 12 的位置,因此,繪制指針的過程中,要以 12 到圓心的連線為準(zhǔn)。
//指針
//時(shí)
canvas.save();
canvas.rotate(mHour % 12 / 12 * 360 + mMin % 60 / 60 * 30);// 計(jì)算需要轉(zhuǎn)過的角度
canvas.drawLine(0, mHourBegin, 0, -mHourLen + mHourBegin, mHourPaint);
canvas.restore();
//分
canvas.save();
canvas.rotate(mMin % 60 / 60 * 360 + mSec % 60 / 60 * 6);// 計(jì)算需要轉(zhuǎn)過的角度
canvas.drawLine(0, mMinBegin, 0, -mMinLen + mMinBegin, mMinPaint);
canvas.restore();
//秒
canvas.save();
canvas.rotate(mSec % 60 / 60 * 360);// 計(jì)算需要轉(zhuǎn)過的角度
canvas.drawLine(0, mSecBegin, 0, -mSecLen + mSecBegin, mSecPaint);
canvas.restore();
4)事件
視圖已經(jīng)繪制好了,下面需要讓這個(gè)視圖動(dòng)起來,這個(gè)比較簡單,利用 Handler 就可以實(shí)現(xiàn)了。這里我們定義了四個(gè)狀態(tài),分別對(duì)應(yīng)開始、停止、重置、繼續(xù)。
public static final int STEP_START = 0;
public static final int STEP_STOP = 1;
public static final int STEP_RESET = 2;
public static final int STEP_NEXT = 3;
在 handler 中處理事件:
private boolean bContinue;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case STEP_START:
bContinue = true;
sendEmptyMessage(STEP_NEXT);
break;
case STEP_NEXT:
if (bContinue) {
timeUp();
invalidate();
sendEmptyMessageDelayed(STEP_NEXT, 1 * 1000);
}
break;
case STEP_STOP:
bContinue = false;
handler.removeCallbacksAndMessages(null);
break;
case STEP_RESET:
bContinue = true;
mHour = 0;
mMin = 0;
mSec = 0;
handler.removeCallbacksAndMessages(null);
sendEmptyMessage(STEP_NEXT);
break;
}
}
};
暴露出控制時(shí)間的方法:
public void setTime(int h, int m, int s) {
mHour = h;
mMin = m;
mSec = s;
invalidate();
}
public void start() {
handler.sendEmptyMessage(STEP_START);
}
public void stop(){
handler.sendEmptyMessage(STEP_STOP);
}
public void reset(){
handler.sendEmptyMessage(STEP_RESET);
}