Android 自定义时钟

428 阅读2分钟

一、前言

时钟绘制有很多案例,但是也考验对坐标系和三角函数的理解。

时钟自定义作为Android 开发入门的经典案例,想必比少人都绘制过各种个样的时钟。从时钟的定义入门无非是最好的,因为在这种案例中,既可以学到文字绘制、线条的绘制、Canvas旋转应有尽有。

不过,这里的难点就要考验对初中和高中数学的熟悉程度,因为在这个过程中需要大量用到余弦函数和正弦函数,甚至还会用到反三角函数。

另外,我们也要熟悉坐标体系的变化,Canvas的任何变换方法都和矩阵有关,只不过官方封装了方法,但抵触逻辑依然是矩阵。

二、实现

2.1 预览效果

2.1.1 有边框的版本

2.1.2 有边框的版本

2.2 实现步骤

其实难点并不多,我们按照顺序和规则绘制即可,主要步骤如下

  • 绘制时钟表盘

  • 绘制刻度

  • 绘制文本

  • 绘制边框

当然,为了便于绘制,我们可以把中心点移动到View中心点,这样,View中心点就是坐标原点。

2.3 详细设计

基本参数设置,主要是绘制的一些工具和变量。

    private static final float _1PX = 1;  // 1px 偏移
    private Paint strokePaint;
    private float radius; //半径
    private float borderSize = 20; //边框宽度
    private boolean stopDraw = false; //tick控制
    private boolean withOutline = false; //边框控制

2.4 文本时间格式化

我们要展示文本时间,当然还需要格式化时间,这里我们使用SimpleDateFormat,这里要补充一点,SimpleDateFormate线程不安全,如果有多线程绘制的需求一定要注意

    final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

2.5 更新逻辑

更新逻辑我们使用View自带的方法,这里我们定义一个Runnable的任务去更新,主要解决两方面的问题,第一是防止非UI线程更新,这个很好理解。第二防止任务频率不稳定,如果在规定的时间内多了了任务,这个时候我们要删除原有的任务,类似debounce的设计。

    Runnable invalidateTask = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };

更新方法如下

   private void startTicking() {
        if (stopDraw) return;
        removeCallbacks(invalidateTask);
        postDelayed(invalidateTask, 500);
    }

2.6 完整逻辑

下面是代码实现,不是很复杂,上手也很容易,所有的绘制基本上都是围绕着圆形展开,另外对于文本得绘制,千万不要旋转,不然9和6颠倒,效果是很奇怪的。

public class TickingClockView extends View {

    private static final float _1PX = 1;  // 1px 偏移
    private Paint strokePaint;
    private float radius; //半径
    private float borderSize = 20; //边框宽度
    private boolean stopDraw = false; //tick控制
    private boolean withOutline = false; //边框控制
    final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

    public TickingClockView(Context context) {
        this(context, null);
    }

    public TickingClockView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TickingClockView(Context context, AttributeSet attrs,
                            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initCanvas(context);
    }

    private void initCanvas(Context c) {
        strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        radius = Math.min(getWidth(), getHeight()) / 2f - borderSize;

        long timeMillis = System.currentTimeMillis();
        String format = sdf.format(timeMillis);

        canvas.save();
        canvas.translate(getWidth() / 2F, getHeight() / 2F);

        //绘制表盘
        strokePaint.setStyle(Paint.Style.FILL);
        strokePaint.setColor(0xFFdddddd);
        canvas.drawCircle(0, 0, radius - borderSize + _1PX, strokePaint);

        strokePaint.setStyle(Paint.Style.STROKE);
        strokePaint.setTextSize(48);
        canvas.rotate(0);
        strokePaint.setStrokeCap(Paint.Cap.ROUND);
        for (int i = 0; i < 12; i++) {
            strokePaint.setColor(0xff333333);
            strokePaint.setStrokeWidth(borderSize);

            double radians = Math.toRadians(-i * 30);
            float startX = (float) (Math.cos(radians) * (radius - borderSize * 3));
            float startY = (float) (Math.sin(radians) * (radius - borderSize * 3));
            float stopX = (float) (Math.cos(radians) * (radius - borderSize));
            float stopY = (float) (Math.sin(radians) * (radius - borderSize));

            String text = "" + (i + 1);
            float textWidth = strokePaint.measureText(text);
            int clockHourPosX = (int) ((Math.cos(Math.toRadians(i * 30 + 270 + 30)) * (radius - borderSize * 5)) - textWidth / 2);
            int clockHourPosY = (int) ((Math.sin(Math.toRadians(i * 30 + 270 + 30)) * (radius - borderSize * 5)) + textWidth / 2);
            canvas.drawLine(startX, startY, stopX, stopY, strokePaint);

            strokePaint.setStrokeWidth(borderSize / 2F);
            strokePaint.setColor(Color.BLACK);
            strokePaint.setStyle(Paint.Style.FILL);
            canvas.drawText(text, clockHourPosX, clockHourPosY, strokePaint);
            for (int j = 1; j < 5; j++) {
                double toRadians = Math.toRadians(-i * 30 - j * 6);
                float minStartX = (float) (Math.cos(toRadians) * (radius - borderSize * 2));
                float minStartY = (float) (Math.sin(toRadians) * (radius - borderSize * 2));
                float minStopX = (float) (Math.cos(toRadians) * (radius - borderSize));
                float minStopY = (float) (Math.sin(toRadians) * (radius - borderSize));
                canvas.drawLine(minStartX, minStartY, minStopX, minStopY, strokePaint);
            }

        }

        String[] strFormatTimes = format.split(":");
        int secondText = Integer.parseInt(strFormatTimes[2]);
        int minuteText = Integer.parseInt(strFormatTimes[1]);
        int hourText = Integer.parseInt(strFormatTimes[0]) % 12;
        strokePaint.setStyle(Paint.Style.STROKE);

        //时针
        double toRadians = Math.toRadians(-90 + hourText * 30 + 30 * minuteText / 60f + 30 * secondText / 3600f);
        float startX = (int) (Math.cos(toRadians));
        float startY = (int) (Math.sin(toRadians));
        float stopX = (int) (Math.cos(toRadians) * (radius - borderSize * 12));
        float stopY = (int) (Math.sin(toRadians) * (radius - borderSize * 12));
        strokePaint.setColor(Color.BLACK);
        strokePaint.setStrokeWidth(15);
        canvas.drawLine(startX, startY, stopX, stopY, strokePaint);

        strokePaint.setStrokeWidth(borderSize);
        canvas.drawCircle(0, 0, borderSize, strokePaint);

        //分针
        double toRadians1 = Math.toRadians(-90 + minuteText * 6 + 6 * secondText / 60f);
        startX = (int) (Math.cos(toRadians1) * (-30));
        startY = (int) (Math.sin(toRadians1) * (-30));
        stopX = (int) (Math.cos(toRadians1) * (radius - borderSize * 8));
        stopY = (int) (Math.sin(toRadians1) * (radius - borderSize * 8));
        strokePaint.setStrokeWidth(6);
        strokePaint.setColor(Color.RED);
        canvas.drawLine(startX, startY, stopX, stopY, strokePaint);

        strokePaint.setStrokeWidth(borderSize / 2f);
        canvas.drawCircle(0, 0, borderSize / 2f, strokePaint);

        //秒针
        double toRadians2 = Math.toRadians(-90 + 6 * secondText);
        startX = (int) (Math.cos(toRadians2) * (-60));
        startY = (int) (Math.sin(toRadians2) * (-60));
        stopX = (int) (Math.cos(toRadians2) * (radius - borderSize * 6));
        stopY = (int) (Math.sin(toRadians2) * (radius - borderSize * 6));
        strokePaint.setStrokeWidth(4);
        strokePaint.setColor(Color.DKGRAY);
        canvas.drawLine(startX, startY, stopX, stopY, strokePaint);

        strokePaint.setColor(Color.CYAN);
        strokePaint.setStrokeWidth(borderSize / 4);
        canvas.drawCircle(0, 0, borderSize / 4, strokePaint);


        strokePaint.setStrokeWidth(1);
        strokePaint.setStyle(Paint.Style.STROKE.FILL);
        strokePaint.setTextSize(60f);

        strokePaint.setColor(Color.BLACK);
        float measureText = strokePaint.measureText(format);
        canvas.drawText(format, -measureText / 2, radius / 2f, strokePaint);

        if (withOutline) {
            //绘制裱框
            strokePaint.setColor(0xFFe5f9ef);
            strokePaint.setStyle(Paint.Style.STROKE);
            strokePaint.setStrokeWidth(borderSize);
            canvas.drawCircle(0, 0, radius - borderSize / 2, strokePaint);
        }
        canvas.restore();
        startTicking();
    }

    Runnable invalidateTask = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };

    private void startTicking() {
        if (stopDraw) return;
        removeCallbacks(invalidateTask);
        postDelayed(invalidateTask, 500);
    }

    public void setWithOutline(boolean withOutline) {
        this.withOutline = withOutline;
        postInvalidate();
    }

    public boolean isWithOutline() {
        return withOutline;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopDraw = true;
    }
}

三、总结

绘制时钟案例是非常好的入门教程,因此很多时候,如果要学习Canvas绘制,简单的案例索然乏味,甚至没有成就感,但是时钟相比来说,会让你感觉的很多收获。

刻度表是非常入门级的,有的博客上说这是程序员进阶的级别,实际上夸大其词。

本篇到这里结束了,希望本篇内容对你有所帮助。