Android自定义View分享——一个时钟

写在前面

这是笔者在学习自定义View以来,分享的第四篇不太复杂但是“长的”还算可以的View效果。之前分享过一篇图片合并效果的自定义View,如果有兴趣的可以看看:
Android自定义View分享——仿微信朋友圈图片合并效果

今天要做这样效果,一个已经被无数人写过的例子:

一个平凡的时钟

虽然被很多人写过,但还要写,因为经典,写完还要总结出些东西出来。下面开始。

需求分析

  1. 首先我们要有一个红色的圆,作为钟表的基本轮廓,圆盘上面要有刻度线,还要有对应的数字。
  2. 在圆的中心有指针,一长一短,时针分针。
  3. 还有一个会动态变化的绿色的圆,这个圆其实表示秒针变化,别人一般画三个指针,我们来搞一下特殊,不要秒针,画成动态变化的圆。
  4. 我们需要每隔一段时间重绘一次控件,以此体现秒钟动态变化效果。
  5. 前面步骤讲的都是onDraw()方法里的操作,当然我们还要重写onMeasured()方法。

所涉及的知识点

根据我们做的需求分析,来分析下会用到那些知识点(哪些类以及方法)

  1. 为了画圆,我们需要用到canvas.drawCircle(cx, cy, radius, paint)方法,前面三个参数分别表示圆心坐标,半径,第四个参数是画笔。
  2. 为了绘制刻度线,我们需要用到canvas.drawLine(startX, startY, stopX, stopY, paint)方法。两点确定一条直线,所以前面两个参数表示起点的坐标,第三、四个参数表是终点坐标,最后一个参数表示画笔。
  3. 为了写字,我们需要用到canvas.drawText(text, x, y, paint)方法,第一个参数就是要绘制的文字内容,第二、三个参数表是文字坐标,第四个参数是画笔。需要注意的是文字的坐标表示和其他不太一样,关于文字坐标计算不在这里记录,因为这是一个单独的知识点,拉开来将很大篇幅。
  4. 为了要围绕圆周来画线,写字,需要利用canvas.rotate(degree)、canvas.save()、canvas.restore()方法简化“围绕圆周绘制xx”这类型的操作所导致的数学运算。

拆解步骤,分析代码

在绘制所有的东西之前,我们需要先做一件事情:调用
canvas.translate(getMeasuredWidth()/2, getMeasuredHeight()/2)
方法,将坐标原点移动到控件中心,对于大量圆周类的操作,这将简化很多计算。以下各步骤都以坐标原点在控件中心为前提来描述

绘制红色的背景圆

首先我们需要初始化画笔,然后取一个比控件宽度的一半还要小一点点的距离,作为圆周的半径(因为圆周如果直接贴着控件边上,很不好看,所以需要小一点点),那么就能得到如下一段代码。

//画笔初始化
Paint circlePaint = new Paint();
circlePaint.setColor(Color.RED);
circlePaint.setStrokeWidth(circleWidth);
circlePaint.setStyle(Paint.Style.STROKE);
//绘制圆周,paddingSpace就是前面所讲的一点点距离
float radius = getMeasuredWidth()/2 - paddingSpace - circleWidth/2;
canvas.drawCircle(0, 0, radius, circlePaint);

绘制刻度线以及文字

同样地我们需要有绘制刻度线的画笔,绘制文字的画笔。然后就是利用canvas.rotate()、canvas.save()、canvas.restore()方法完成围绕圆周画线、写字这样子的一个操作。我们得到如下一段代码:

//绘制刻度线画笔初始化
Paint linePaint = new Paint(); 
linePaint.setColor(Color.BLACK);
linePaint.setStrokeWidth(4);

//绘制文字的画笔初始化
Paint textPaint = new Paint();
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(30);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Paint.Align.CENTER);

//画刻度以及写字,普通刻度线长为10,整点刻度线长度为20
canvas.save();
//将刻度1转到最上面,30是相应的角度
canvas.rotate(30);
//在转动坐标系过程里刻度线的起点位置、普通线的终点位置,20表示线的长度,可以根据自己需要修改
float lineStart = -(radius-circleWidth/2), commonLineEnd = lineStart+20;
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
//这里的20表示的是整点的刻度线比普通刻度线长多少,可以修改,但是记住在循环里面的划线部分需要跟着变化
float textY = commonLineEnd+20-fontMetrics.ascent;//计算基线Y轴坐标
for(int lineNumber = 5, clockNumber = 1; lineNumber < 65; lineNumber++){
	if(lineNumber%5 == 0){
		//如果画到了整点
		canvas.drawLine(0, lineStart, 0, commonLineEnd+20, linePaint);
		canvas.drawText(clockNumber+"", 0, textY, textPaint);
		clockNumber++;
	}else{
		//绘制普通的刻度
		canvas.drawLine(0, lineStart, 0, commonLineEnd, linePaint);
	}
	canvas.rotate(6);
}
//将坐标系还原回去
canvas.restore();

绘制指针

其实绘制指针就是画线,真的和前面画刻度线没有什么很大区别,需要做的只是根据当前时间决定旋转角度,代码如下:

//画笔初始化
Paint pointerPaint = new Paint();
//绘制指针画笔的初始化
pointerPaint.setColor(Color.BLACK);
pointerPaint.setStrokeWidth(10);
//存储当前的状态,开始画指针
canvas.save();
//画时针
float pointerDegree = ( hour+minute/60f )*360/12;     //计算时针的角度
canvas.rotate(pointerDegree);
canvas.drawLine(0, 40, 0, -radius/3, pointerPaint);
canvas.restore();
//画分针
canvas.save();
pointerDegree = minute/60f*360;
canvas.rotate(pointerDegree);
canvas.drawLine(0, 40, 0, -radius/2, pointerPaint);
//将坐标系状态还原
canvas.restore();

绘制绿色的弧线来表示秒钟

这个操作其实就是“在360°表示60秒的前提下,根据当前的秒数来计算弧线所扫过的角度”,将得到如下的代码:

//绘制弧线用的画笔、矩形初始化
Paint arcPaint = new Paint();
RectF arcRectF = new RectF();
arcPaint.setColor(Color.GREEN);
arcPaint.setStrokeWidth(circleWidth);
arcPaint.setStyle(Paint.Style.STROKE);
//用弧线的形式来填充秒数
float position = halfMeasuredWidth-paddingSpace-circleWidth/2;
arcRectF.set(-position, -position, position, position);
canvas.drawArc(arcRectF, -90, millisecond /60000f*360, false, arcPaint);

重写onMeasure()方法

这里没有什么很新的知识点,都是很通用的写法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	setMeasuredDimension(getMeasuredSize(widthMeasureSpec), getMeasuredSize(heightMeasureSpec));
}

//都是比较常规的测量代码
private int getMeasuredSize(int measureSpec){
	int measuredMode = MeasureSpec.getMode(measureSpec);
	//默认大小要稍微大一些
	int measuredSize = 800;
	if(measuredMode == MeasureSpec.EXACTLY){
		measuredSize = MeasureSpec.getSize(measureSpec);
	}else if(measuredMode == MeasureSpec.AT_MOST){
		measuredSize = Math.min(measuredSize, MeasureSpec.getSize(measureSpec));
	}
	return measuredSize;
}

完整代码

经过拆解步骤之后,其实基本绘制流程都结束了,然而我们的控件还不能使用,那么还欠缺什么呢?

  • 我们还没进行时分秒的计算,以及定时执行重绘。
  • onDraw()方法里的代码绝对不能像上面那么写,我们不能在onDraw()方法里面创建大量对象,前面的代码只是为了方便讲解这么写。
  • 也许我们还要定义一些public方法,提供一些操作时钟的方法。

由于前面所提到的东西不是某一块的内容,而是整体,所以放在完整代码里面一起展现

/**
 * 一个平凡的时钟
 */
public class ClockView extends View{

    private final float paddingSpace = 10;              //控件内容到控件边界的距离
    private final float circleWidth = 20;               //圆周宽度

    private final Paint circlePaint = new Paint();      //绘制圆周用的画笔
    private final Paint linePaint = new Paint();        //绘制刻度线用的笔
    private final Paint textPaint = new Paint();        //绘制文字用的画笔
    private final Paint pointerPaint = new Paint();     //绘制指针用个画笔
    private final Paint arcPaint = new Paint();         //画弧线的笔
    private final RectF arcRectF = new RectF();         //画弧线用的矩形

    //时、分、毫秒,为了技术方便没有使用秒,而是毫秒,不过对外提供的是秒
    private int hour = 8;
    private int minute = 0;
    private int millisecond = 0;
    //标记时钟是否是在运行
    private boolean isRun;

    public ClockView(Context context) {
        super(context);
        init();
    }

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    //对绘图所用的工具做初始化
    private void init(){
        //绘制时钟盘的画笔的初始化
        circlePaint.setColor(Color.RED);
        circlePaint.setStrokeWidth(circleWidth);
        circlePaint.setStyle(Paint.Style.STROKE);

        //绘制刻度线画笔的初始化
        linePaint.setColor(Color.BLACK);
        linePaint.setStrokeWidth(4);

        //绘制文字画笔的初始化
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(30);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTextAlign(Paint.Align.CENTER);

        //绘制指针画笔的初始化
        pointerPaint.setColor(Color.BLACK);
        pointerPaint.setStrokeWidth(10);

        //绘制弧线画笔的初始化
        arcPaint.setColor(Color.GREEN);
        arcPaint.setStrokeWidth(circleWidth);
        arcPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasuredSize(widthMeasureSpec), getMeasuredSize(heightMeasureSpec));
    }

    //都是比较常规的测量代码
    private int getMeasuredSize(int measureSpec){
        int measuredMode = MeasureSpec.getMode(measureSpec);
        //默认大小要稍微大一些
        int measuredSize = 800;
        if(measuredMode == MeasureSpec.EXACTLY){
            measuredSize = MeasureSpec.getSize(measureSpec);
        }else if(measuredMode == MeasureSpec.AT_MOST){
            measuredSize = Math.min(measuredSize, MeasureSpec.getSize(measureSpec));
        }
        return measuredSize;
    }

    //这里是核心代码
    @Override
    protected void onDraw(Canvas canvas) {
        int halfMeasuredWidth = getMeasuredWidth()/2;
        canvas.translate(halfMeasuredWidth, getMeasuredHeight()/2);
        //画圆
        float radius = halfMeasuredWidth - paddingSpace - circleWidth/2;
        canvas.drawCircle(0, 0, radius, circlePaint);
        //画刻度以及写字,普通刻度线长为10,整点刻度线长度为20
        canvas.save();
        //将刻度1转到最上面,30是相应的角度
        canvas.rotate(30);
        //在转动坐标系过程里刻度线的起点位置、普通线的终点位置,20表示线的长度,可以根据自己需要修改
        float lineStart = -(radius-circleWidth/2), commonLineEnd = lineStart+20;
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        //这里的20表示的是整点的刻度线比普通刻度线长多少,可以修改,但是记住在循环里面的划线部分需要跟着变化
        float textY = commonLineEnd+20-fontMetrics.ascent;//计算基线Y轴坐标
        for(int lineNumber = 5, clockNumber = 1; lineNumber < 65; lineNumber++){
            if(lineNumber%5 == 0){
                //如果画到了整点
                canvas.drawLine(0, lineStart, 0, commonLineEnd+20, linePaint);
                canvas.drawText(clockNumber+"", 0, textY, textPaint);
                clockNumber++;
            }else{
                //绘制普通的刻度
                canvas.drawLine(0, lineStart, 0, commonLineEnd, linePaint);
            }
            canvas.rotate(6);
        }
        canvas.restore();
        //画指针
        canvas.save();
        //画时针
        float pointerDegree = ( hour+minute/60f )*360/12;     //计算时针的角度
        canvas.rotate(pointerDegree);
        canvas.drawLine(0, 40, 0, -radius/3, pointerPaint);
        canvas.restore();
        //画分针
        canvas.save();
        pointerDegree = minute/60f*360;
        canvas.rotate(pointerDegree);
        canvas.drawLine(0, 40, 0, -radius/2, pointerPaint);
        canvas.restore();
        //用弧线的形式来填充秒数
        float position = halfMeasuredWidth-paddingSpace-circleWidth/2;
        arcRectF.set(-position, -position, position, position);
        canvas.drawArc(arcRectF, -90, millisecond /60000f*360, false, arcPaint);
        super.onDraw(canvas);
        //如果时钟需要继续走动
        if(isRun){
            calculateTime();
            postInvalidateDelayed(100);
        }
    }

    //在时间变化过程中计算时间用的方法
    private void calculateTime(){
        if(millisecond == 60000){
            millisecond = 0;
            if(minute == 59){
                minute = 0;
                if(hour == 11) hour = 0;
                else hour++;
            }else{
                minute++;
            }
        }else{
            millisecond+=100;
        }
    }

    /**
     * 设置要显示的时间
     * @param hour 小时(0-11)
     * @param minute 分钟(0-59)
     * @param second 秒钟(0-59)
     */
    public void setTime(int hour, int minute, int second){
        //输入有误
        if(hour<0 || hour>11 || minute<0 || minute>59 || second<0 || second>59){
            return;
        }
        this.hour = hour;
        this.minute = minute;
        this.millisecond = second*1000;
        invalidate();
    }

    /**
     * 让时钟开始运行
     */
    public void star(){
        isRun = true;
        invalidate();
    }

    /**
     * 让时钟停止运行
     */
    public void paus(){
        isRun = false;
    }
}

项目源码:
https://github.com/kingfarou/SimpleCustomView

小结

  1. 其实整个例子做下来,除了一些数学计算比较繁琐,并没有很大的难度,主要就是API的调用。
  2. 通过时钟的例子,我们学习到怎么样“围绕圆周绘制各种东西”,这种需求如果你不用些技巧,你可能要进行很繁琐的三角函数计算。
  3. 你有没有注意到,我们至始至终没有调用任何动画相关的东西,然而我们做出了动态的效果?核心的问题就在于绿色弧线的绘制,通过不断地调用invalidate()方法或者postInvalidateDelayed()方法执行重绘,在每一次的重绘里一点点地改变弧线角度,就达到了动画效果。为什么说时钟效果是一个很经典的自定义View?因为现在一些很流行的效果比如音频图、心电图、进度条,其实他们在动态变化这方面和时钟的逻辑都是一样的,都是通过在不断地重绘当中,一点点的改变状态,达到了动态的效果。如果你能很好地理解时钟动态效果的实现逻辑,你能实现很多其他View的效果。

自定义View并没有想象当中那么难,今天的分享到此结束。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值