Android自定义控件:时钟

下面是项目在手机上运行的效果图

GIF演示图

效果图

样式效果演示图

效果图

效果图

效果图

实现原理分析

  • 刻度线绘制:画一个刻度线很简单,就是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 即可,或者自行设置。

  2. 绘制刻度线

    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();
    
  3. 绘制指针

    绘制时针,分针,秒针,我们分别用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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容