1.前言
前面的文章中介绍了绘制基本几何图案的方法,其实只要灵活的运用这些方法就可以自定义出我们想要的View。最近观察到家中墙上挂的圆形时钟,于是就在思考怎样用自定义View的方法去实现一个圆形时钟,并且可以记录时间。今天就为大家带来一个自定义的圆形时钟。
2.实现分析
按类型来划分自定义View的实现方式大概可以分为三种,自绘控件、组合控件和继承控件。
当Android自带的View满足不了开发需求时,自定义View就发挥了很好的作用,在这里我们使用的是继承View实现自绘控件。建立一个自定义View需要继承于View类,并且实现其中的至少一个构造函数的两个方法:onMeasure()和onDraw(),onMeasure()用于设置自定义View的尺寸,onDraw()用于绘制View中的内容。
实现后的效果:
实现的关键是:
1.新建一个ClockView的类并且继承于View,重写onMeasure()和onDraw()
2.获取设备屏幕的尺寸
3.在onDraw()方法中进行绘制,需要绘制一个圆形做为表盘
4.绘制表盘中的刻度,数字和指针
5.得到当前设备的时间,设置表盘中指针显示的时间
6.开启一个线程,使得每经过一秒钟获取一次系统时间并且更新表盘中的指针开模拟钟表的计时功能
在获取当前设备屏幕尺寸方面我们需要用到WindowManager这个类,WindowManager是应用程序使用界面和窗口的管理器,可以通过这个类中的getDefaultDisplay().getWidth()和getDefaultDisplay().getHeight()方法分别获取当前设备屏幕的长和高。
//获取屏幕窗口
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
//获取当前屏幕的宽
width = windowManager.getDefaultDisplay().getWidth();
//获取当前屏幕的高
height = windowManager.getDefaultDisplay().getHeight();
此时width和height的值就是当前设备屏幕的长和高
Android系统在绘制View之前,必须对View进行测量,就是告诉系统需要画一个多大的View,这个过程是在onMeasure()方法中进行的,因此在重写onDraw()方法之前,需要先重写onMeasure()
Android系统还给我们提供了一个MeasureSpec类,通过这个类可以帮助我们去测量View。
测量的模式可以分为以下三种:
— EXACTLY
即精确值模式,当我们在XML中将控件的layout_width属性或者layout_height属性指定为具体数值时,比如layout_width="100dp",或者指定为layout_width="match_parent"属性或layout_width="fill_parent"属性时(占据父View的大小),系统使用的是EXACTLY模式
— AT_MOST
即最大值模式,当控件的layout_width属性或者layout_height属性指定为warp_content时,控件大小一般随着控件的子空间或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可
— UNSPECIFIED
即未指定尺寸,它不指定其大小测量模式
View类中默认的onMeasure()方法只支持EXACTLY模式,所以在自定义View的过程中如果不重写onMeasure()方法,自定义的View就只能使用EXACTLY模式。即控件可以响应你指定的具体高和宽的具体值或者是match_parent(fill_parent)属性。如果要让自定义View支持warp_content属性,那么就必须重写onMeasure()方法来指定warp_content时的尺寸
在重写onMeasure()方法的过程中,通过查看super.onMeasure()方法可以发现,系统最终会通过调用setMeasuredDimension(int measureWidth, int measureHeight)方法将测量后的宽和高
//设置控件的尺寸
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取屏幕窗口
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
//获取当前屏幕的宽
width = windowManager.getDefaultDisplay().getWidth();
//获取当前屏幕的高
height = windowManager.getDefaultDisplay().getHeight();
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
在onMeasure()方法中还使用了measureWidth()和measureHeight()这两个方法,这两个方法分别用来定义控件的宽和高
//设置控件的宽
private int measureWidth(int widthMeasureSpec)
{
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if(widthMode == MeasureSpec.EXACTLY)
{
width = widthSize;
}
else if(widthMode == MeasureSpec.AT_MOST)
{
width = Math.min(width, widthSize);
}
return width;
}
//设置控件的高
private int measureHeight(int heightMeasureSpec)
{
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
}
else if(heightMode == MeasureSpec.AT_MOST)
{
height = Math.min(height, heightSize);
}
return height;
}
当测量好一个View之后,我们就可以通过重写onDraw()方法,并在Canvas对象上来绘制所需要的图形了,关于画图的基本方法前面的文章已经做过介绍,这里就不在介绍了。首先我们需要绘制一个原型做为表盘,并且在表盘上画上刻度,绘制出来的效果入下图所示
从图中可以看出,刻度把整个圆形平均分为60份,并且整点的刻度的长度和宽度要要大于其它刻度,相对应的代码为
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//获取状态栏的高度
Rect statusBar = new Rect();
getWindowVisibleDisplayFrame(statusBar);
statusBarHeight = statusBar.top;
//初始化圆心使圆心在屏幕的中心位置
centerX = width/2;
centerY = height/2 - statusBarHeight;
//计算圆的半径
radius = Math.min(centerX, centerY) - 30;
//初始化画笔
dialPaint = new Paint();
dialPaint.setColor(Color.BLACK);
dialPaint.setAntiAlias(true);
dialPaint.setStyle(Paint.Style.STROKE);
dialPaint.setStrokeWidth(5);
//以当前屏幕的中心为原点画圆,绘制表盘
canvas.drawCircle(centerX, centerY, radius, dialPaint);
//绘制圆心
canvas.drawPoint(centerX, centerY, dialPaint);
//绘制刻度
calibrationPaint = new Paint();
calibrationPaint.setColor(Color.BLACK);
calibrationPaint.setAntiAlias(true);
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
calibrationPaint.setStrokeWidth(6);
degreeLength = DEFAULT_LONG_DEGREE_LENGTH;
}
else
{
calibrationPaint.setStrokeWidth(3);
degreeLength = DEFAULT_SHORT_DEGREE_LENGTH;
}
canvas.drawLine(centerX, centerY - radius, centerX, centerY - radius + degreeLength, calibrationPaint);
canvas.rotate(360 / 60, centerX, centerY);
}
}
最开始需要选择屏幕的中心为表盘的原点进行画圆,在表盘中画刻度时需要注意,每画一个刻度需要使得画布旋转30度,这样就可以把表盘平均分为60个等分
在绘制完成表盘后还要需要在刻度上加上数字,最开始的思路是随着画刻度的过程中在刻度下使用canvas.drawText()方法添加数字
//绘制数字
numberPaint = new Paint();
numberPaint.setColor(Color.BLACK);
numberPaint.setAntiAlias(true);
numberPaint.setTextAlign(Paint.Align.CENTER);//以数字的中心对齐
numberPaint.setStrokeWidth(3);
numberPaint.setTextSize(40);
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
calibrationPaint.setStrokeWidth(6);
degreeLength = DEFAULT_LONG_DEGREE_LENGTH;
String num = String.valueOf(i / 5 == 0 ? 12 : i / 5);
canvas.drawText(num, centerX, centerY - radius + degreeLength + 40, numberPaint);
}
else
{
calibrationPaint.setStrokeWidth(3);
degreeLength = DEFAULT_SHORT_DEGREE_LENGTH;
}
canvas.drawLine(centerX, centerY - radius, centerX, centerY - radius + degreeLength, calibrationPaint);
canvas.rotate(360 / 60, centerX, centerY);
}
但是这样画出来的大部分数字的方向是相反的,这样画出的效果图如下
这样的表盘肯定不是我们想要的,而且看起来也不够美观。为此我们要重新想办法,在刻度上画数字的时候有正确的方向。为了避免数字的方向都是指向圆心的,因此我们要通过计算算出1~12这12个数字的坐标再进行绘制。
下面我们以计算数字1的坐标为例子,分析计算数字坐标的方法。(其余数字的坐标计算方法与数字1相似,这里仅仅给出实现的代码就不做过多的分析)
由图可以看出数字1的坐标和圆心构成了一个直角三角形,因此计算数字1的坐标可以通过三角函数进行计算,由上图可知我们设三角形的三边分别为a,b,c。
c=radius(半径)-刻度线长度-数字离刻度线的距离
a=c*sin30(sinπ/6)
b=c*cos30(cosπ/6)
由此可以推断出当绘制数字12时夹角为0,当绘制数字1时夹角为30,当绘制数字2时夹角为60......
当绘制某一个数字时夹角的弧度为i * 30 * Math.PI / 180(0<i<12),因此绘制表盘数字的代码为
//绘制数字
numberPaint = new Paint();
numberPaint.setStrokeWidth(3);
numberPaint.setAntiAlias(true);
numberPaint.setTextSize(40);
numberPaint.setColor(Color.BLACK);
numberPaint.setTextAlign(Paint.Align.CENTER);//以数字的中心对齐
float textSize = numberPaint.getFontMetrics().bottom - numberPaint.getFontMetrics().top;
float distance = radius - DEFAULT_SHORT_DEGREE_LENGTH - 30;
float a, b;//坐标
for(int i = 0; i < 12; i++)
{
a = (float) (distance * Math.sin(i * 30 * Math.PI / 180));
b = (float) (distance * Math.cos(i * 30 * Math.PI / 180));
if(i == 0)
{
canvas.drawText("12", centerX + a, centerY - b + textSize / 3, numberPaint);
}
else
{
canvas.drawText(String.valueOf(i), centerX + a, centerY - b + textSize / 3, numberPaint);
}
}
在完成了表盘的绘制以后接下来还需要在表盘上添加时针,分针和秒针,这里的指针其实就是三根长度和宽度不一样的直线,由于需要画三根指针并且这三根指针需要不重合,所以在这里需要使用canvas.save()和canvas.rotate()配合使用,具体的代码如下
private void drawHour(Canvas canvas, int hourTime, int minuteTime)
{
//绘制时针
hourPaint = new Paint();
hourPaint.setStrokeWidth(8);
hourPaint.setAntiAlias(true);
hourPaint.setColor(Color.GREEN);
hourPaint.setStyle(Paint.Style.FILL);
hourReverseLength = radius/8;
hourLength = 2*radius/5;
float hourDegrees = hourTime * 30 + (minuteTime/60f)*30;
canvas.rotate(hourDegrees, centerX, centerY);
canvas.drawLine(centerX, centerY + hourReverseLength, centerX, centerY - hourLength, hourPaint);
}
private void drawMinute(Canvas canvas, int minuteTime)
{
//绘制分针
minutePaint = new Paint();
minutePaint.setStrokeWidth(6);
minutePaint.setAntiAlias(true);
minutePaint.setColor(Color.RED);
minutePaint.setStyle(Paint.Style.FILL);
minuteReverseLength = radius/6;
minuteLength = 3*radius/5;
canvas.restore();
canvas.rotate(minuteTime * 6, centerX, centerY);
canvas.drawLine(centerX, centerY + minuteReverseLength, centerX, centerY - minuteLength, minutePaint);
}
private void drawSecond(Canvas canvas, int secondTime)
{
//绘制秒针
secondPaint = new Paint();
secondPaint.setStrokeWidth(4);
secondPaint.setAntiAlias(true);
secondPaint.setColor(Color.BLACK);
secondPaint.setStyle(Paint.Style.FILL);
secondReverseLength = radius/4;
secondLength = 4*radius/5;
canvas.restore();
canvas.rotate(secondTime * 6, centerX, centerY);
canvas.drawLine(centerX, centerY + secondReverseLength, centerX, centerY - secondLength, secondPaint);
}
这时自定义时钟就基本完成了,但是这时自定义的时钟还是处于静止状态的,我们还要继续添加计时功能,能够让绘制的时钟走起来。要实现这样的效果原理很简单,基本原理就是我们可以通过Handler,每经过1秒钟就获取一次系统时间再使用invalidate()方法刷新View,这样就可以模拟出时钟计时的效果
在主线程中需要使用sendEmptyMessageDelayed()方法,每过1秒钟向Handler里发送一条消息,当Handler接收到消息后就获取当前系统时间并且刷新View
private Handler handler = new Handler()
{
public void handleMessage(Message msg)
{
if(msg.what == 1234)
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
hour = calendar.get(Calendar.HOUR);
minute = calendar.get(Calendar.MINUTE);
second = calendar.get(Calendar.SECOND);
Log.d("time", hour + ":" + minute + ":" + second);
invalidate();
}
}
};
这时自定义时钟就由静态变为动态的,带有计时功能并且可以自动同步当前设备的系统时间
3.总结
在做自定义View时细节至关重要,在自定义时钟的过程中数字的方向和时针每次走的度数都是很重要的细节,数字的方向不能都指向圆心,而时针在走过一小时的时间需要慢慢走过,而不能每次走一格。只有多多在细节上下功夫才能做出比较友好的View。
以上Demo的源代码地址:点击打开链接