Android 用Canvas轻松绘制一个时钟

接下来这篇文章主要是结束如何轻松自定义一个View并使用Canvas绘制一个时钟的案例,话不多说先上图瞅瞅,一共实现了两种效果,一种是秒动(秒针一秒走一针)、另一种是连动式的,秒针不会停会一直走动,话不多说,先看看下面两张效果图。
秒动.gif
连动式.gif
好了上面的图就是目前两种效果,纯原生代码实现,思路也比较简单,根据Canvas绘制一个紫色圆形背景、然后绘制刻度尺、接着绘制时针、分针、秒针,其中主要的是时分秒三个指针是如何转动的,下面来说说具体思路。
##1、绘制表盘
首先我们先来简单看一下如何绘制表盘,因为这里比较简单,我们直接上代码看一下就能明白。

//绘制紫色背景
private void drawClockBg(Canvas canvas) {
        mClockBgPaint.setStyle(Paint.Style.FILL);
        mClockBgPaint.setColor(0xFF6200EE);
        canvas.drawCircle(
                mWidth / 2f,
                mHeight / 2f,
                centerPoint - marginOffset,
                mClockBgPaint
        );
    }
//绘制刻度尺
 private void drawScaleLine(Canvas canvas) {
        canvas.save();
        //循环绘制刻度线
        for (int i = 0; i < 60; i++) {
            canvas.drawLine(
                    centerPoint,
                    marginOffset,
                    centerPoint,
                    (i % 5 == 0) ? mScaleLongSize : mScaleSize,
                    mClockScalePaint
            );
            canvas.rotate(CLockUtils.CLOCK_CYCLE / CLockUtils.CLOCK_SCALE_SIZE,
                    Math.min(mWidth, mHeight) / 2f,
                    Math.min(mWidth, mHeight) / 2f);
        }
        canvas.restore();
    }

这里非常的简单刻度主要循环旋转画布绘制每秒钟/分钟的刻度,其中小时刻度会比分钟/秒钟刻度稍长一些,然后就是绘制重要知识点,就是此处使用了canvas.save & canvas.restore()其主要作用是为了不影响其它画布的绘制。

canvas.save 保存当前矩阵和剪辑到一个私有堆栈。后续调用转换、缩放、旋转、倾斜、concat或clipRect,clipPath将所有操作照常。这里直白点理解可以理解为把先前画布的效果存起来然后再作画,绘制不会影响先前的画布状态。
canvas.restore() 恢复画布先前保存的状态。

##2绘制时针、分针、秒针
刻度绘制完后我们接下来需要开始绘制时针、分针、秒针了。这三根指针的绘制操作其实是一致的包括旋转,这里为了减少代码冗余,抽出公共方法来处理,只不过他们绘制的画笔参数以及旋转角度等参数不同,我们来看下代码。

//绘制时针、分钟、秒针的公共方法
 private void drawPointer(Canvas canvas, float degress, float startX, float startY,
                             float stopX, float stopY, @androidx.annotation.NonNull Paint paint) {
        //保存画布当前状态
        canvas.save();
        //旋转秒针画布
        canvas.rotate(degress, centerPoint, centerPoint);
        //绘制秒针
        canvas.drawLine(startX, startY, stopX, stopY, paint);
        //恢复画布状态
        canvas.restore();
    }

这里也非常好理解,代码也做了精剪,这里并非主要的逻辑,这里仅仅是根据传入的角度degress来控制旋转角度,主要逻辑是如何使指针转动起来并且是准确的转动,这里是通过自己计算后得来的,具体对代码做了详细的注释,来上代码:

//根据时间计算时针、分针、秒针的旋转角度
    private void calculateDegrees() {
        minuteDegrees = 0;
        hourDegrees = 0;

        //获取当前小时数
        //当前小时数
        int currentHour = Calendar.getInstance().get(Calendar.HOUR);
        //获取当前分钟数
        //当前分钟数
        int currentMinute = Calendar.getInstance().get(Calendar.MINUTE);
        //计算分针旋转的角度(当前分钟数 / 刻度数60° * 周期数360°)
        minuteDegrees = currentMinute / CLockUtils.CLOCK_SCALE_SIZE * CLockUtils.CLOCK_CYCLE;
        //根据分针旋转角度,计算时针偏移量(分钟旋转角度 / 周期数360° * 一小时间隔角度30°) + (一小时间隔角度30° * 当前小时数)
        float minutesDegressOffset = (minuteDegrees / CLockUtils.CLOCK_CYCLE * 30) + (30 * currentHour);
        //计算时针旋转的角度(当前小时数 / 一周小时总数12小时 / 一周刻度数60 * 周期数360° + 分钟偏移量)
        hourDegrees = currentHour / CLockUtils.CLOCK_HOURS_SIZE /
                CLockUtils.CLOCK_SCALE_SIZE * CLockUtils.CLOCK_CYCLE + minutesDegressOffset;

        //获取当前秒数
        currentSecond = Calendar.getInstance().get(Calendar.SECOND);
        //计算秒针旋转的角度(当前秒数/刻度数60°/周期数360°)
        secondDegrees = currentSecond / CLockUtils.CLOCK_SCALE_SIZE * CLockUtils.CLOCK_CYCLE;
    }

这里的计算逻辑稍微复杂一些简单来说一下逻辑这里以秒动模式为例,首先秒针转动每秒转动一个刻度,总共60个刻度,那么获取当前秒数 / 刻度数60 / 一周是360度得出每秒的角度,分钟也是同样的逻辑,根据分钟数来计算,这里比较复杂的是时针的角度了,首先计算分钟的角度(当前分钟数 / 刻度数60° * 周期数360°)。

然后由于分针转动一周,时针转动的角度是一个大刻度也就是比如现在是3点,时针指向3的刻度,如果分针转动一周就是4点钟,那么3-4就是一个大刻度就是30度角,那么此时根据分针旋转角度,计算时针偏移量(分钟旋转角度 / 周期数360° * 一小时间隔角度30°) + (一小时间隔角度30° * 当前小时数),这个偏移量就好比假设分针从0刻度就是12点这个刻度位置走到了3点半的刻度,其实时针是会有转动的,不会继续停留在3的刻度,而是3-4之间,这个偏移量就是计算时针走了多少的,最后根据我自己思考的出的公式,计算时针旋转的角度(当前小时数 / 一周小时总数12小时 / 一周刻度数60 * 周期数360° + 分钟偏移量)。这里也是花了比较多的时间来思考的,也是主要的算法逻辑,看不懂的小伙伴可以多看几遍或者自己上手敲一敲,或者跟一下我的代码。

##3执行转动(秒动还是连动)
这里处理指针转动主要是基于Android的消息机制,使用Handler间隔1000ms来发送消息(这是秒动逻辑),连动则是根据当前秒数与60s的时间差来,然后通过属性动画ValueAnimator来做秒针动画,下面来看下代码你就明白了。

//旋转指针
    private void rotatePointer() {
        calculateDegrees();
        //秒动
        if (isSecondAnimator) {
            invalidate();
            mHandler.postDelayed(mClockRunnable, DELAYM_ILLIS);
            return;
        }
        //非秒动
        long delayMillts = DELAYM_ILLIS * (60 - currentSecond);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(secondDegrees, CLockUtils.CLOCK_CYCLE);
        valueAnimator.setDuration(delayMillts);
        valueAnimator.setInterpolator(null);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                secondDegrees = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
        mHandler.postDelayed(mClockRunnable, delayMillts);
    }

秒动大家都能理解就是一秒钟发一次消息,请求绘制一下即可,连动比较复杂,是根据获取的当前秒数与60做差值然后乘1000ms得来的,例如现在打开时钟控件页面,此时的秒数是45s,那么秒针初始化位置是45s的位置,那么走完60s就只剩15s的时间了,所以这里的delayMillts就是当前秒数与60s的差值,然后做动画即可。

家人们逻辑说到这里基本上就说完了,这里代码都是一片段的形式附上的,具体代码我已经上传到github上了,有兴趣的小伙伴可以checkout了解一下。
GitHub链接>>>点击跳转
家人们觉得不错的话,github给个star哈,感恩!

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值