Android自定义控件之圆形时钟

声明:原创作品,转载请注明出处 http://www.jianshu.com/p/3b789490fc04

最近,电脑突然罢工了,搞了我好长时间才弄好。。所以写这篇文章耽搁了很长时间。废话不多说今天我给大家带来一个最近自己造的轮子——自定义时钟。对自定义控件有兴趣的朋友可以看看,具体内容我会尽量讲的详细。先看一下效果图:

《Android自定义控件之圆形时钟》
《Android自定义控件之圆形时钟》
《Android自定义控件之圆形时钟》

大家在做自定义控件时,可以把自己想像成一名艺术家。你在创作自己的艺术品。那么作为一名画家,你肯定得需要至少两样工具:画笔和画布。这两样是作画的基础,缺一不可。那么Android有这两样东西吗,答案是肯定的。在Android中Paint就是我们的画笔,Canvas就是我们的画布。那么这两样东西该如何去用呢?其实也很简单,Paint提供了很多方法,我们通过这些方法可以对这只笔进行设置,比如笔的颜色,画出来线条的粗细等等。而Canvas则负责具体要画的东西,比如点,线,矩形,圆形等等,有关具体的使用细节我一会儿会详细讲解。这里你只需要大体知道有这么个东西就可以了。

好了,回到我们的主题上来,画笔和画布都有了,那么问题来了,,,挖掘机技术哪家强。。。。。日。。再来一遍,,那么问题来了,如果是你想要在现实生活中画一个时钟,你觉得都得要画什么呢?我想小时候大家一定都有在自己手上画手表的经历吧。首先,当然得有一个边框吧,然后是圆心、刻度以及数字,当然还有最重要的指针,这也是构成时钟最基本的要素,相信你当时一定画的很漂亮。那么在Android中到底该如何去画呢。接下来,我就带大家一起看看,这些东西是如何一步步画在手机上的。

1.准备工作

首先,我们得自己定义一个类取名叫TimeView,让其继承View,然后创建构造方法,最后我们要覆写onDraw(Canvas canvas)方法,我们具体的画图逻辑就在这个方法中。具体代码如下:

public class TimeView extends View{
    private Context mContext;
    private Paint mPaint;
    public TimeView(Context context) {
        super(context);
        this.mContext = context;
        initPaint();
    }
    public TimeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        initPaint();
    }
    /**
     * 初始化画笔
     */
    private void initPaint(){
        mPaint = new Paint();
        //抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(0);
    }
    @Override
    protected void onDraw(Canvas canvas) {
    //画具体内容
    }
}

在这里我们定义了两个构造方法,第一个大家应该都很好理解,关键是第二个,入参多了个AttributeSet,可能大家对这东西比较陌生。我们知道,想要使用一个控件时,有两种方法,第一,我们可以在Java代码中直接new一个,第二种就是在XML布局文件中声明。这两种方法也正好对应以上两种构造方法。如果你不写第二种构造方法,那么你在XML布局文件中直接使用时会报错的。在构造方法中,我们创建了一只画笔。然后给它设置一些属性,其中setAntiAlias(true)的作用是抗锯齿,顾名思义如果不设置的话,在图形边缘会有一些锯齿状的痕迹。然后给这只笔设置颜色,以及风格。风格一共有三种:Paint.Style.STROKE,描边效果,比如你画一个圆,显示的就是一个圆环;Paint.Style.FILL、填充效果,显示的整个圆;Paint.Style.FILL_AND_STROKE,这个既有描边,又有填充其实效果和FILL差不多。如果设置成STROKE,那么你可以用setStrokeWidth()给这条边设置宽度。这样我们的画笔就准备好了,画布就是我们onDraw(Canvas canvas)中的canvas已经给我们提供好了。好了,这样我们就已经写好了一个自定义控件。然后我们就可以在XML布局文件中引用了,注意:控件名前一定要加具体的包名。好了这样我们运行一下发现什么都没有,因为我们在onDraw方法中还没干任何事情,不过别着急,接下来我们一步步来实现。

<com.example.administrator.timeviewdemo.TimeView
    android:id="@+id/time_view"
    android:layout_width="300dp"
    android:layout_height="300dp"/>

2.画边框

我们的边框就是一个简单的圆:

@Override
protected void onDraw(Canvas canvas){
    //圆形边框
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3, mPaint);
}

我们可以看到,要想画一个圆,只用调用canvas的drawCircle(float x,float y,float radius,Paint mPaint)方法,它接受四个参数,其中想x、y为圆的圆心。这里我要说一下Android的坐标系,它的坐标原点默认在屏幕的左上角,向右为为X轴正方向,向下为Y轴正方向

《Android自定义控件之圆形时钟》

这里我们圆心选在控件的中心,即宽高的一半。第三个参数是圆的半径,这里我们就取控件宽的三分之一,第四个为之前我们创建的画笔。我们来运行看一下效果:

《Android自定义控件之圆形时钟》

怎么样还不错吧,总算有点东西了,这里我们的Style设的是Paint.Style.STROKE,我们换成Paint.Style.FILL试下:

《Android自定义控件之圆形时钟》

可以看到圆内被填充了,这下你应该知道FILL和STROKE的区别了吧。好了我们来看下一个

3.画中心点

有了外面的边框我们还可以再给它一个中心点,当然你觉得没必要,不加也可以。不过我们还是来看一下在Android中是如何画一个点的。其实也很简单,你只需调用canvas.drawPoint(float x,float y,Paint mPaint)方法,我想这方法也不用在过多的解释了,x,y为中心点的坐标,mPaint为之前的画笔。

《Android自定义控件之圆形时钟》

4.画刻度线

时钟自然是少不了刻度线啦,所以我们来看看刻度线是如何画的。刻度线说白了就是一条条的直线。那么在Canvas中有画直线的方法吗?答案是必须的。画布给我们提供一个叫canvas.drawLine(float fromX,float fromY,float stopX,float stopY,Paint mPaint)的方法;我想大家应该在初中就知道两点决定一条直线,所以这个方法中一二两个参数分别为起始点的x、y坐标,三四两个参数为终点坐标,第五个自然为我们的画笔啦。好了有了这个方法,只要求出起点坐标和终点坐标,理论上我们能画出任意的直线。不过这里可能有人要坐不住了:你扯独自呢,这么多刻度线,怎么求啊?确实,这么多刻度线,要想一条一条求出起点坐标和终点坐标,确实不太现实。那么有没有简单点的方法呢?先别急,在回答这个问题之前我们先来看一下Canvas的操作坐标系的几个方法:

  1. canvas.translate(float x,float y);
  2. canvas.rotate(float degree);
  3. canvas.rotate(float degree,float x,float y);

这里我简单说一下这几个方法,第一个是坐标系的平移,传入的两个参数,分别为平移后坐标原点的X、Y坐标,说白了就是你想把坐标原点移到哪个点就传入哪个点;第二个方法是把坐标系旋转一定角度,传入正数则顺时针转,负数则相反。第三个方法是绕着传入的(X,Y)点旋转一定度数。好了,知道了这几个方法现在再画刻度线是不是有点思路了呢。我们知道,要想求出所有刻度的起始与终点坐标很复杂,也不太现实。但求一条刻度的坐标还是好求的。为了坐标表示方便我们移动一下坐标系,即调用canvas.translate(getWidth()/2,getHeight()/2)将坐标原点移到圆心处。

《Android自定义控件之圆形时钟》

如上图所示,我们把坐标原点移到圆心,这样如果我们要画图中绿色刻度线,其实就很简单了。起始坐标和终点坐标的Y轴坐标均为0,起始坐标的X轴坐标为半径减去刻度线长度,而终点坐标的X轴坐标就是半径。怎么样,这样画一条刻度线是不是挺简单的,相信你一定能画好。好,接下来我们再画一条,不过在画之前,我们得做一个小小的动作,就是把坐标系旋转一下。如下图:

《Android自定义控件之圆形时钟》

我们把原来的红色坐标系顺时针旋转了a角度得到了黄色坐标系,也就是调用了canvas.rotate(a),我们之前说过顺时针转,要传入正值,所以这里的a是一个正数。好了,这样我们再来求一下黄色X’轴上的刻度线,会发现它的坐标和第一条刻度线的坐标是一样的。是不是问题变得很简单了。这样不管你要画几条刻度线,不管你想画在哪,只要旋转你的坐标系,而不用反复的计算刻度线的坐标。比如,我们都知道圆是360度,你想每隔一度,就画一条刻度线,那么你就每次旋转一度,然后画一条线。这样不断循环后,就画出了360条刻度线。当然你可以根据自己的需求画任意条。代码如下:

@Override
protected void onDraw(Canvas canvas) {
    //圆形边框
    mPaint.setStrokeWidth(2);
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3, mPaint);
    //圆心
    mPaint.setStrokeWidth(5);
    canvas.drawPoint(getWidth() / 2, getHeight() / 2, mPaint); 
    //设置刻度线线宽
    mPaint.setStrokeWidth(1); 
    //将坐标原点移到圆心处
    canvas.translate(getWidth()/2,getHeight()/2);
    for (int i = 0; i < 360; i++) {      
       //这里刻度线长度我设置为25
       canvas.drawLine(getWidth() / 3-25, 0,getWidth() /3, 0, mPaint);
       canvas.rotate(1); 
    }
}

效果如下:我是每隔1度画了一条刻度线。为便于观看,我放大了整个图片,可以看到我们的刻度线分布的还是很均匀、整齐的。

《Android自定义控件之圆形时钟》

当然如果你觉得刻度线的长度都一样长,太单调了你也可以进行适当的改变。比如你可以每秒钟设置一个中等长度,每五秒钟设置一个最长的长度,然后其他的刻度线都设置一个最小的长度。我们知道圆是360度,并且秒针转一圈为60秒,所以一秒就对应360度/60秒=6度,那么五秒也就是5*6 = 30度。得到这两个关键的角度我们就可以写代码了:

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setStrokeWidth(2); 
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3,mPaint);
    mPaint.setStrokeWidth(5);
    canvas.drawPoint(getWidth() / 2, getHeight() / 2, mPaint);
    mPaint.setStrokeWidth(1); 
    canvas.translate(getWidth() / 2, getHeight() / 2);
    for (int i = 0; i < 360; i++) {
        if (i % 30 == 0) {//长刻度
            canvas.drawLine(getWidth() / 3 - 25, 0,getWidth() / 3, 0, mPaint);
        } else if (i % 6 == 0) {//中刻度*/ 
            canvas.drawLine(getWidth() / 3 - 14, 0,getWidth() / 3, 0, mPaint);
        } else {//短刻度
            canvas.drawLine(getWidth() / 3 - 9, 0,getWidth() / 3, 0, mPaint); 
        } 
        canvas.rotate(1); 
   }

效果如下:

《Android自定义控件之圆形时钟》

4.画数字

接下来我们在时钟上画上1-12的数字,有关写字Canvas给我提供了这样一个方法:drawText(String text,float x,float y,Paint mPaint);其中text指我们要写的字,mPaint是我们的画笔,那么x,y是什么呢?很显然x和y是用来给文字定位用的,x指的文字最左边的X坐标,那么y呢,难道是文字最下边的Y坐标吗。其实不是的。我们来看下图:

《Android自定义控件之圆形时钟》

上图给出个文字的一些尺寸参数,我们可以看到其中那条黑线,即Baseline,上文的y其实就是这条线的Y坐标。Baseline到文字顶部距离叫做ascent,Baseline到文字底部叫做descent,我们知道一般文字上部和下部会有一点padding,所以top和bottom的距离会略大于ascent,descent。如果有两行文字,那么上一行的descent到下一行的ascent的距离就叫做leading,即行间距。那么我们如何能得到这些参数呢。其实很简单,在调用drawText方法之前,我们一般会通过mPaint.setTextSize(float size);来设置字体大小,设完以后,我们就可以通过mPaint.getFontMetrics()方法来得到一个Paint.FontMetrics对象,这个对象封装了上述我们要的文字尺寸信息。代码如下:

Paint mPaint = new Paint();
mPaint.setTextSize(50);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float ascent = fontMetrics.ascent;
float bottom = fontMetrics.bottom;
float descent = fontMetrics.descent;
float leading = fontMetrics.leading;
float top = fontMetrics.top;

注意:上述这些参数大小与具体是什么文字无关,只与字体大小和字体格式有关。并且,在Baseline上方的尺寸为负,下方为正。也就是top、ascent都是负数,bottom和descent为正数。

好了,知道了如何写文字后,我们就可以在时钟上写上我们要的十二个数字了,一共12个数字,一个圆360度,所以每个30度写一个字。这样我们就可以用之前的方法,没写完一个数,就将坐标系旋转30度。代码如下:

mPaint.setTextSize(25);
mPaint.setStyle(Paint.Style.FILL);
Rect textBound = new Rect();//创建一个矩形
for (int i = 0; i <12; i++) {
    if (i == 0){
        //将文字装在上面创建的矩形中,即这个矩形就是文字的边框
        mPaint.getTextBounds(12+"",0,(12+"").length(),textBound);
        canvas.drawText(12+"",-textBound.width()/2,-(getWidth()/3-50),mPaint);
        canvas.rotate(30);
    }else{
        mPaint.getTextBounds(i+"",0,(i+"").length(),textBound);
        canvas.drawText(i+"",-textBound.width()/2,-(getWidth()/3-50),mPaint);
        canvas.rotate(30); 
   }
}

上面的代码还是好理解的,我们创建一个循环,每循环一次就写个文字,并且将坐标系顺时针旋转30度,其中我们可以看到,我们创建了一个矩形,然后我们调用mPaint.getTextBounds(String text,int start,int end,Rect textBound)将文字的边框存入其中,这个方法传入四个参数,第一个为我们要画的字符串,第二三个参数分别为这个字符串的开始角标和结束叫角标,最后一个为矩形。这样我们就可以把这个矩形理解为这个字符串的边框,有了边框我们就可以知道这个字符串的很多参数,比如上下左右的坐标,以及字符串的宽高等。这样当我们画数字时,它的X坐标就是文字宽度的一半,注意别忘了负号。好了我们来看下效果如何:

《Android自定义控件之圆形时钟》

没错正如你所料,虽然数字是有了,而且还挺整齐的,不过文字也跟着旋转了。看来简单的旋转坐标系是不行了。那还有其他办法吗,有的人可能会说了,直接算出每个数字的具体坐标然后在画。这样当然可以,只要你够耐心,而三角函数还不错的话,可以尝试下。不过我还是劝你不要这么干,因为这样计算既麻烦而且算的准确度也不高。那么还有什么更好的办法呢。这里我想到了一个好办法,可以给大家参考一下。其实我们每一次画数字的时候可以提取出一个动作,举个例子,比如我们要画数字“1”,如下图所示,我们知道“12”和“1”之间为30度,那么我们可以先将图中黑色坐标系顺时针旋转30度,得到蓝色坐标系,然后我们将蓝色坐标系沿着Y轴反方向移动合适的距离,得到红色坐标系,然后再将坐标系逆时针转30度得到绿色坐标系,我们的目标就是在绿色坐标系的中心画上数字,具体怎么画,我想也不用多说了。画完后,再将坐标系原路返回。也就是,将绿色坐标系顺时针旋转30度,回到红色坐标系,然后将红色坐标系沿着Y轴正方向移动和之前平移时同样的距离,得到蓝色坐标系,最后将蓝色坐标系逆时针旋转30度回到原来的黑色坐标,即刚开始的坐标系。这样经过一系列的动作,画完一个数字,我们的坐标系还是和原来没画数字时的一样。这样我们就可把这一系列动作写成一个方法,在每次画数字之前调用它就行。

《Android自定义控件之圆形时钟》

这系列动作我们可以写成如下方法:

    private void drawNum(Canvas canvas, int degree, String text, Paint paint) { 
        Rect textBound = new Rect();
        paint.getTextBounds(text, 0, text.length(), textBound);
        canvas.rotate(degree);
        canvas.translate(0, 50 - getWidth() / 3);//这里的50是坐标中心距离时钟最外边框的距离,当然你可以根据需要适当调节
        canvas.rotate(-degree);
        canvas.drawText(text, -textBound.width() / 2, 
               textBound.height() / 2, paint); 
        canvas.rotate(degree);
        canvas.translate(0, getWidth() / 3 - 50);  
        canvas.rotate(-degree);
    }

这个方法,我们传入四个参数,分别为画布,要画数字与12点之间的夹角,要画的数字以及画笔。接下来,在我们每次画数字是调用这个方法就行了:

mPaint.setTextSize(25);
mPaint.setStyle(Paint.Style.FILL);
for (int i = 0; i < 12; i++) { 
   if (i == 0) { 
       drawNum(canvas, i * 30, 12 + "", mPaint);
    } else {
       drawNum(canvas, i * 30, i + "", mPaint); 
   }
}

代码还是挺直观的,我就不过多解释了。我们来看一下效果:

《Android自定义控件之圆形时钟》

只能用两字形容“完美”。

3.画指针

好了,数字也总算画好了,接下来就只剩下指针了,指针分秒针、分针和时针,知道一种怎么画就可以。其实很简单,这里我直接调用drawLine()方法,代码如下:

//秒针
canvas.save()
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
//其实坐标点(0,0)终点坐标(0,-190),这里的190为秒针长度
canvas.drawLine(0, 0, 0, 
       -190, mPaint);
canvas.restore();
//分针
canvas.save();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
canvas.rotate(30);
canvas.drawLine(0, 0, 0,
        -130, mPaint);
canvas.restore();
//时针
canvas.save();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(7);
canvas.rotate(90);
canvas.drawLine(0, 0, 0,
        -90, mPaint);
canvas.restore();

因为我们每个指针的旋转角度都不同,所以为了避免相互影响,我们把每个指针画在canvas.save()和canvas.restore()之间,相当每个指针都画在不同的图层上,最后合并为一张图。

《Android自定义控件之圆形时钟》

好了这样我们的时钟算是画完了,不过细心的朋友可能会发现,这里还有个bug,分针在5分钟时,时针不应该是正对着的,而是有点偏差的,那么这偏差具体是多少呢?还有现在的时钟还是静态的,又如何让它动起来呢?由于篇幅有限,这些内容将写在下篇文章Android自定义控件之圆形时钟(续)中。当然全部代码我已经上传到GitHub上,有兴趣的可以去看一下,记得给个星星哦。。

GitHub地址:https://github.com/Lloyd0577/CustomClockForAndroid

    原文作者:蛇发女妖
    原文地址: https://www.jianshu.com/p/3b789490fc04
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞