对于一个安卓开发者而言,在学习自定义View的过程中,肯定有尝试去实现一个时钟控件,本文就看看如何快速的实现一个靠谱的时钟控件。
准备工作
在开始编码之前,我们需要明确实现时钟控件过程中,涉及到一些要点和可能遇到的阻碍。这里从几个方面来说:
- 表盘,表盘由圆形和刻度组成,两者均需绘制
- 指针,主要是时针、分针、秒针。其中时针随着时间推移,会产生细微的位移;分针每分钟移动一格;秒针每秒钟移动一格。
- 时间的同步
设计
首先我们需要设计刻度的格式,主要包括当为整点时刻时,对应位置的刻度线最好长一点。
其次三个指针的长度也应该不一样,要保持时针最短,秒针最长。
最后,我们需要实现秒针每秒钟移动一格的动画效果
就以上要点中,我们需要关注的是如何将圆等分60份;如何在圆弧上向圆心方向画出刻度;如何实现秒针移动的动画效果。
接下来,我们就这些问题,来看下具体如何实现
实现
控件的实现其实很简单,采用继承View
,实现onDraw
方法的方式。先绘制表盘,再绘制指针。
表盘主要就是由一个圆组成,然后我们需要在圆弧上画出刻度。在获取圆弧上的刻度位置时,我们需要用到三角函数的知识,通过正余弦来计算每个刻度的起始坐标和结束坐标。确定了每个刻度在圆弧上的起始坐标和圆内的结束坐标,我们就可以绘制一条直线来代表指针
在计算起止坐标时,我们主要通过下面的公式来获取,其中mRadius
是圆的半径,moveDegree
是基于12点钟位置顺时针移动的角度
float offY = (float) (mRadius - mRadius * Math.cos(Math.toRadians(moveDegree)));
当确定刻度线的起始位置后,我们就可以根据刻度线预设的长度来计算它的结束位置了(HOUR_SCALE_LENGTH
是默认时针长度)。
float endY = (float) (offY + HOUR_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));
当获取刻度位置逻辑确定后,我们就可以来循环绘制60个刻度了,绘制代码可以参考如下片段:
/**
* 绘制刻度盘,主要包括:
* 1、表盘圆形
* 2、60个刻度
*/
private void drawPanel(Canvas canvas) {
if (!isPanelDraw) {
// 获取对应的参数属性:控件宽高,矩形内切圆半径,内切圆圆心位置,三个指针轨迹圆的半径等。
int measuredHeight = getMeasuredHeight();
int measuredWidth = getMeasuredWidth();
// 表盘内切圆半径
mRadius = Math.min(measuredHeight, measuredWidth) / 2;
// 表盘圆心x y 坐标
mCenterX = measuredWidth / 2;
mCenterY = measuredHeight / 2;
// 时针轨迹圆半径:内切圆的一半
mHourHandRadius = (int) (mRadius * 0.50);
// 分针轨迹圆半径:内切圆的6/10
mMinuteHandRadius = (int) (mRadius * 0.60);
// 秒针轨迹圆半径:内切圆的8/10
mSecondHandRadius = (int) (mRadius * 0.80);
}
// 画笔设置
mPanelPaint.setStyle(Paint.Style.STROKE);
mPanelPaint.setStrokeWidth(2);
// 绘制表盘内切圆
canvas.drawCircle(mCenterX, mCenterY, mRadius, mPanelPaint);
// 绘制圆心位置
mPanelPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mCenterX, mCenterY, DOT_RADIUS, mPanelPaint);
int moveDegree = 0;
// 从0-360开始绘制刻度
while (moveDegree < CIRCLE_DEGREE) {
// 计算刻度在圆弧上的位置,也就是起始位置
float offX = (float) (mCenterX + mRadius * Math.sin(Math.toRadians(moveDegree)));
float offY = (float) (mRadius - mRadius * Math.cos(Math.toRadians(moveDegree)));
if (moveDegree % OFF_HOUR_DEGREE == 0) {
// 计算刻度在圆内的位置,也就是终点位置,整点位置刻度加长
float endX = (float) (offX - HOUR_SCALE_LENGTH * Math.sin(Math.toRadians(moveDegree)));
float endY = (float) (offY + HOUR_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));
mPanelPaint.setStrokeWidth(HOUR_STROKE);
canvas.drawLine(offX, offY, endX, endY, mPanelPaint);
} else {
// 计算刻度在圆内的位置,也就是终点位置
float endX = (float) (offX - MINUTE_SCALE_LENGTH * Math.sin(Math.toRadians(moveDegree)));
float endY = (float) (offY + MINUTE_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));
mPanelPaint.setStrokeWidth(MINUTE_STROKE);
// 绘制刻度
canvas.drawLine(offX, offY, endX, endY, mPanelPaint);
}
moveDegree += OFF_MINUTE_DEGREE;
}
isPanelDraw = true;
}
最终的绘制效果为:
绘制完表盘,我们接下来就需要绘制表针了。表针的绘制其实可以参考表盘的绘制,区别是,表针的起点是在圆心位置,终点落在圆内。而且时针、分针、秒针长短不一致,也即是需要绘制三条线段。代码可以参考如下:
/**
* 绘制指针,主要包括:
* 1、时针
* 2、分针
* 3、秒针
*/
private void drawScale(Canvas canvas) {
// 获取当前时间
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
// 计算三个表针的行进角度
float secondDegree = CIRCLE_DEGREE / 60 * second;
float minuteDegree = CIRCLE_DEGREE / 60 * minute;
float hourDegree = CIRCLE_DEGREE / 12 * hour + minute * 30 / 60;
// 计算时针当前的终点位置
float endHourX = (float) (mCenterX + mHourHandRadius * Math.sin(Math.toRadians(hourDegree)));
float endHourY = (float) (mCenterY - mHourHandRadius * Math.cos(Math.toRadians(hourDegree)));
// 计算分针当前的终点位置
float endMinuteX =
(float) (mCenterX + mMinuteHandRadius * Math.sin(Math.toRadians(minuteDegree)));
float endMinuteY =
(float) (mCenterY - mMinuteHandRadius * Math.cos(Math.toRadians(minuteDegree)));
// 计算秒针当前的终点位置
float endSecondX =
(float) (mCenterX + mSecondHandRadius * Math.sin(Math.toRadians(secondDegree)));
float endSecondY =
(float) (mCenterY - mSecondHandRadius * Math.cos(Math.toRadians(secondDegree)));
// 绘制时针
mPointerPaint.setStrokeWidth(HOUR_STROKE);
canvas.drawLine(mCenterX, mCenterY, endHourX, endHourY, mPointerPaint);
// 绘制分针
mPointerPaint.setStrokeWidth(MINUTE_STROKE);
canvas.drawLine(mCenterX, mCenterY, endMinuteX, endMinuteY, mPointerPaint);
// 绘制秒针
mPointerPaint.setStrokeWidth(SECOND_STROKE);
canvas.drawLine(mCenterX, mCenterY, endSecondX, endSecondY, mPointerPaint);
}
为了实现秒针的动画效果,我们采用重复绘制的形式来处理的(当然也可以采用动画来处理)。方式就是每隔一秒向UI线程发送重新绘制时钟控件的消息,因为时间会更新,所以指针也会对应移动。调用顺序为:
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawPanel(canvas);
drawScale(canvas);
// 实现动画
postDelayed(runnable, 1000);
}
我们可以看下最终的效果
最后
上述实现中,其实还有很多优化的地方,我们注意到:为了实现秒针的移动,我们每隔一秒重新绘制了整个View,事实上,表盘和刻度是没有必要一直绘制的。因此,就造成了系统资源的浪费。基于这一点,我们应该考虑进行优化,比如将表盘独立出来,使用动画来操作秒针的移动等。感兴趣的读者可以尝试一下,需要注意的是,在判断哪种方式更优秀时,我们还需要以实际的测试结果来说明。
自定义控件在安卓中的地位极为重要,想要成为一个合格的安卓应用开发工程师,就必须熟练掌握自定义控件这个技能。