Android自定義View之時鐘
效果(gif效果不是很好)
GitHub地址
繪製思路
1. 繪製錶盤和長短刻度
2. 繪製各個時間點的數字
3. 繪製時針,分針,秒針
4. 讓各個指標轉起來,根據時間計算角度
開始繪製
1. 定義引數(attrs檔案)
<declare-styleable name="Clock"> <!--數值--> <attr name="mClockRingWidth" format="dimension"/> <attr name="mDefaultWidth" format="dimension"/> <attr name="mDefaultLength" format="dimension"/> <attr name="mSpecialWidth" format="dimension"/> <attr name="mSpecialLength" format="dimension"/> <attr name="mHWidth" format="dimension"/> <attr name="mMWidth" format="dimension"/> <attr name="mSWidth" format="dimension"/> <!--顏色--> <attr name="mCircleColor" format="color"/> <attr name="mHColor" format="color"/> <attr name="mMColor" format="color"/> <attr name="mSColor" format="color"/> <attr name="mNumColor" format="color"/> </declare-styleable>
2. 繼承View,定義引數
// 圓形和刻度的畫筆、指標的畫筆、數字的畫筆 private Paint mCirclePaint,mPointerPaint,mNumPaint; // 時鐘的外環寬度、時鐘的半徑、預設刻度的寬度、預設刻度的長度 // 特殊刻度的寬度、特殊刻度的長度、時針的寬度、分針的寬度、秒針的寬度 private float mClockRingWidth,mClockRadius,mDefaultWidth,mDefaultLength, mSpecialWidth,mSpecialLength,mHWidth,mMWidth,mSWidth; // 圓形和刻度的顏色,時針的顏色,分針的顏色,秒針的顏色,數字的顏色 private int mCircleColor,mHColor,mMColor,mSColor,mNumColor;
3. 初始化引數以及畫筆
public Clock(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context,attrs); initPaint(); Calendar mCalendar= Calendar.getInstance(); //獲取當前小時數 int hours = mCalendar.get(Calendar.HOUR); //獲取當前分鐘數 int minutes = mCalendar.get(Calendar.MINUTE); //獲取當前秒數 int seconds=mCalendar.get(Calendar.SECOND); setTime(hours,minutes,seconds); //開啟定時 start(); } /** * 初始化自定義引數 */ private void init(Context context,AttributeSet attributeSet){ TypedArray ta = context.obtainStyledAttributes(attributeSet, R.styleable.Clock); mClockRingWidth=ta.getDimension(R.styleable.Clock_mClockRingWidth,SizeUtils.dp2px(context,4)); mDefaultWidth=ta.getDimension(R.styleable.Clock_mDefaultWidth,SizeUtils.dp2px(context,1)); mDefaultLength=ta.getDimension(R.styleable.Clock_mDefaultLength,SizeUtils.dp2px(context,8)); mSpecialWidth=ta.getDimension(R.styleable.Clock_mSpecialWidth,SizeUtils.dp2px(context,2)); mSpecialLength=ta.getDimension(R.styleable.Clock_mSpecialLength,SizeUtils.dp2px(context,14)); mHWidth=ta.getDimension(R.styleable.Clock_mHWidth,SizeUtils.dp2px(context,6)); mMWidth=ta.getDimension(R.styleable.Clock_mMWidth,SizeUtils.dp2px(context,4)); mSWidth=ta.getDimension(R.styleable.Clock_mSWidth,SizeUtils.dp2px(context,2)); //顏色 mCircleColor=ta.getColor(R.styleable.Clock_mCircleColor, Color.RED); mHColor=ta.getColor(R.styleable.Clock_mHColor, Color.BLACK); mMColor=ta.getColor(R.styleable.Clock_mMColor, Color.BLACK); mSColor=ta.getColor(R.styleable.Clock_mSColor, Color.RED); mNumColor=ta.getColor(R.styleable.Clock_mNumColor, Color.BLACK); //記得釋放 ta.recycle(); } /** * 初始化畫筆 */ private void initPaint() { //時鐘的畫筆 mCirclePaint=new Paint(); mCirclePaint.setAntiAlias(true); mCirclePaint.setStyle(Paint.Style.STROKE); //指標的畫筆 mPointerPaint=new Paint(); mPointerPaint.setAntiAlias(true); mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPointerPaint.setStrokeCap(Paint.Cap.ROUND); //數字的畫筆 mNumPaint=new Paint(); mNumPaint.setStyle(Paint.Style.FILL); mNumPaint.setTextSize(60); mNumPaint.setColor(mNumColor); }
4. 測量View並取值
在這裡本想著使用 wrap_content 模式的時候拋異常提示一下呢,後來想了想沒有做,因為時鐘這個控制元件必須要給定一個數值的,哪怕是 match_parent 也行,等View測量完畢在 onSizeChanged 方法中取到view的實際寬高,然後進行一個包括半徑的一些引數初始化。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getMeasureSize(true, widthMeasureSpec);
int height = getMeasureSize(false, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth=w;
mHeight=h;
mCenterX=w/2;
mCenterY=h/2;
mClockRadius= (float) ((float) (w/2)*0.8);
}
/**
* 獲取View尺寸
*
* @param isWidth 是否是width,不是的話,是height
*/
private int getMeasureSize(boolean isWidth, int measureSpec) {
int result = 0;
int specSize = MeasureSpec.getSize(measureSpec);
int specMode = MeasureSpec.getMode(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
if (isWidth) {
result = getSuggestedMinimumWidth();
} else {
result = getSuggestedMinimumHeight();
}
break;
case MeasureSpec.AT_MOST:
if (isWidth)
result = Math.min(specSize, mWidth);
else
result = Math.min(specSize, mHeight);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
5. onDraw 方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先設定畫布為view的中心點
canvas.translate(mCenterX,mCenterY);
//繪製外圓和刻度
drawCircle(canvas);
//繪製數字
drawNums(canvas);
//繪製時針,分針,秒針和中間小圓點
drawPointer(canvas);
}
-
5.1 繪製外圓和刻度(分特殊刻度和普通刻度,顏色分別設定)
/**
* 繪製外圓和刻度
* @param canvas
*/
private void drawCircle(Canvas canvas) {
mCirclePaint.setStrokeWidth(mClockRingWidth);
mCirclePaint.setColor(mCircleColor);
//先畫出圓環
canvas.drawCircle(0,0,mClockRadius,mCirclePaint);
for (int i = 0; i < 60; i++) {
if (i%5==0){//特殊的刻度
mCirclePaint.setStrokeWidth(mSpecialWidth);
mCirclePaint.setColor(mHColor);
canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mSpecialLength,mCirclePaint);
}else {//普通刻度
mCirclePaint.setStrokeWidth(mDefaultWidth);
mCirclePaint.setColor(mSColor);
canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mDefaultLength,mCirclePaint);
}
// 通過旋轉畫布的方式快速設定刻度
canvas.rotate(6);
}
}
此時的效果
-
5.2 繪製數字
有了繪製刻度的經驗,我們繪製數字也可以這麼幹,對,就是旋轉畫布,省時省力,不用計算每一個數字的位置,你會發現每個數字是從左上角開始繪製的,與我們想要的效果有些偏差,所以我們可以在文字外面包一個矩形,然後我們擺正矩形的位置即可,詳情請看程式碼。(如有好的建議煩請告知)
從12點鐘方向開始,每次旋轉畫布30度,繪製數字,繪製完之後你會發現有的數字是倒著的,這裡需要我們特殊處理一下,詳細請看程式碼。
/**
* 繪製文字
* @param canvas
*/
private void drawNums(Canvas canvas) {
for (int i = 0; i < 12; i++) {
canvas.save();
if (i==0){ //繪製12點的數字
Rect textBound = new Rect();
canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
String text="12";
mNumPaint.getTextBounds(text, 0, text.length(), textBound);
canvas.drawText(text, -textBound.width()/2,
textBound.height() / 2, mNumPaint);
}else { //繪製其他數字
Rect textBound = new Rect();
canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
String text=i+"";
mNumPaint.getTextBounds(text, 0, text.length(), textBound);
canvas.rotate(-i*30); //因畫布被旋轉了,所以要把畫布正過來再繪製數字
canvas.drawText(text, -textBound.width()/2,
textBound.height() / 2, mNumPaint);
}
canvas.restore();
canvas.rotate(30);
}
}
此時的效果
-
5.3 繪製時針和分針和秒針
在這裡我們只繪製時針分針和秒針,暫時不根據時間進行弧度計算,後期在啟動定時器的時候會進行計算的,因為我們要實現手動設定時間,所以不可在繪製的時候把弧度計算死值,我們要動態更改。
/**
* 繪製指標,每次繪製完恢復畫布狀態,使用 save 和 restore 方法
* 指標長短根據半徑長度進行計算
* @param canvas
*/
private void drawPointer(Canvas canvas) {
//時針
canvas.save();
mPointerPaint.setColor(mHColor);
mPointerPaint.setStrokeWidth(mHWidth);
canvas.rotate(mH, 0, 0);
canvas.drawLine(0, -20, 0,
(float) (mClockRadius*0.45), mPointerPaint);
canvas.restore();
// 分針
canvas.save();
mPointerPaint.setColor(mMColor);
mPointerPaint.setStrokeWidth(mMWidth);
canvas.rotate(mM, 0, 0);
canvas.drawLine(0, -20, 0,
(float) (mClockRadius*0.6), mPointerPaint);
canvas.restore();
//秒針
canvas.save();
mPointerPaint.setColor(mSColor);
mPointerPaint.setStrokeWidth(mSWidth);
canvas.rotate(mS, 0, 0);
canvas.drawLine(0, -40, 0,
(float) (mClockRadius*0.75), mPointerPaint);
canvas.restore();
//最後繪製一個小圓點,要不然沒效果
mPointerPaint.setColor(mSColor);
canvas.drawCircle(0,0,mHWidth/2,mPointerPaint);
}
此時的效果
6. 根據時間計算指標弧度,讓指標動起來
我們知道秒針走一圈是60秒,而圓的一圈是360度,那麼
秒針一秒鐘旋轉:360 / 60 = 6度
分針一秒鐘旋轉:360 / 60 / 60 = 0.1 度
時針一秒鐘旋轉:360 / (12*3600) = 1 / 120 = 0.0083度
/**
* 定時器
*/
private Timer mTimer=new Timer();
private TimerTask task = new TimerTask() {
@Override
public void run() {
if (mS == 360) {
mS = 0;
}
if (mM == 360){
mM = 0;
}
if (mH == 360){
mH = 0;
}
//具體計算
mS = mS + 6;
mM = mM + 0.1f;
mH = mH + 1.0f/120;
//子執行緒用postInvalidate
postInvalidate();
}
};
-
6.1 開啟定時後時鐘就動起來了
/**
*開啟定時器
*/
public void start() {
mTimer.schedule(task,0,1000);
}
7. 開始處理手動設定時間
(注:在此引用了 蛇髮女妖 的簡書裡的解決方案,裡面說的非常好)
在這裡的問題就是分針在30分的時候,時鐘卻還是在1點整,秒針都走了30多秒了,分針確還停在30分鐘的位置上。
如圖:
蛇髮女妖 的簡書裡的原話是這麼說的:
我們知道30分30秒其實就是30.5分鐘,而我們計算時僅僅只算了30分鐘的角度,少了那0.5分鐘。所以我們還是得把傳入的秒轉換為分鐘,即 分鐘
= (分鐘 + 秒 * 1.0f/60f) *6f
;同理時針的角度和分針秒針都有關,我們得把傳入的分和秒也都轉換為小時再計算它的角度,即 小時= (小時 + 分鐘 * 1.0f/60f + 秒 * 1.0f/3600f)*30f
;
8. 手動設定時間的最終解決方案
public void setTime(int h, int m, int s) {
if (h >= 24 || h < 0 || m >= 60 || m < 0 || s >= 60 || s < 0) {
Toast.makeText(getContext(), "時間不正確", Toast.LENGTH_SHORT).show();
return;
}
//需要以12點為準,所以統一減去180度
if (h >= 12) {
mH = (h + m * 1.0f/60f + s * 1.0f/3600f - 12)*30f-180;
} else {
mH = (h + m * 1.0f/60f + s * 1.0f/3600f)*30f-180;
}
mM = (m + s * 1.0f/60f) *6f-180;
mS = s * 6f-180;
}
9. 總結
自定義View總能讓我們或多或少學到點東西,多動手多實踐,共同進步!
祝:工作順利!