下面是项目在手机上运行的效果图
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 + "";
}
}
}
自定义属性