Android 自定义时钟控件 时针、分针、秒针的绘制这一篇就够了


作者:没有伞的壹哥哥 


转载链接:

https://juejin.im/post/5d00f09c6fb9a07ef06f8d03


文末有彩蛋


前言


对于 Android 开发者来说,自定义 View 是绕不开的一个坎。二对一自定义 View 自定义时钟必然是首选,那么我们该如何绘制自定义时钟呢?本篇我结合 github 上一个有趣的三方库,来给大家讲讲如何作出我们的第一个时钟。


640?wx_fmt=gif



前期准备


对于所有的自定义 View 来说,构造方法、onMeasure(),onDraw() 这几个方法都是必不可少的。所以哦你们先打出这套模版:

 
 
 
   public ClockView(Context context) {	
        super(context);	
    }	
	
    public ClockView(Context context, AttributeSet attrs) {	
        super(context, attrs);	
    }	
	
    @Override	
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {	
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);	
    }	
	
    @Override	
    protected void onDraw(Canvas canvas) {	
        super.onDraw(canvas);	
    }

重写 onMessure() 方法


重写 onMeasure() 方法的本质在于配置控件大小,而配置控件大小的重点就在于配置 setMeasuredDimension(..., ...) 方法。关于具体的配置细节可以参照:点击查看 blog.csdn.net/qq_43377749… 这里以为是自定义时钟控件,所以内容很简单,在三种模式下分别放回三种值即可:

 
 
	
    @Override	
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {	
        setMeasuredDimension(measureDimension(widthMeasureSpec), measureDimension(heightMeasureSpec));	
    }	
	
    private int measureDimension(int measureSpec) {	
        int defaultSize = 800;	
        int model = MeasureSpec.getMode(measureSpec);	
        int size = MeasureSpec.getSize(measureSpec);	
        switch (model) {	
            case MeasureSpec.EXACTLY:	
                return size;	
            case MeasureSpec.AT_MOST:	
                return Math.min(size, defaultSize);	
            case MeasureSpec.UNSPECIFIED:	
                return defaultSize;	
            default:	
                return defaultSize;	
        }	
    }


配置 xml 文件


因为是自定义控件,所以逼着在这里自定义了一个控件属性文件,位于 /res/values/attr.xml 具体内容如下:

 
 
<?xml version="1.0" encoding="utf-8"?>	
<resources>	
    <declare-styleable name="ClockView">	
        <attr name="clock_backgroundColor" format="color" />	
        <attr name="clock_lightColor" format="color" />	
        <attr name="clock_darkColor" format="color" />	
        <attr name="clock_textSize" format="dimension" />	
    </declare-styleable>	
 	
</resources>

开始搭建之旅


现在让我们开始搭建时钟,由于是时钟的搭建,所以我们基本可以分为一下三个步骤:


  • 获得当前系统时间

  • 绘制时针

  • 绘制分针

  • 绘制秒针


获取当前时间


首先,要绘制时钟,必然要获得当前的时间,要不三根指针非重合在一起不可,所以让我们先来研究下如何获得当前系统时间,这里我们就需要使用到一个叫做 Calendar 的工具类,Calendar 是 Android 开发中需要获取时间时必不可少的一个工具类。


所需要的信息基本可以分为


  • milliSecond (毫秒,保证秒针滚动平滑不是一跳一跳)

  • second

  • minute

  • hour

 
 
 
 
  private void getCurrentTime() {	
        Calendar calendar = Calendar.getInstance();	
        float milliSecond = calendar.get(Calendar.MILLISECOND);	
        float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;// 精确到小数点后 保证圆滑	
        float minute = calendar.get(Calendar.MINUTE) + second / 60;	
        float hour = calendar.get(Calendar.HOUR) + minute / 60;	
    }

 
 
	
        Calendar calendar = Calendar.getInstance();	
        float milliSecond = calendar.get(Calendar.MILLISECOND);	
        float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;// 精确到小数点后 保证圆滑	
        float minute = calendar.get(Calendar.MINUTE) + second / 60;	
        float hour = calendar.get(Calendar.HOUR) + minute / 60;	
    }

但是这里有个问题,自定义 View 中,绘制时是根据某个倾斜角度进行绘制的,而非给系统一个浮点型的时间,他就会自动取绘制。所以这里我们还需要知道 每个时间(分秒时,占总时间的比重所代表的偏转角),在这里我们这三个全局私有变量:

 
 
    /* 时针角度 */	
    private float mHourDegree;	
    /* 分针角度 */	
    private float mMinuteDegree;	
    /* 秒针角度 */	
    private float mSecondDegree;    	
    private void getCurrentTime(){	
        Calendar calendar = Calendar.getInstance();	
        float milliSecond = calendar.get(Calendar.MILLISECOND);	
        float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;	
        float minute = calendar.get(Calendar.MINUTE) + second / 60;	
        float hour   = calendar.get(Calendar.HOUR)   + minute / 60;	
        mSecondDegree = second / 60 * 360;	
        mMinuteDegree = minute / 60 * 360;	
        mHourDegree   = hour   / 60 * 360;	
	
    }

最后别忘了在 onDraw() 中调用。


绘制秒针


为了区别于时针分针单一的矩形,这里的秒针我们用一个三角尖代替:


  • 既然是绘制图像,自定义画笔就是必不可少的

  • 然后,画笔是用于上色的,所以我们还需要一个 Path 类对象将这个小三角的边界画出来

  • 由于绘制是在成员方法中进行,所以我们需要定一个 Canvas 对象,来保存 onDraw() 中由于绘制视图的 Canvas

  • 除此之外,秒针是有长度的,所以我们需要一个整型长度变量

  • 最后,我们还需要一个整型变量来存储颜色值,颜色值应该从我们先前定义的 xml 文件的属性中获取。


定义画笔和颜色


由于时间是需要反复更新的,所以 onDraw() 方法也是要被反复调用的。这就使得 Paint 等变量不能再其中定义,而需要在构造方法中定义,否则难免有内存溢出的风险。

 
 
	
    /* 亮色,用于分针、秒针、渐变终止色 */	
    private int mLightColor;	
    /* 秒针画笔 */	
    private Paint mSecondHandPaint;	
    public ClockView(Context context, AttributeSet attrs) {	
        super(context, attrs);	
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);	
        mLightColor = ta.getColor(R.styleable.ClockView_clock_lightColor, Color.parseColor("#ffffff"));	
        ta.recycle();	
 	
        mSecondHandPaint = new Paint(Paint.ANTI_ALIAS_FLAG);	
        mSecondHandPaint.setStyle(Paint.Style.FILL);	
        mSecondHandPaint.setColor(mLightColor);	
    }


定义长度值和 Path


长度值和 Path 的定义和 Paint 一样,不适合在 onDraw() 中,建议大家在 onSizeChanged 中定义,这个方法的提供了测量长度的各个形参。

 
 
    /* 加一个默认的padding值,为了防止用camera旋转时钟时造成四周超出view大小 */	
    private float mDefaultPadding;	
    private float mPaddingTop;	
    /* 时钟半径,不包括padding值 */	
    private float mRadius;	
    @Override	
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {	
        super.onSizeChanged(w, h, oldw, oldh);	
        mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(),	
                h - getPaddingTop() - getPaddingBottom()) / 2;// 各个指针长度	
        mDefaultPadding = 0.12f * mRadius;	
        mPaddingTop = mDefaultPadding + h / 2 - mRadius + getPaddingTop();// 钟离上边界距离	
    }

绘制秒针


这里把绘制方法命名为:drawSecondNeedle() 首先我们需要获得 Canvas 参数:

 
 
    /* 秒针路径 */	
    private Path mSecondHandPath = new Path();	
    @Override	
    protected void onDraw(Canvas canvas) {	
        super.onDraw(canvas);	
        mCanvas = canvas;	
        getCurrentTime();	
        drawSecondNeedle();	
        invalidate();	
    }

根据这个参数我们开始绘制秒针:

  • 首先绘制画笔的 Style 设为 FILL 填充

  • 定义一个 Path 对象由于绘制

  • 调用 Path 对象的 moveTo 方法设定绘制起点

  • 调用 lineTo 方法,绘制线条

  • 调用 Canvas 的 close 方法将终点与起点连线形成封闭图形

 
 
    private void drawSecondNeedle() {	
        mCanvas.save();// ❑ save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。	
        mCanvas.rotate(mSecondDegree, getWidth() / 2, getHeight() / 2);// 设置指针位置	
        mSecondHandPath.reset();	
        float offset = mPaddingTop;	
	
        mSecondHandPath.moveTo(getWidth() / 2, offset + 0.26f * mRadius);// 这三行绘制三角尖	
        mSecondHandPath.lineTo(getWidth() / 2 - 0.05f * mRadius, offset + 0.34f * mRadius);	
        mSecondHandPath.lineTo(getWidth() / 2 + 0.05f * mRadius, offset + 0.34f * mRadius);	
        mSecondHandPath.close();	
        mSecondHandPaint.setColor(mLightColor);	
        mCanvas.drawPath(mSecondHandPath, mSecondHandPaint);	
        mCanvas.restore();// ❑ restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。	
    }
640?wx_fmt=gif


绘制分针


要绘制分针首先得有以下准备:


  • 一个用于绘制分针的 Path 对象

  • 一个用于绘制中心轴圆圈的 RectF 对象

  • 一个用于画笔对象

 
 
    /* 分针路径 */	
    private Path mMinuteHandPath = new Path();	
    /* 分针画笔 */	
    private Paint mMinuteHandPaint;	
    /* 小时圆圈的外接矩形 */	
    private RectF mCircleRectF = new RectF();	
    /**	
     * 绘制分针	
     */	
    private void drawMinuteNeedle() {	
        mCanvas.save();	
        mCanvas.rotate(mMinuteDegree, getWidth() / 2, getHeight() / 2);	
        mMinuteHandPath.reset();	
	
        float offset = mPaddingTop ;	
        mMinuteHandPath.moveTo(getWidth() / 2 - 0.01f * mRadius, getHeight() / 2 - 0.03f * mRadius);	
        mMinuteHandPath.lineTo(getWidth() / 2 - 0.008f * mRadius, offset + 0.365f * mRadius);	
        mMinuteHandPath.quadTo(getWidth() / 2, offset + 0.345f * mRadius,	
                getWidth() / 2 + 0.008f * mRadius, offset + 0.365f * mRadius);	
        mMinuteHandPath.lineTo(getWidth() / 2 + 0.01f * mRadius, getHeight() / 2 - 0.03f * mRadius);	
        mMinuteHandPath.close();	
        mMinuteHandPaint.setStyle(Paint.Style.FILL);	
        mCanvas.drawPath(mMinuteHandPath, mMinuteHandPaint);	
	
        mCircleRectF.set(getWidth() / 2 - 0.03f * mRadius, getHeight() / 2 - 0.03f * mRadius,//绘制指针轴的小圆圈	
                getWidth() / 2 + 0.03f * mRadius, getHeight() / 2 + 0.03f * mRadius);	
        mMinuteHandPaint.setStyle(Paint.Style.STROKE);	
        mMinuteHandPaint.setStrokeWidth(0.02f * mRadius);	
        mCanvas.drawArc(mCircleRectF, 0, 360, false, mMinuteHandPaint);	
        mCanvas.restore();	
    }
  • 首先我们根据之前计算获得的角度旋转画笔到当前要绘制的时间

  • 然后我们绘制分针,绘制方法很简单,首先我们将画笔移到 View 中心篇左的地方

  • 然后用 lineTo 绘制一条直线

  • 接着用 quadTo 绘制一条曲线到右边对称点

  • 再接着 用 lineTo 绘制一条直线到中心篇右

  • 最后调用 close 方法闭合图形即可


640?wx_fmt=other


至于绘制圆心轴的方法就不说了 就是最基本的绘制圆的方法,先设定 RectF 对象,在调用 fraeArc 方法绘制即可。


绘制时针


绘制是真的过程与绘制分针一模一样,由于轴心圆的 RectF 可以直接调用之前绘制分针用到的,所以甚至是更简单些:

 
 
    /* 时针路径 */	
    private Path mHourHandPath = new Path();	
    /* 时针画笔 */	
    private Paint mHourHandPaint;	
    /**	
     * 绘制时针	
     */	
    private void drawHourHand() {	
        mCanvas.save();	
        mCanvas.rotate(mHourDegree, getWidth() / 2, getHeight() / 2);	
        mHourHandPath.reset();	
        float offset = mPaddingTop;	
        mHourHandPath.moveTo(getWidth() / 2 - 0.018f * mRadius, getHeight() / 2 - 0.03f * mRadius);	
        mHourHandPath.lineTo(getWidth() / 2 - 0.009f * mRadius, offset + 0.48f * mRadius);	
        mHourHandPath.quadTo(getWidth() / 2, offset + 0.46f * mRadius,	
                getWidth() / 2 + 0.009f * mRadius, offset + 0.48f * mRadius);	
        mHourHandPath.lineTo(getWidth() / 2 + 0.018f * mRadius, getHeight() / 2 - 0.03f * mRadius);	
        mHourHandPath.close();	
        mHourHandPaint.setStyle(Paint.Style.FILL);	
        mCanvas.drawPath(mHourHandPath, mHourHandPaint);	
	
        mCircleRectF.set(getWidth() / 2 - 0.03f * mRadius, getHeight() / 2 - 0.03f * mRadius,	
                getWidth() / 2 + 0.03f * mRadius, getHeight() / 2 + 0.03f * mRadius);	
        mHourHandPaint.setStyle(Paint.Style.STROKE);	
        mHourHandPaint.setStrokeWidth(0.01f * mRadius);	
        mCanvas.drawArc(mCircleRectF, 0, 360, false, mHourHandPaint);	
        mCanvas.restore();	
    }	
}

最后运行效果:


640?wx_fmt=gif

让我们优化下界面:


640?wx_fmt=gif


到此为止我们的小时钟就定义完啦,如果大家阅读过程中发现错误,欢迎评论区中指出呦~~

推荐阅读

分享一套Android快速开发模板,包含常用主流框架,下载即用

简历上的哪些内容才是 HR 眼中的干货?

微信扫一扫识别小程序


640?wx_fmt=png长按识别小程序,参与抽奖

640?wx_fmt=png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值