Android自定义View之自己画一个时钟

前言

之前看了《Android自定义组件开发详解》这本书,上面有一个例子就是讲的就是如何绘制一个时钟,研究了一下里面的原理,于是自己也写了一个,并且还加上了一些效果,算是一个升级版本的时钟吧。接下来主要就是讲解一下如何绘制一个自定义的时钟的。
还是先来看一下最后实现的效果吧:
显示阿拉伯数字,背景深绿色
cmd-markdown-logo
显示希腊数字,背景黑色
cmd-markdown-logo

自定义时钟绘制

创建好一个工程之后,我们需要创建一个类RuiWatchView,让这个类继承View,并实现View的三个构造方法
自定义View的时候,我们需要重写它的onDraw方法,在这个方法里面我们可以进行一系列的图形绘制操作
下面通过代码看一下分别在构造函数和onDraw方法里面分别执行了什么操作:
构造方法:

1
2
3
4
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //创建了一个画笔(Paint),并进行一些属性的设置
mPaint.setColor(mBackgroundColor);
mPaint.setStyle(Paint.Style.FILL);
mCalendar = Calendar.getInstance(); //绘制时钟需要时间值,因此获取Calendar的实例

onDraw方法:

1
2
3
4
5
6
7
mWidth = this.getMeasuredWidth(); //获取控件的宽度
mHeight = this.getMeasuredHeight(); //获取控件的高度
int len = Math.min(mWidth, mHeight); //取高度和宽度较小的值
mPaint.setTextSize(len / 24); //设置画笔属性
mPaint.setStrokeWidth(len / 2 / 720);
drawPlate(canvas, len); //绘制表盘
drawPoints(canvas, len); //绘制指针

从上面的代码中可以看出来,构造方法里面主要是执行了一些初始化的操作,而在onDraw方法里面执行的就是具体的绘制步骤了,所有的绘制操作都是在这个方法里面完成的。下面我们分解一下每个过程,具体看看是怎么实现的。

绘制表盘

绘制表盘刻度

在onDraw方法中我们已经确定了时针的大小了,半径为len的一半
接下来我们就需要绘制一个圆形,作为一表盘的轮廓:

1
2
3
4
int r = len / 2;
mPaint.setColor(mBackgroundColor);
//绘制表盘,圆心位置(r,r),半径需要减去圆的边框宽度
canvas.drawCircle(r, r, r-r/144, mPaint);

圆形轮廓画好之后,就需要在圆形上面绘制刻度了,一半时钟都是由60根刻度线组成的,其中每5根刻度线就有一根长的刻度线,表示整点小时,其他的刻度线长度稍短,每个刻度之间的角度为6度,这样所有的刻度正好组成一个360度的圆。所以可以通过一个for循环来实现:每绘制一条刻度线,就将图形旋转6度,然后再绘制刻度线,以此类推,直到所有刻度线全部绘制完成。

1
2
3
4
5
6
7
8
9
10
11
for(int i=0; i< 60; i++){
mPaint.setColor(Color.WHITE);
if(i % 5 == 0){ //显示整点数的较长刻度线
mPaint.setStrokeWidth(r / 72);
canvas.drawLine(r+9*r/10, r, len-r/144, r, mPaint); //长刻度线从19*r/10处开始绘制,长度为r/10
}else {
mPaint.setStrokeWidth(r / 180);
canvas.drawLine(r+14*r/15, r, len-r/144, r, mPaint);//短刻度线从29*r/15处开始绘制,长度为r/15
}
canvas.rotate(6 , r, r); //将绘制的圆形以(r,r)为中心顺时针旋转6度
}

cmd-markdown-logo

绘制表盘时间数字

表盘刻度线画好之后就应该绘制表盘上显示的数字了,数字应该显示从1点到12点,总共12个数字,同样可以使用for循环来实现:通过圆形半径、每个数字位置划过的角度大小,然后再通过计算半径的正弦值和余弦值就可以得出每个数字所在的位置坐标,再将数字绘制到对应的坐标点上去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
canvas.translate(r,r);//将画布原点移动到表盘的中心点
//绘制时间数字
for(int i=1; i<= 12; i++){
int degree = 30 * i; //计算每个数字所在位置的角度
double radians = Math.toRadians(degree); //将角度转换为弧度,以便计算正弦值和余弦值
mPaint.setStrokeWidth(4);
String hourText;
if(mDisplayType == 1){
hourText = getHoursGreece(i);
}else {
hourText = getHoursArabia(i)+"";
}
Rect rect = new Rect(); //获取数字的宽度和高度
mPaint.getTextBounds(hourText, 0, hourText.length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
canvas.drawText(hourText,
(float) ((r * 4 / 5) * Math.cos(radians) - textWidth/2),
(float) ((r * 4 / 5) * Math.sin(radians) + textHeight/2),
mPaint); //通过计算出来的坐标进行数字的绘制
}

cmd-markdown-logo

绘制时针、分针和秒针

获取当前的时间

在构造方法中我们已经获取了一个Calendar的实例,在这里就可以通过这个实例来获取当前的的时间:

1
2
3
4
mCalendar.setTimeInMillis(System.currentTimeMillis()); //设置Calendar为当前时间
int hours = mCalendar.get(Calendar.HOUR) % 12; //获取当前的小时
int minutes = mCalendar.get(Calendar.MINUTE); //获取当前的分钟
int seconds = mCalendar.get(Calendar.SECOND); //获取当前的秒

绘制时针

绘制时针的时候,首先我们要根据当前的时间来进行一个角度的计算,根据这个角度可以绘制出一条直线,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//画时针,计算时针的转过的角度
degree = 360 / 12 * hours + (30 * minutes / 60);//角度的计算由当前的小时占用的角度加上分针走过的百分比占用的角度之和
radians = Math.toRadians(degree);
//时针的起点为圆的中点
//通过三角函数计算时针终点的位置,时针最短,取长度的0.5倍
endX = (int) (startX + r * Math.cos(radians) * 0.5); //计算直线终点x坐标
endY = (int) (startY + r * Math.sin(radians) * 0.5); //计算直线终点y坐标
canvas.save();
mPaint.setStrokeWidth(r / 71);
//初始角度是0,应该从12点钟开始算,所以要逆时针旋转90度
canvas.rotate((-90), r, r); // 因为角度是从x轴为0度开始计算的,所以要逆时针旋转90度,将开始的角度调整到与y轴重合
canvas.drawLine(startX, startY, endX, endY, mPaint); //根据起始坐标绘制时针
radians = Math.toRadians(degree - 180); //时针旋转180度,绘制小尾巴
endX = (int) (startX + r * 0.05 * Math.cos(radians));
endY = (int) (startY + r * 0.05 * Math.sin(radians));
canvas.drawLine(startX, startY, endX, endY, mPaint);
canvas.restore();

下面的分针和秒针的绘制原理基本上与时针是一致的,就不一一赘敘了,贴上代码

绘制分针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//画分针,计算分针转过的角度
degree = 360 / 60 * minutes + (6 * seconds / 60);
radians = Math.toRadians(degree);
endX = (int) (startX + r * Math.cos(radians) * 0.6);
endY = (int) (startY + r * Math.sin(radians) * 0.6);
canvas.save();
mPaint.setStrokeWidth(r / 120);
canvas.rotate(-90, r, r);
canvas.drawLine(startX, startY, endX, endY, mPaint);
radians = Math.toRadians(degree - 180);
endX = (int) (startX + r * 0.08 * Math.cos(radians));
endY = (int) (startY + r * 0.08 * Math.sin(radians));
canvas.drawLine(startX, startY, endX, endY, mPaint);
canvas.restore();

绘制秒针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//画秒针
degree = 360 / 60 * seconds;
radians = Math.toRadians(degree);
endX = (int)(startX + r * Math.cos(radians) * 0.7);
endY = (int)(startY + r * Math.sin(radians) * 0.7);
canvas.save();
mPaint.setStrokeWidth(r / 240);
canvas.rotate(-90, r, r);
canvas.drawLine(startX, startY, endX, endY, mPaint);
radians = Math.toRadians(degree - 180);
endX = (int)(startX + r * 0.1 * Math.cos(radians));
endY = (int)(startY + r * 0.1 * Math.sin(radians));
canvas.drawLine(startX, startY, endX, endY, mPaint);
canvas.restore();

添加自定义设置

最后还为时钟设置了两个自定义属性,分别是表盘的背景颜色和表盘数字显示方式,这两个属性均可以通过xml文件设置或者通过java代码进行设置,下面我们来看一下如何自定义这两个属性:
第一步、在资源文件styles.xml中定义自己想要的一些属性,这里定义了custom_background和custom_display_type这两个属性

1
2
3
4
5
6
7
<declare-styleable name="RuiWatchView">
<attr name="custom_background" format="color"></attr>
<attr name="custom_display_type" format="integer">
<enum name="Arabia" value="0"></enum>
<enum name="Greece" value="1"></enum>
</attr>
</declare-styleable>

第二步、在布局文件中设置该属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:rui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.hurui.ruiwatchview.RuiWatchView
android:id="@+id/watch_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
rui:custom_background="#064539"
rui:custom_display_type="Arabia"/>
</LinearLayout>

这里需要注意一点,使用自定义属性的时候,需要在根布局中定义一个属性

1
xmlns:rui="http://schemas.android.com/apk/res-auto"

然后才能在xml文件中定义自己设置的属性

第三步、在自定义View的构造函数中获取这两个属性

1
2
3
4
5
6
7
8
9
public RuiWatchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RuiWatchView);
mBackgroundColor = typedArray.getColor(R.styleable.RuiWatchView_custom_background, Color.BLACK);
mDisplayType = typedArray.getInteger(R.styleable.RuiWatchView_custom_display_type, 0);
......
typedArray.recycle();
}

设置表盘颜色

1、xml设置方式:

1
rui:custom_background="#064539"

2、代码中设置

1
2
RuiWatchView mRuiWatchView = findViewById(R.id.watch_view);
mRuiWatchView.setPlateColor(RuiWatchView.PLATE_COLOR.BLACK);

注意:在xml文件中可以随意定义颜色值,但是在java代码中进行设置的话,只是定义了四个简单的枚举类型:BLACK,GREEN_DARK,GREEN_LIGHT,BLUE

设置表盘数字格式

1、xml设置方式:

1
rui:custom_display_type="Arabia"

2、代码中设置

1
2
RuiWatchView mRuiWatchView = findViewById(R.id.watch_view);
mRuiWatchView.setDisplayType(RuiWatchView.DISPLAY_TYPE.GREECE);

注意:这里的数字类型只定义了两种,Arabia(阿拉伯)、Greece(希腊)

调用方法

1
mRuiWatchView.start()

以上这句代码就是让绘制的时钟开始工作,具体的原理可以看下面的代码

1
2
3
4
5
6
7
8
9
10
//开始执行绘制过程,每秒绘制一次
public void start(){
new Timer().schedule(new TimerTask() {
@Override
public void run() {
postInvalidate();
}
}, 0, 1000);
}

其实就是利用Timer定时器,每隔一秒钟的时间重新进行一次时钟的绘制,还是很简单的。

如何在自己的项目中调用

1、在项目gradle.build中添加下列代码

1
2
3
4
5
6
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

2、在app的gradle.build中添加依赖

1
2
3
dependencies {
compile 'com.github.hurui1990:RuiWatchView:v1.0.2'
}

这样就可以在自己的项目中使用这个时钟控件了

总结

这样的话,一个自定义的时钟就绘制完成了,还是比较简单的,不过里面运用的知识点还是不少的,大家可以通过这个例子来进一步的深入学习一下自定义View