下面是項目在手機上運行的效果圖
GIF演示圖
樣式效果演示圖
實現原理分析
- 刻度線繪制:畫一個刻度線很簡單,就是canvas.drawLine,但是根據角度每30度繪制一個刻度線怎么實現呢,我們一開始想到的可能會是根據角度,利用三角函數等去計算每個刻度線的開始坐標和結束坐標,但這種方式未免過于復雜,稍有不慎就會計算錯誤。但是利用畫布的旋轉canvas.rotate就會非常的簡單,刻度線只需按照12點鐘方向繪制即可,每次繪制完一個刻度線,畫布旋轉30度,再按照12點鐘方向繪制即可。
- 指針繪制:同樣也是通過canvas.drawLine繪制3個指針,為paint設置不同的屬性實現時針,分針,秒針的顯示樣式,同理,如果我們根據角度去計算指針的坐標,那就很復雜,這里也是通過畫布的旋轉,那么旋轉的角度怎么確定呢,就是根據當前時間去確定(具體算法后面代碼中具體分析)。
- 動態:為了實現時鐘的動態轉動,我們需要在onDraw中每一秒鐘獲取一次當前時間,然后計算3個指針的旋轉角度,再繪制就行了。
這樣一分析,其實自定義時鐘很簡單,就是繪制圓,然后通過畫布的旋轉繪制刻度線和指針。
具體實現過程
-
繪制圓
//繪制圓 canvas.drawCircle(centerX, centerY, radius, circlePaint);
其中centerX和centerY為圓心,用當前控件的中心點即可,radius為圓的半徑,采用當前控件寬高的最小值/2 即可,或者自行設置。
-
繪制刻度線
12個刻度線,循環12次,每3個刻度線就是一刻鐘的刻度線,可以設置不同的樣式區分。然后根據12點鐘方向繪制刻度線。
開始x坐標:圓心x坐標;
開始y坐標:圓心y坐標-半徑+間隙;
結束x坐標:圓心x坐標;
結束y坐標:開始y坐標+刻度線長度;
每繪制完一個刻度線后,畫布就在之前的基礎上旋轉30度,繼續繪制12點鐘刻度線,這樣,刻度線就基于旋轉后的畫布繪制,也就是斜著繪制了刻度線,很方便的實現了刻度線的繪制。
這里給出主要的繪制代碼,全部代碼后面貼出
//刻度線長度 private final static int MARK_LENGTH = 20; //刻度線與圓的間隙 private final static int MARK_GAP = 12; //繪制刻度線 for (int i = 0; i < 12; i++) { if (i % 3 == 0) {//一刻鐘 markPaint.setColor(mQuarterMarkColor); } else { markPaint.setColor(mMinuteMarkColor); } canvas.drawLine( centerX, centerY - radius + MARK_GAP, centerX, centerY - radius + MARK_GAP + MARK_LENGTH, markPaint); canvas.rotate(30, centerX, centerY); } canvas.save();
-
繪制指針
繪制時針,分針,秒針,我們分別用3個canvas去繪制,最后再將這3個畫布的bitmap繪制到控件的canvas中,為的是單獨控制每個畫布的旋轉角度。
首先分析時針的指針角度,鐘一圈是12個小時,360度,那么每小時就是30度,假設當前時間的小時是h(12小時制),那么時針的旋轉角度就是h*30,同刻度線一樣,我們也不去計算該角度的指針的各種坐標,而是直接將時針的畫布旋轉h*30度,然后繪制12點鐘方向的時針就行了。
接著是分針角度,鐘一圈是60分鐘,360度,那么每分鐘就是6度,假設當前時間的分鐘是m,那么分針的旋轉角度就是m*6
最后是秒針角度,鐘一圈是60秒,360度,那么每秒就是6度,假設當前時間的秒數是s,那么秒針的旋轉角度就是s*6
分析完了時針,分針,秒針的角度獲取,那么之后就很簡單了,在onDraw中,我們每過一秒獲取一次當前時間的時分秒,按照上面的算法計算角度,然后旋轉相應的畫布,之后繪制相應的指針(當然要注意畫布的清空和還原),那么一個隨著時間的流逝而旋轉的時鐘就出來了。
這里給出繪制時針的主要代碼,其他兩個指針是類似的,具體代碼后面貼出
@Override protected void onDraw(Canvas canvas) { Calendar calendar = Calendar.getInstance(); int hour12 = calendar.get(Calendar.HOUR); int minute = calendar.get(Calendar.MINUTE); int second = calendar.get(Calendar.SECOND); //保存畫布狀態 hourCanvas.save(); //清空畫布 hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //旋轉畫布 hourCanvas.rotate(hour12 * 30, centerX, centerY); //繪制12點鐘方向的時針 hourCanvas.drawLine(centerX, centerY, centerX, centerY - hourLineLength, hourPaint); //重置畫布狀態,即撤銷之前旋轉的角度,回到未旋轉之前的狀態 hourCanvas.restore(); canvas.drawBitmap(hourBitmap, 0, 0, null); //每隔1s重新繪制 postInvalidateDelayed(1000); }
但是我們會發現有一點小小的不足,秒針是會一秒一秒的轉,但是時針和分針總是在整數位置,當過了60秒,分針才會跳到下一分鐘,當過了60分鐘,時針才會跳到下一個小時,我們平常看的時鐘都是隨著秒針的轉動,分針和時針都是有一定的偏移量的,當然我們的時鐘也要這么炫酷,那么如何計算呢?
時針:前面說過,每小時時針旋轉30度,假設當前時間的小時是h(12小時制),那么時針的旋轉角度就是h*30。那么每分鐘時針旋轉多少度呢,答案是30/60=0.5度(每小時60分鐘,每小時30度),所以時針的偏移量就是m*0.5,那么假設當前的時間是1:30,那么時針旋轉的角度就是1*30+30*0.5,就是45度,改成變量公式就是h*30+m*0.5,那么修改下上面的代碼
hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
分針:假設當前時間的分鐘是m,那么分針的旋轉角度就是m*6,每秒鐘分針旋轉6/60(每分鐘60秒,每分鐘6度),所以分針的偏移量是s*0.1,那么分針畫布旋轉的的代碼就是
minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
秒針:秒針就按照每秒鐘6度旋轉
secondCanvas.rotate(second * 6, centerX, centerY);
總結
經過上面的3個步驟,我們就繪制出了一個會慢慢移動的時鐘了。
完整的代碼和項目大家可以到我的github中查看,里面有相關的使用方法,同時這個項目上傳到了maven倉庫,可以通過gradle直接使用
compile 'com.don:clockviewlibrary:1.0.1'
github地址:https://github.com/zhijieeeeee/ClockView
完整代碼
public class ClockView extends View {
//使用wrap_content時默認的尺寸
private final static int DEFAULT_SIZE = 400;
//刻度線寬度
private final static int MARK_WIDTH = 8;
//刻度線長度
private final static int MARK_LENGTH = 20;
//刻度線與圓的距離
private final static int MARK_GAP = 12;
//時針寬度
private final static int HOUR_LINE_WIDTH = 10;
//分針寬度
private final static int MINUTE_LINE_WIDTH = 6;
//秒針寬度
private final static int SECOND_LINE_WIDTH = 4;
//圓心坐標
private int centerX;
private int centerY;
//圓半徑
private int radius;
//圓的畫筆
private Paint circlePaint;
//刻度線畫筆
private Paint markPaint;
//時針畫筆
private Paint hourPaint;
//分針畫筆
private Paint minutePaint;
//秒針畫筆
private Paint secondPaint;
//時針長度
private int hourLineLength;
//分針長度
private int minuteLineLength;
//秒針長度
private int secondLineLength;
private Bitmap hourBitmap;
private Bitmap minuteBitmap;
private Bitmap secondBitmap;
private Canvas hourCanvas;
private Canvas minuteCanvas;
private Canvas secondCanvas;
//圓的顏色
private int mCircleColor = Color.WHITE;
//時針的顏色
private int mHourColor = Color.BLACK;
//分針的顏色
private int mMinuteColor = Color.BLACK;
//秒針的顏色
private int mSecondColor = Color.RED;
//一刻鐘刻度線的顏色
private int mQuarterMarkColor = Color.parseColor("#B5B5B5");
//分鐘刻度線的顏色
private int mMinuteMarkColor = Color.parseColor("#EBEBEB");
//是否繪制3個指針的圓心
private boolean isDrawCenterCircle = false;
//獲取時間監聽
private OnCurrentTimeListener onCurrentTimeListener;
public void setOnCurrentTimeListener(OnCurrentTimeListener onCurrentTimeListener) {
this.onCurrentTimeListener = onCurrentTimeListener;
}
public ClockView(Context context) {
super(context);
init();
}
public ClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClockView);
mCircleColor = a.getColor(R.styleable.ClockView_circle_color, Color.WHITE);
mHourColor = a.getColor(R.styleable.ClockView_hour_color, Color.BLACK);
mMinuteColor = a.getColor(R.styleable.ClockView_minute_color, Color.BLACK);
mSecondColor = a.getColor(R.styleable.ClockView_second_color, Color.RED);
mQuarterMarkColor = a.getColor(R.styleable.ClockView_quarter_mark_color, Color.parseColor("#B5B5B5"));
mMinuteMarkColor = a.getColor(R.styleable.ClockView_minute_mark_color, Color.parseColor("#EBEBEB"));
isDrawCenterCircle = a.getBoolean(R.styleable.ClockView_draw_center_circle, false);
a.recycle();
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
reMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
centerX = width / 2 ;
centerY = height / 2;
radius = Math.min(width, height) / 2;
hourLineLength = radius / 2;
minuteLineLength = radius * 3 / 4;
secondLineLength = radius * 3 / 4;
//時針
hourBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
hourCanvas = new Canvas(hourBitmap);
//分針
minuteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
minuteCanvas = new Canvas(minuteBitmap);
//秒針
secondBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
secondCanvas = new Canvas(secondBitmap);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制圓
canvas.drawCircle(centerX, centerY, radius, circlePaint);
//繪制刻度線
for (int i = 0; i < 12; i++) {
if (i % 3 == 0) {//一刻鐘
markPaint.setColor(mQuarterMarkColor);
} else {
markPaint.setColor(mMinuteMarkColor);
}
canvas.drawLine(
centerX,
centerY - radius + MARK_GAP,
centerX,
centerY - radius + MARK_GAP + MARK_LENGTH,
markPaint);
canvas.rotate(30, centerX, centerY);
}
canvas.save();
Calendar calendar = Calendar.getInstance();
int hour12 = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
//(方案一)每過一小時(3600秒)時針添加30度,所以每秒時針添加(1/120)度
//(方案二)每過一小時(60分鐘)時針添加30度,所以每分鐘時針添加(1/2)度
hourCanvas.save();
//清空畫布
hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
hourCanvas.drawLine(centerX, centerY,
centerX, centerY - hourLineLength, hourPaint);
if (isDrawCenterCircle)//根據指針的顏色繪制圓心
hourCanvas.drawCircle(centerX, centerY, 2 * HOUR_LINE_WIDTH, hourPaint);
hourCanvas.restore();
//每過一分鐘(60秒)分針添加6度,所以每秒分針添加(1/10)度;當minute加1時,正好second是0
minuteCanvas.save();
//清空畫布
minuteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
minuteCanvas.drawLine(centerX, centerY,
centerX, centerY - minuteLineLength, minutePaint);
if (isDrawCenterCircle)//根據指針的顏色繪制圓心
minuteCanvas.drawCircle(centerX, centerY, 2 * MINUTE_LINE_WIDTH, minutePaint);
minuteCanvas.restore();
//每過一秒旋轉6度
secondCanvas.save();
//清空畫布
secondCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
secondCanvas.rotate(second * 6, centerX, centerY);
secondCanvas.drawLine(centerX, centerY,
centerX, centerY - secondLineLength, secondPaint);
if (isDrawCenterCircle)//根據指針的顏色繪制圓心
secondCanvas.drawCircle(centerX, centerY, 2 * SECOND_LINE_WIDTH, secondPaint);
secondCanvas.restore();
canvas.drawBitmap(hourBitmap, 0, 0, null);
canvas.drawBitmap(minuteBitmap, 0, 0, null);
canvas.drawBitmap(secondBitmap, 0, 0, null);
//每隔1s重新繪制
postInvalidateDelayed(1000);
if (onCurrentTimeListener != null) {
//小時采用24小時制返回
int h = calendar.get(Calendar.HOUR_OF_DAY);
String currentTime = intAdd0(h) + ":" + intAdd0(minute) + ":" + intAdd0(second);
onCurrentTimeListener.currentTime(currentTime);
}
}
/**
* 初始化
*/
private void init() {
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setStyle(Paint.Style.FILL);
circlePaint.setColor(mCircleColor);
markPaint = new Paint();
circlePaint.setAntiAlias(true);
markPaint.setStyle(Paint.Style.FILL);
markPaint.setStrokeCap(Paint.Cap.ROUND);
markPaint.setStrokeWidth(MARK_WIDTH);
hourPaint = new Paint();
hourPaint.setAntiAlias(true);
hourPaint.setColor(mHourColor);
hourPaint.setStyle(Paint.Style.FILL);
hourPaint.setStrokeCap(Paint.Cap.ROUND);
hourPaint.setStrokeWidth(HOUR_LINE_WIDTH);
minutePaint = new Paint();
minutePaint.setAntiAlias(true);
minutePaint.setColor(mMinuteColor);
minutePaint.setStyle(Paint.Style.FILL);
minutePaint.setStrokeCap(Paint.Cap.ROUND);
minutePaint.setStrokeWidth(MINUTE_LINE_WIDTH);
secondPaint = new Paint();
secondPaint.setAntiAlias(true);
secondPaint.setColor(mSecondColor);
secondPaint.setStyle(Paint.Style.FILL);
secondPaint.setStrokeCap(Paint.Cap.ROUND);
secondPaint.setStrokeWidth(SECOND_LINE_WIDTH);
}
/**
* 重新設置view尺寸
*/
private void reMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
if (measureWidthMode == MeasureSpec.AT_MOST
&& measureHeightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE);
} else if (measureWidthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE, measureHeight);
} else if (measureHeightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measureWidth, DEFAULT_SIZE);
}
}
public interface OnCurrentTimeListener {
void currentTime(String time);
}
/**
* int小于10的添加0
*
* @param i
* @return
*/
private String intAdd0(int i) {
DecimalFormat df = new DecimalFormat("00");
if (i < 10) {
return df.format(i);
} else {
return i + "";
}
}
}
自定義屬性
<declare-styleable name="ClockView">
<attr name="circle_color" format="color" />
<attr name="hour_color" format="color" />
<attr name="minute_color" format="color" />
<attr name="second_color" format="color" />
<attr name="quarter_mark_color" format="color" />
<attr name="minute_mark_color" format="color" />
<attr name="draw_center_circle" format="boolean" />
</declare-styleable>