实现原理分析

  • 刻度线绘制:画一个刻度线很简单,就是canvas.drawLine,但是根据角度每30度绘制一个刻度线怎么实现呢,我们一开始想到的可能会是根据角度,利用三角函数等去计算每个刻度线的开始坐标和结束坐标,但这种方式未免过于复杂,稍有不慎就会计算错误。但是利用画布的旋转canvas.rotate就会非常的简单,刻度线只需按照12点钟方向绘制即可,每次绘制完一个刻度线,画布旋转30度,再按照12点钟方向绘制即可。
  • 指针绘制:同样也是通过canvas.drawLine绘制3个指针,为paint设置不同的属性实现时针,分针,秒针的显示样式,同理,如果我们根据角度去计算指针的坐标,那就很复杂,这里也是通过画布的旋转,那么旋转的角度怎么确定呢,就是根据当前时间去确定(具体算法后面代码中具体分析)。
  • 动态:为了实现时钟的动态转动,我们需要在onDraw中每一秒钟获取一次当前时间,然后计算3个指针的旋转角度,再绘制就行了。

这样一分析,其实自定义时钟很简单,就是绘制圆,然后通过画布的旋转绘制刻度线和指针。

具体实现过程

  1. 绘制圆
//绘制圆
canvas.drawCircle(centerX, centerY, radius, circlePaint);

其中centerX和centerY为圆心,用当前控件的中心点即可,radius为圆的半径,采用当前控件宽高的最小值/2 即可,或者自行设置。

  1. 绘制刻度线
    12个刻度线,循环12次,每3个刻度线就是一刻钟的刻度线,可以设置不同的样式区分。然后根据12点钟方向绘制刻度线。
    开始x坐标:圆心x坐标;
    开始y坐标:圆心y坐标-半径+间隙;
    结束x坐标:圆心x坐标;
    结束y坐标:开始y坐标+刻度线长度;
    每绘制完一个刻度线后,画布就在之前的基础上旋转30度,继续绘制12点钟刻度线,这样,刻度线就基于旋转后的画布绘制,也就是斜着绘制了刻度线,很方便的实现了刻度线的绘制。
    这里给出主要的绘制代码,全部代码后面贴出
//刻度线长度
private final static int MARK_LENGTH = 20;

//刻度线与圆的间隙
private final static int MARK_GAP = 12;

//绘制刻度线
for (int i = 0; i < 12; i++) {
    if (i % 3 == 0) {//一刻钟
        markPaint.setColor(mQuarterMarkColor);
    } else {
        markPaint.setColor(mMinuteMarkColor);
    }
    canvas.drawLine(
            centerX,
            centerY - radius + MARK_GAP,
            centerX,
            centerY - radius + MARK_GAP + MARK_LENGTH,
            markPaint);
    canvas.rotate(30, centerX, centerY);
}
canvas.save();
  1. 绘制指针
    绘制时针,分针,秒针,我们分别用3个canvas去绘制,最后再将这3个画布的bitmap绘制到控件的canvas中,为的是单独控制每个画布的旋转角度。
    首先分析时针的指针角度,钟一圈是12个小时,360度,那么每小时就是30度,假设当前时间的小时是h(12小时制),那么时针的旋转角度就是h*30,同刻度线一样,我们也不去计算该角度的指针的各种坐标,而是直接将时针的画布旋转h*30度,然后绘制12点钟方向的时针就行了。
    接着是分针角度,钟一圈是60分钟,360度,那么每分钟就是6度,假设当前时间的分钟是m,那么分针的旋转角度就是m*6
    最后是秒针角度,钟一圈是60秒,360度,那么每秒就是6度,假设当前时间的秒数是s,那么秒针的旋转角度就是s*6
    分析完了时针,分针,秒针的角度获取,那么之后就很简单了,在onDraw中,我们每过一秒获取一次当前时间的时分秒,按照上面的算法计算角度,然后旋转相应的画布,之后绘制相应的指针(当然要注意画布的清空和还原),那么一个随着时间的流逝而旋转的时钟就出来了。
    这里给出绘制时针的主要代码,其他两个指针是类似的,具体代码后面贴出
@Override
protected void onDraw(Canvas canvas) {
    Calendar calendar = Calendar.getInstance();
    int hour12 = calendar.get(Calendar.HOUR);
    int minute = calendar.get(Calendar.MINUTE);
    int second = calendar.get(Calendar.SECOND);

    //保存画布状态
    hourCanvas.save();
    //清空画布
    hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    //旋转画布
    hourCanvas.rotate(hour12 * 30, centerX, centerY);
    //绘制12点钟方向的时针
    hourCanvas.drawLine(centerX, centerY,
            centerX, centerY - hourLineLength, hourPaint);
    //重置画布状态,即撤销之前旋转的角度,回到未旋转之前的状态
    hourCanvas.restore();

    canvas.drawBitmap(hourBitmap, 0, 0, null); 

    //每隔1s重新绘制
    postInvalidateDelayed(1000);
}

但是我们会发现有一点小小的不足,秒针是会一秒一秒的转,但是时针和分针总是在整数位置,当过了60秒,分针才会跳到下一分钟,当过了60分钟,时针才会跳到下一个小时,我们平常看的时钟都是随着秒针的转动,分针和时针都是有一定的偏移量的,当然我们的时钟也要这么炫酷,那么如何计算呢?

时针:前面说过,每小时时针旋转30度,假设当前时间的小时是h(12小时制),那么时针的旋转角度就是h*30。那么每分钟时针旋转多少度呢,答案是30/60=0.5度(每小时60分钟,每小时30度),所以时针的偏移量就是m*0.5,那么假设当前的时间是1:30,那么时针旋转的角度就是1*30+30*0.5,就是45度,改成变量公式就是h*30+m*0.5,那么修改下上面的代码

hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);

分针:假设当前时间的分钟是m,那么分针的旋转角度就是m*6,每秒钟分针旋转6/60(每分钟60秒,每分钟6度),所以分针的偏移量是s*0.1,那么分针画布旋转的的代码就是

minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);

秒针:秒针就按照每秒钟6度旋转

secondCanvas.rotate(second * 6, centerX, centerY);

总结

经过上面的3个步骤,我们就绘制出了一个会慢慢移动的时钟了。

完整的代码和项目大家可以到我的github中查看,里面有相关的使用方法,同时这个项目上传到了maven仓库,可以通过gradle直接使用

compile 'com.don:clockviewlibrary:1.0.1'

github地址:https://github.com/zhijieeeeee/ClockView

完整代码

public class ClockView extends View {

    //使用wrap_content时默认的尺寸
    private final static int DEFAULT_SIZE = 400;

    //刻度线宽度
    private final static int MARK_WIDTH = 8;

    //刻度线长度
    private final static int MARK_LENGTH = 20;

    //刻度线与圆的距离
    private final static int MARK_GAP = 12;

    //时针宽度
    private final static int HOUR_LINE_WIDTH = 10;

    //分针宽度
    private final static int MINUTE_LINE_WIDTH = 6;

    //秒针宽度
    private final static int SECOND_LINE_WIDTH = 4;

    //圆心坐标
    private int centerX;
    private int centerY;

    //圆半径
    private int radius;

    //圆的画笔
    private Paint circlePaint;

    //刻度线画笔
    private Paint markPaint;

    //时针画笔
    private Paint hourPaint;

    //分针画笔
    private Paint minutePaint;

    //秒针画笔
    private Paint secondPaint;

    //时针长度
    private int hourLineLength;

    //分针长度
    private int minuteLineLength;

    //秒针长度
    private int secondLineLength;

    private Bitmap hourBitmap;
    private Bitmap minuteBitmap;
    private Bitmap secondBitmap;

    private Canvas hourCanvas;
    private Canvas minuteCanvas;
    private Canvas secondCanvas;

    //圆的颜色
    private int mCircleColor = Color.WHITE;
    //时针的颜色
    private int mHourColor = Color.BLACK;
    //分针的颜色
    private int mMinuteColor = Color.BLACK;
    //秒针的颜色
    private int mSecondColor = Color.RED;
    //一刻钟刻度线的颜色
    private int mQuarterMarkColor = Color.parseColor("#B5B5B5");
    //分钟刻度线的颜色
    private int mMinuteMarkColor = Color.parseColor("#EBEBEB");
    //是否绘制3个指针的圆心
    private boolean isDrawCenterCircle = false;

    //获取时间监听
    private OnCurrentTimeListener onCurrentTimeListener;

    public void setOnCurrentTimeListener(OnCurrentTimeListener onCurrentTimeListener) {
        this.onCurrentTimeListener = onCurrentTimeListener;
    }

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

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

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClockView);
        mCircleColor = a.getColor(R.styleable.ClockView_circle_color, Color.WHITE);
        mHourColor = a.getColor(R.styleable.ClockView_hour_color, Color.BLACK);
        mMinuteColor = a.getColor(R.styleable.ClockView_minute_color, Color.BLACK);
        mSecondColor = a.getColor(R.styleable.ClockView_second_color, Color.RED);
        mQuarterMarkColor = a.getColor(R.styleable.ClockView_quarter_mark_color, Color.parseColor("#B5B5B5"));
        mMinuteMarkColor = a.getColor(R.styleable.ClockView_minute_mark_color, Color.parseColor("#EBEBEB"));
        isDrawCenterCircle = a.getBoolean(R.styleable.ClockView_draw_center_circle, false);
        a.recycle();
        init();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        reMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        centerX = width / 2 ;
        centerY = height / 2;
        radius = Math.min(width, height) / 2;

        hourLineLength = radius / 2;
        minuteLineLength = radius * 3 / 4;
        secondLineLength = radius * 3 / 4;

        //时针
        hourBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        hourCanvas = new Canvas(hourBitmap);

        //分针
        minuteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        minuteCanvas = new Canvas(minuteBitmap);

        //秒针
        secondBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        secondCanvas = new Canvas(secondBitmap);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制圆
        canvas.drawCircle(centerX, centerY, radius, circlePaint);
        //绘制刻度线
        for (int i = 0; i < 12; i++) {
            if (i % 3 == 0) {//一刻钟
                markPaint.setColor(mQuarterMarkColor);
            } else {
                markPaint.setColor(mMinuteMarkColor);
            }
            canvas.drawLine(
                    centerX,
                    centerY - radius + MARK_GAP,
                    centerX,
                    centerY - radius + MARK_GAP + MARK_LENGTH,
                    markPaint);
            canvas.rotate(30, centerX, centerY);
        }
        canvas.save();

        Calendar calendar = Calendar.getInstance();
        int hour12 = calendar.get(Calendar.HOUR);
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);

        //(方案一)每过一小时(3600秒)时针添加30度,所以每秒时针添加(1/120)度
        //(方案二)每过一小时(60分钟)时针添加30度,所以每分钟时针添加(1/2)度
        hourCanvas.save();
        //清空画布
        hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
        hourCanvas.drawLine(centerX, centerY,
                centerX, centerY - hourLineLength, hourPaint);
        if (isDrawCenterCircle)//根据指针的颜色绘制圆心
            hourCanvas.drawCircle(centerX, centerY, 2 * HOUR_LINE_WIDTH, hourPaint);
        hourCanvas.restore();

        //每过一分钟(60秒)分针添加6度,所以每秒分针添加(1/10)度;当minute加1时,正好second是0
        minuteCanvas.save();
        //清空画布
        minuteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
        minuteCanvas.drawLine(centerX, centerY,
                centerX, centerY - minuteLineLength, minutePaint);
        if (isDrawCenterCircle)//根据指针的颜色绘制圆心
            minuteCanvas.drawCircle(centerX, centerY, 2 * MINUTE_LINE_WIDTH, minutePaint);
        minuteCanvas.restore();

        //每过一秒旋转6度
        secondCanvas.save();
        //清空画布
        secondCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        secondCanvas.rotate(second * 6, centerX, centerY);
        secondCanvas.drawLine(centerX, centerY,
                centerX, centerY - secondLineLength, secondPaint);
        if (isDrawCenterCircle)//根据指针的颜色绘制圆心
            secondCanvas.drawCircle(centerX, centerY, 2 * SECOND_LINE_WIDTH, secondPaint);
        secondCanvas.restore();

        canvas.drawBitmap(hourBitmap, 0, 0, null);
        canvas.drawBitmap(minuteBitmap, 0, 0, null);
        canvas.drawBitmap(secondBitmap, 0, 0, null);

        //每隔1s重新绘制
        postInvalidateDelayed(1000);

        if (onCurrentTimeListener != null) {
            //小时采用24小时制返回
            int h = calendar.get(Calendar.HOUR_OF_DAY);
            String currentTime = intAdd0(h) + ":" + intAdd0(minute) + ":" + intAdd0(second);
            onCurrentTimeListener.currentTime(currentTime);
        }
    }

    /**
     * 初始化
     */
    private void init() {
        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setStyle(Paint.Style.FILL);
        circlePaint.setColor(mCircleColor);

        markPaint = new Paint();
        circlePaint.setAntiAlias(true);
        markPaint.setStyle(Paint.Style.FILL);
        markPaint.setStrokeCap(Paint.Cap.ROUND);
        markPaint.setStrokeWidth(MARK_WIDTH);

        hourPaint = new Paint();
        hourPaint.setAntiAlias(true);
        hourPaint.setColor(mHourColor);
        hourPaint.setStyle(Paint.Style.FILL);
        hourPaint.setStrokeCap(Paint.Cap.ROUND);
        hourPaint.setStrokeWidth(HOUR_LINE_WIDTH);

        minutePaint = new Paint();
        minutePaint.setAntiAlias(true);
        minutePaint.setColor(mMinuteColor);
        minutePaint.setStyle(Paint.Style.FILL);
        minutePaint.setStrokeCap(Paint.Cap.ROUND);
        minutePaint.setStrokeWidth(MINUTE_LINE_WIDTH);

        secondPaint = new Paint();
        secondPaint.setAntiAlias(true);
        secondPaint.setColor(mSecondColor);
        secondPaint.setStyle(Paint.Style.FILL);
        secondPaint.setStrokeCap(Paint.Cap.ROUND);
        secondPaint.setStrokeWidth(SECOND_LINE_WIDTH);

    }

    /**
     * 重新设置view尺寸
     */
    private void reMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (measureWidthMode == MeasureSpec.AT_MOST
                && measureHeightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE);
        } else if (measureWidthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(DEFAULT_SIZE, measureHeight);
        } else if (measureHeightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(measureWidth, DEFAULT_SIZE);
        }
    }

    public interface OnCurrentTimeListener {
        void currentTime(String time);
    }

    /**
     * int小于10的添加0
     *
     * @param i
     * @return
     */
    private String intAdd0(int i) {
        DecimalFormat df = new DecimalFormat("00");
        if (i < 10) {
            return df.format(i);
        } else {
            return i + "";
        }
    }
}

自定义属性

<declare-styleable name="ClockView">
    <attr name="circle_color" format="color" />
    <attr name="hour_color" format="color" />
    <attr name="minute_color" format="color" />
    <attr name="second_color" format="color" />
    <attr name="quarter_mark_color" format="color" />
    <attr name="minute_mark_color" format="color" />
    <attr name="draw_center_circle" format="boolean" />
</declare-styleable>