来来来,先看看效果。
记得在学校学习的时候也写过一个C语言的手表,不过指针好像一直转不准,想想还是当年太年轻~囧~~
最近在研究canvas,看到某人写的例子里面也有只手表,今天便花了点时间自己重新写了一个,当是练手了。
例子本身没什么难的,初学者应该都看得懂。
首先拆分这个view:
所以onDraw里面的内容是这样:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.mTotalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
this.mTotalHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop();
int smallerSize = this.mTotalWidth < this.mTotalHeight ? this.mTotalWidth : mTotalHeight;
mStandard = smallerSize >> 1;
//将画布移至中心
canvas.translate(getPaddingLeft() + this.mTotalWidth >> 1, getPaddingTop() + this.mTotalHeight >> 1);
drawAppearance(canvas);
drawTime(canvas);
if (!mIsStop) {
postInvalidateDelayed(INTERVAL_REFRESH);
}
}
第5、6行是为了计算padding;因为手表是圆形,所以表应该是画在一个长宽均为表的直径的正方形内,所以第8行取了view长度、宽度中的一个较小值。
同时为了保证view的扩展性,手表的半径、指针的长度等都没有写固定值,而是根据view 的大小按比例来计算的(mStandard即作为整个view其他部分的长度参考),所以你就能看到上面那个gif中有三种不同尺寸的大小,除了我改了下整点时刻的字体大小外,没有改过其他任何地方的一句代码。
如果你愿意,当然也可以把字体做成适配view大小的,或者干脆写一个属性,在布局文件中赋值然后再代码中获取。
第14行中,有个drawAppearance(canvas);方法,主要是画表的刻度等。
/** * 画外观,包括刻度和整点时刻的文字 */
private void drawAppearance(Canvas canvas) {
//表的半径为标准长度值的0.8,留点空隙
float radius = mStandard * 0.8f;
//只描边,不填充
mPaint.setStyle(Paint.Style.STROKE);
//画表的大圆
canvas.drawCircle(0, 0, radius, mPaint);
//画表的圆心
canvas.drawCircle(0, 0, 5, mPaint);
canvas.save();
drawCalibration(canvas, radius);
canvas.restore();
}
可以看到,表的半径是上面那个mStandard值的0.8,第7行画表最外面的圆,第9行画圆心,12行开始画刻度。因为 drawCalibration(canvas, radius);方法中涉及到了canvas的旋转等操作,为了避免对后续的操作造成影响,所以在该方法的调用过后还原了那些操作,因此你可以看到有save、restore的调用。
画刻度的方法drawCalibration(canvas, radius);代码如下:
/** * 画刻度 */
private void drawCalibration(Canvas canvas, float radius) {
//整点时刻文字的大小
mPaint.setTextSize(DensityUtil.sp2px(mContext, TIME_NUMBER_SIZE));
//使文字居中
mPaint.setTextAlign(Paint.Align.CENTER);
int calibrationHour = DensityUtil.dp2px(mContext, CALIBRATION_LENGTH_HOUR);
int calibrationMinute = DensityUtil.dp2px(mContext, CALIBRATION_LENGTH_MINUTE);
int diver = DensityUtil.dp2px(mContext, CALIBRATION_TEXT_DIVER);
//使表的12点在正上方
canvas.rotate(210);
for (int i = 0; i < 60; i++) {
int length = calibrationMinute;
if (i % 5 == 0) {
length = calibrationHour;
canvas.drawText(i / 5 + 1 + "", 0, radius - CALIBRATION_LENGTH_HOUR - diver, mPaint);
}
canvas.drawLine(0, radius, 0, radius - length, mPaint);
canvas.rotate(6, 0, 0);
}
}
可以看到第7行使用了工具类,将sp转成px。做过自定义控件的朋友应该都知道,直接写数值是不科学的,这样在不同屏幕上面的显示效果很差,无法适配,应该使用sp转换成px的方式来设置字体的大小。dp同理,其实和布局文件中使用dp、sp是一个道理。
下面画刻度的过程就是每次旋转画布6°,重复60次的过程。
类似于旋转等的这些操作就像是虽然你的笔还是原来的地方画,但是画纸旋转或者移动了,所以你画的东西的方位就产生变化。
对于后面的过程,聪明的你想必已经无需我多做解释了。
画完刻度画指针,来看onDraw方法中的drawTime:
private void drawTime(final Canvas canvas) {
Calendar calendar = Calendar.getInstance();
canvas.save();
drawPointer(canvas, calendar);
drawDigitalTime(canvas, calendar);
canvas.restore();
}
这里有一个画指针时间和一个画数字时间的,先看drawPointer(canvas, calendar);
/** * 画指针 */
private void drawPointer(Canvas canvas, Calendar calendar) {
canvas.save();
canvas.rotate(270);
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
//每小时所占的角度为30度
float degree = (hour + minute / 60.0f) * 30;
canvas.save();
canvas.rotate(degree);
canvas.drawLine(0, 0, mStandard * POINTER_LENGTH_HOUR, 0, mPaint);
canvas.restore();
//每分钟所占的角度为6度
degree = (minute + second / 60.0f) * 6;
canvas.save();
canvas.rotate(degree);
canvas.drawLine(0, 0, mStandard * POINTER_LENGTH_MINUTE, 0, mPaint);
canvas.restore();
//每秒钟所占的角度为6度
degree = second * 6;
canvas.save();
canvas.rotate(degree);
canvas.drawLine(0, 0, mStandard * POINTER_LENGTH_SECOND, 0, mPaint);
canvas.restore();
canvas.restore();
}
第7行旋转了270°,为什么是270这个是试出来的,没仔细研究,有时间再看。
其他注释都写得很多了,没什么好说,就是每次旋转画完一个指针之后记得还原旋转的操作了,免得你转了几次之后自己都晕了。
下面是drawDigitalTime方法:
/** * 画当前时间的数字显示 */
private void drawDigitalTime(Canvas canvas, Calendar calendar) {
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
String time = String.format("北京时间:%s:%s", hour, minute);
Rect bound = new Rect();
mPaint.getTextBounds(time, 0, time.length(), bound);
if (mStandard > bound.width()) {
canvas.drawText(time, 0, mTotalHeight >> 3, mPaint);
}
}
画数字时间之前先计算下字符串的宽度,如果宽度值比上面那个mStandard还大,那就不画那个数字时间了。
最后是view界面的刷新问题,一开始考虑在画指针的时候写个while循环然后每个多少秒刷新,但是会造成ui阻塞以及可能重叠等问题,后来想想其实直接调用invalidate就可以了,同时为了避免过度刷新,加了一个延迟刷新的操作,所以onDraw方法中18~21行中可以看到:
if (!mIsStop) { postInvalidateDelayed(INTERVAL_REFRESH); }
代码在这里
end.