https://github.com/zuohp/My_Clock.git
1. 绘制表盘和长短刻度
2. 绘制各个时间点的数字
3. 绘制时针,分针,秒针
4. 让各个指针转起来,根据时间计算角度
// 圆形和刻度的画笔、指针的画笔、数字的画笔
private Paint mCirclePaint,mPointerPaint,mNumPaint;
// 时钟的外环宽度、时钟的半径、默认刻度的宽度、默认刻度的长度
// 特殊刻度的宽度、特殊刻度的长度、时针的宽度、分针的宽度、秒针的宽度
private float mClockRingWidth,mClockRadius,mDefaultWidth,mDefaultLength,
mSpecialWidth,mSpecialLength,mHWidth,mMWidth,mSWidth;
// 圆形和刻度的颜色,时针的颜色,分针的颜色,秒针的颜色,数字的颜色
private int mCircleColor,mHColor,mMColor,mSColor,mNumColor;
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);
}
在这里本想着使用 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;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先设置画布为view的中心点
canvas.translate(mCenterX,mCenterY);
//绘制外圆和刻度
drawCircle(canvas);
//绘制数字
drawNums(canvas);
//绘制时针,分针,秒针和中间小圆点
drawPointer(canvas);
}
/**
* 绘制外圆和刻度
* @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);
}
}
有了绘制刻度的经验,我们绘制数字也可以这么干,对,就是旋转画布,省时省力,不用计算每一个数字的位置,你会发现每个数字是从左上角开始绘制的,与我们想要的效果有些偏差,所以我们可以在文字外面包一个矩形,然后我们摆正矩形的位置即可,详情请看代码。(如有好的建议烦请告知)
从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);
}
}
在这里我们只绘制时针分针和秒针,暂时不根据时间进行弧度计算,后期在启动定时器的时候会进行计算的,因为我们要实现手动设置时间,所以不可在绘制的时候把弧度计算死值,我们要动态更改。
/**
* 绘制指针,每次绘制完恢复画布状态,使用 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);
}
我们知道秒针走一圈是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();
}
};
/**
*开启定时器
*/
public void start() {
mTimer.schedule(task,0,1000);
}
(注:在此引用了 蛇发女妖 的简书里的解决方案,里面说的非常好)
蛇发女妖 的简书地址:http://www.jianshu.com/p/c2abd6226897
在这里的问题就是分针在30分的时候,时钟却还是在1点整,秒针都走了30多秒了,分针确还停在30分钟的位置上。
蛇发女妖 的简书里的原话是这么说的:
我们知道30分30秒其实就是30.5分钟,而我们计算时仅仅只算了30分钟的角度,少了那0.5分钟。所以我们还是得把传入的秒转换为分钟,即 分钟
= (分钟 + 秒 * 1.0f/60f) *6f
;同理时针的角度和分针秒针都有关,我们得把传入的分和秒也都转换为小时再计算它的角度,即 小时= (小时 + 分钟 * 1.0f/60f + 秒 * 1.0f/3600f)*30f
;
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;
}
自定义View总能让我们或多或少学到点东西,多动手多实践,共同进步!
祝:工作顺利!