Android自定义控件练手——简单的时钟

不飞则已,一飞冲天;不鸣则已,一鸣惊人。这篇文章主要讲述Android自定义控件练手——简单的时钟相关的知识,希望能为你提供帮助。
    首先这应该是一个老生常谈的设计了,但是毕竟身为小白的自己都没动手做过,不动手怎么提高自己呢,所以在这梅林沉船闲暇之际,我就把我的设计流程与思路记录下来。首先来看看效果图吧:

Android自定义控件练手——简单的时钟

文章图片

    如上图就是一个简单并没有美化过的时钟,接下来我就来讲讲我的设计流程与思路。
一.首先继承view重写里面的onDraw方法。
    我们要搭建好了画布才能开始在里面画画,而onDraw方法中的canvas当然就是起到画布的作用。
1 public class MyClockView extends View { 2 3public MyClockView(Context context) { 4super(context); 5init(); //初始化的方法 6} 7 8public MyClockView(Context context, AttributeSet attrs) { 9super(context, attrs); 10init(); 11} 12 13public MyClockView(Context context, AttributeSet attrs, int defStyleAttr) { 14super(context, attrs, defStyleAttr); 15init(); 16} 17 18public void init() { 19 20} 21 22@Override 23protected void onDraw(Canvas canvas) { 24super.onDraw(canvas); 25} 26 27 }

二.准备需要用到的工具。
    要画一个时钟当然首先得要有笔才行,第一步上面我们得到了画布,现在我们还需要一个paint的画笔。似乎绘图用的工具就这么多了,一张画布,一支笔,那么让我们想想还需要用到些什么变量,我就先把时钟结构拆分成了,时、分、秒针,一个圆圈框和时钟的刻度与数字,但是这些能代表些什么deep♂dark♂fantastic的呢,再让我们想想这里一般绘图当然要和坐标挂钩,那就都变成二维坐标吧,时分秒针都是直线,就是画线,圆框就是画圆要知道圆心与半径,刻度也是画线,数字就是写字,拆分为了,时分秒针两端的坐标,圆心与半径,刻度两端的坐标,数字绘制开始的坐标。
三.坐标绘制的算法分析。
    我们应该是都知道要想根据坐标绘制就要先知道它的原点在哪,一般默认情况下它的原点定在屏幕左上角,并且y轴下半部为正半轴,上半部为负半轴。如图:
Android自定义控件练手——简单的时钟

文章图片

【Android自定义控件练手——简单的时钟】 
    根据上面图的坐标系,我们现在要画一个圆形,里面再画刻度,再画时分秒针,首先我们要确定圆心在哪,我按照习惯以控件长宽最短的一边为直径来定圆心的坐标,我在刚才继承view之后重写onSizeChanged方法(这个方法是当宽高放生变化和第一次会执行)做获取半径长:
1 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 2mWidth = w; //获得宽度 3mHeight = h; //获得高度 4 5//以最短的一边为所要绘制圆形的直径 6if (mWidth > mHeight) { 7arcRa = mHeight / 2; //以最短的一边算出半径 8} else { 9arcRa = mWidth / 2; //以最短的一边算出半径 10} 11super.onSizeChanged(w, h, oldw, oldh); 12}

    这样在默认坐标系中,圆心坐标即为(arcRa,arcRa),这里我用arcRa代表半径。有了圆心和半径,那就可以顺利的用canvas.drawCircle画出一个圆。接下来我们要构思画刻度,刻度说白了就是把一个从圆心来等分,一个圆一圈是360度,也就是说把360度等分了,分多少呢,60秒等于1分钟,60分钟等于1小时,那就分60份咯,但是时钟有长短刻度,小刻度是分成60份了,那代表小时的长刻度怎么分,转一圈是12小时,那就分成12份。
    具体刻度切分的思路就上面那么多,那说了这么多,到底要干什么?当然都是为了确定坐标。接下来交给三角函数算法如下图:
Android自定义控件练手——简单的时钟

文章图片

    我们知道了圆心(arcRa,arcRa)和半径在坐标系中画圆,x轴与圆圈相交于一点(arcRa,0),连接(arcRa,0)与圆心是一条直线如图,并且这条直线垂直于x轴。我们要根据360度分出来的度数,来计算刻度坐标点所要在的位置,假设以圆心垂直于x轴的直线来切分,切分出为θ的角度,我们知道半径arcRa,用三角函数公式可得到如图所示的坐标点point。
    上面就是画刻度与指针的算法了,就是简单的三角函数问题。我用的是三角函数算法来绘制其实还有另外一种简单方法,通过旋转canvas画布来绘制,网上也能够查到,我这里就用我当时顺势想出来的麻烦点蠢的方法来绘制。
四.开始draw啦。
    嗨呀,终于可以开始画了,不过在开始绘制指针之前,先要获取时间才行:
1 private void getCurrentTime() { 2long time = System.currentTimeMillis(); //获取时间 3Calendar mCalendar = Calendar.getInstance(); 4mCalendar.setTimeInMillis(time); 5startHour = mCalendar.get(Calendar.HOUR); //获取小时,12小时制 6startMinute = mCalendar.get(Calendar.MINUTE); //获取分钟 7startSecond = mCalendar.get(Calendar.SECOND); //获取秒 8}

    这里我们获取到的时间其实就是所占的份数,分钟与秒都是总共60份,小时为12份。
    先画圆与刻度:
1 //画圆,通过获取宽高算出最短一边作为直径,坐标原点默认在手机屏幕左上角 2canvas.drawCircle(arcRa, arcRa, arcRa, paint); 3 4//围绕圆形绘制刻度,坐标原点默认在手机屏幕左上角 5for (int i = 0; i < 60; i++) {///2π圆形分成60份,一秒钟与一分钟,所以要绘制60次,这里是从0到59 6float x1, y1, x2, y2; //刻度的两端的坐标即起始于结束的坐标 7float scale; //每个刻度离圆心的最近端坐标点到圆心的距离 8Double du = rr * i; //当前所占的角度 9Double sinx = Math.sin(du); //该角度的sin值 10Double cosy = Math.cos(du); //该角度的cos值 11x1 = (float) (arcRa + arcRa * sinx); //以默认坐标系通过三角函数算出刻度离圆心最远的端点的x轴坐标 12y1 = (float) (arcRa - arcRa * cosy); //以默认坐标系通过三角函数算出刻度离圆心最远的端点的y轴坐标 13if (i % 5 == 0) {//筛选刻度长度 14scale = 5 * arcRa / 6; //长刻度绘制,刻度离圆心的最近端坐标点到圆心的距离,这里取半径的五分之六的长度,可以通过情况来定 15} else { 16scale = 9 * arcRa / 10; //短刻度绘制,这里取半径的十分之六九的长度,可以通过情况来定 17} 18x2 = (float) (arcRa + scale * sinx); //以默认坐标系通过三角函数算出该刻度离圆心最近的端点的x轴坐标 19y2 = (float) (arcRa - scale * cosy); //以默认坐标系通过三角函数算出该刻度离圆心最近的端点的y轴坐标 20canvas.drawLine(x1, y1, x2, y2, paint); //通过两端点绘制刻度 21}

    然后开始绘制时分秒指针:
1//利用三角函数计算分别计算出,时分秒三针所在的坐标点,坐标原点默认在手机屏幕左上角 2float sencondScale = 5 * arcRa / 6; //秒针长度 3float minuteScale = 3 * arcRa / 4; //分针长度 4float hourScale = arcRa / 2; //时针长度 5secondStartPoint.x = (float) (arcRa + sencondScale * Math.sin(secondAngle)); 6secondStartPoint.y = (float) (arcRa - sencondScale * Math.cos(secondAngle)); 7minuteStartPoint.x = (float) (arcRa + minuteScale * Math.sin(minuteAngle)); 8minuteStartPoint.y = (float) (arcRa - minuteScale * Math.cos(minuteAngle)); 9hourStartPoint.x = (float) (arcRa + hourScale * Math.sin(hourAngle)); 10hourStartPoint.y = (float) (arcRa - hourScale * Math.cos(hourAngle)); 11//绘制时、分、秒针,坐标原点默认在手机屏幕左上角 12canvas.drawLine(arcRa, arcRa, secondStartPoint.x, secondStartPoint.y, paint); 13canvas.drawLine(arcRa, arcRa, minuteStartPoint.x, minuteStartPoint.y, paint); 14canvas.drawLine(arcRa, arcRa, hourStartPoint.x, hourStartPoint.y, paint);

    其实上面都是一个画线条的过程,也就是知道两点坐标drawLine的过程。接下来绘制数字,找到需要绘制地点的坐标,我们在长刻度上绘制小时数,一圈12个小时,那就在刚才上面绘制长刻度里面加上:
1//绘制长刻度上的数字1~12 2String number = itime + ""; //当前数字变为String类型 3itime++; //数字加1 4if (itime > 12) {//如果大于数字12,重置为1 5itime = 1; 6} 7float numScale = 4 * arcRa / 5; //数字离圆心的距离,这里取半径的五分之四的长度,可以通过情况来定 8float x3 = (float) (arcRa + numScale * sinx); //以默认坐标系通过三角函数算出x轴坐标 9float y3 = (float) (arcRa - numScale * cosy); //以默认坐标系通过三角函数算出x轴坐标 10paint.getTextBounds(number, 0, number.length(), textBound); //获取每个数字被全部包裹的最小的矩形边框数值 11 12//绘制数字,通过x3,y3根据文字最小包裹矩形边框数值进行绘制点调整 13canvas.drawText(number, x3 - textBound.width() / 2, y3 + textBound.height() / 2, paint);

    绘制文字我以为只要drawText出来就行了,没想到错位了,我就思考怎么会这样,后来发现drawText的参数设定:
/** * text:绘制的文字 * x:绘制原点x坐标 * y:绘制原点y坐标(基线) * paint:用来做画的画笔 */ public void drawText(String text, float x, float y, Paint paint)

这里y是绘制的基线并不是文字中心点,基线这里我用网上找到的图来展示一下:
Android自定义控件练手——简单的时钟

文章图片

    所以还是得我们自己调整下文字的绘制位置,Paint画笔默认绘制文字(SetTextAlign)是按照左下角红点开始绘制:
   
Android自定义控件练手——简单的时钟

文章图片

    所以我就通过获得paint.getTextBounds(number,0,number.length(),textBound); 获取每个数字被全部包裹的最小的矩形边框数值,通过坐标移动将它移动到相应位置。
    最后绘制完毕了!   (╯‵□′)╯︵┻━┻怎么放上去不动,不要唬我!那是忘记进行刷新操作,最后在onDraw方法中绘制完毕最后加上这个:
postInvalidateDelayed(1000); //每秒刷新一次

      结束放上源码与神秘链接:
GitHub:https://github.com/SteinsGateZero/MyclockViewtest.git
1 public class MyClockView extends View { 2private Paint paint; //画笔 3private int mainColor = Color.parseColor("#000000"); //画笔颜色 4private float mWidth, mHeight; //视图宽高 5private float arcRa = 0; //圆半径 6private Double rr = 2 * Math.PI / 60; //2π即360度的圆形分成60份,一秒钟与一分钟 7private Double rr2 = 2 * Math.PI / 12; //2π圆形分成12份,圆形显示12个小时的刻度 8private PointF secondStartPoint, minuteStartPoint, hourStartPoint; //秒,分,时的坐标点 9private int startSecond, startMinute, startHour; //初始化时秒,分,时获取的系统时间 10private Rect textBound = new Rect(); //字体被全部包裹的最小的矩形边框 11 12public MyClockView(Context context) { 13super(context); 14init(); 15} 16 17public MyClockView(Context context, AttributeSet attrs) { 18super(context, attrs); 19init(); 20} 21 22public MyClockView(Context context, AttributeSet attrs, int defStyleAttr) { 23super(context, attrs, defStyleAttr); 24init(); 25} 26 27@Override 28protected void onSizeChanged(int w, int h, int oldw, int oldh) { 29mWidth = w; //获得宽度 30mHeight = h; //获得高度 31 32//以最短的一边为所要绘制圆形的直径 33if (mWidth > mHeight) { 34arcRa = mHeight / 2; //以最短的一边算出半径 35} else { 36arcRa = mWidth / 2; //以最短的一边算出半径 37} 38super.onSizeChanged(w, h, oldw, oldh); 39} 40 41public void init() { 42paint = new Paint(); //初始化画笔 43paint.setColor(mainColor); //设置颜色 44//paint.setAntiAlias(true); //抗锯齿(性能影响) 45paint.setStyle(Paint.Style.STROKE); //设置画笔 46paint.setTextSize(45); //设置字体大小 47secondStartPoint = new PointF(arcRa, 0); //初始化坐标点 48hourStartPoint = new PointF(arcRa, 0); 49minuteStartPoint = new PointF(arcRa, 0); 50} 51 52@Override 53protected void onDraw(Canvas canvas) { 54super.onDraw(canvas); 55 56//①获取系统时间 57getCurrentTime(); 58 59//②当前时间时分秒分别所占的份数(角度),即为上面rr,rr2所得到的每份的角度乘以获得的时间 60Double secondAngle = rr * startSecond; 61Double minuteAngle = rr * startMinute; 62Double hourAngle = rr2 * startHour; 63 64//③利用三角函数计算分别计算出,时分秒三针所在的坐标点,坐标原点默认在手机屏幕左上角 65float sencondScale = 5 * arcRa / 6; //秒针长度 66float minuteScale = 3 * arcRa / 4; //分针长度 67float hourScale = arcRa / 2; //时针长度 68secondStartPoint.x = (float) (arcRa + sencondScale * Math.sin(secondAngle)); 69secondStartPoint.y = (float) (arcRa - sencondScale * Math.cos(secondAngle)); 70minuteStartPoint.x = (float) (arcRa + minuteScale * Math.sin(minuteAngle)); 71minuteStartPoint.y = (float) (arcRa - minuteScale * Math.cos(minuteAngle)); 72hourStartPoint.x = (float) (arcRa + hourScale * Math.sin(hourAngle)); 73hourStartPoint.y = (float) (arcRa - hourScale * Math.cos(hourAngle)); 74 75//④画圆,通过获取宽高算出最短一边作为直径,坐标原点默认在手机屏幕左上角 76canvas.drawCircle(arcRa, arcRa, arcRa, paint); 77 78//⑤围绕圆形绘制刻度,坐标原点默认在手机屏幕左上角 79int itime = 12; //长的刻度要显示的数字,这里从12点刻度开始顺时针绘制 80for (int i = 0; i < 60; i++) {///2π圆形分成60份,一秒钟与一分钟,所以要绘制60次,这里是从0到59 81float x1, y1, x2, y2; //刻度的两端的坐标即起始于结束的坐标 82float scale; //每个刻度离圆心的最近端坐标点到圆心的距离 83Double du = rr * i; //当前所占的角度 84Double sinx = Math.sin(du); //该角度的sin值 85Double cosy = Math.cos(du); //该角度的cos值 86x1 = (float) (arcRa + arcRa * sinx); //以默认坐标系通过三角函数算出刻度离圆心最远的端点的x轴坐标 87y1 = (float) (arcRa - arcRa * cosy); //以默认坐标系通过三角函数算出刻度离圆心最远的端点的y轴坐标 88if (i % 5 == 0) {//筛选刻度长度 89scale = 5 * arcRa / 6; //长刻度绘制,刻度离圆心的最近端坐标点到圆心的距离,这里取半径的五分之六的长度,可以通过情况来定 90 91//绘制长刻度上的数字1~12 92String number = itime + ""; //当前数字变为String类型 93itime++; //数字加1 94if (itime > 12) {//如果大于数字12,重置为1 95itime = 1; 96} 97float numScale = 4 * arcRa / 5; //数字离圆心的距离,这里取半径的五分之四的长度,可以通过情况来定 98float x3 = (float) (arcRa + numScale * sinx); //以默认坐标系通过三角函数算出x轴坐标 99float y3 = (float) (arcRa - numScale * cosy); //以默认坐标系通过三角函数算出x轴坐标 100paint.getTextBounds(number, 0, number.length(), textBound); //获取每个数字被全部包裹的最小的矩形边框数值 101 102//绘制数字,通过x3,y3根据文字最小包裹矩形边框数值进行绘制点调整 103canvas.drawText(number, x3 - textBound.width() / 2, y3 + textBound.height() / 2, paint); 104 105} else { 106scale = 9 * arcRa / 10; //短刻度绘制,这里取半径的十分之六九的长度,可以通过情况来定 107} 108x2 = (float) (arcRa + scale * sinx); //以默认坐标系通过三角函数算出该刻度离圆心最近的端点的x轴坐标 109y2 = (float) (arcRa - scale * cosy); //以默认坐标系通过三角函数算出该刻度离圆心最近的端点的y轴坐标 110canvas.drawLine(x1, y1, x2, y2, paint); //通过两端点绘制刻度 111} 112 113//⑥绘制时、分、秒针,坐标原点默认在手机屏幕左上角 114canvas.drawLine(arcRa, arcRa, secondStartPoint.x, secondStartPoint.y, paint); 115canvas.drawLine(arcRa, arcRa, minuteStartPoint.x, minuteStartPoint.y, paint); 116canvas.drawLine(arcRa, arcRa, hourStartPoint.x, hourStartPoint.y, paint); 117 118postInvalidateDelayed(1000); //每秒刷新一次 119} 120 121private void getCurrentTime() { 122long time = System.currentTimeMillis(); //获取时间 123Calendar mCalendar = Calendar.getInstance(); 124mCalendar.setTimeInMillis(time); 125startHour = mCalendar.get(Calendar.HOUR); //获取小时,12小时制 126startMinute = mCalendar.get(Calendar.MINUTE); //获取分钟 127startSecond = mCalendar.get(Calendar.SECOND); //获取秒 128} 129 }

 

    推荐阅读