自定义view之绘制模拟时钟

原创发布地址

之前在自定义view之写一个带删除按钮的Edittext中简单介绍了如何继承Edittext实现点击区域删除全部文字。

自定义view之可伸缩的圆弧与扇形中介绍了如何制作带有动画效果的圆弧和扇形图。

模拟时钟实现思路

前边两篇都是入门文章,这篇算是一个基础文章,我们来制作一个模拟时钟,与手机上的时间保持同步运转。首先看一下我自己的做的效果图(很low的一个界面):

可以看到在53分钟结束到54分钟开始的时候,时针分针秒针基本保持与时间同步(实际在绘制过程中由于三角函数的double类型转float类型,以及π的位数,还是会有误差)。

时钟实现的难点在于如何绘制指针的重点坐标并时刻刷新保持与手机同步。此处我采用了取巧的方式,后边会详细介绍。

初始化工作

首先同样需要继承view类作为父类,并实现几个构造函数。

private static final float threeSqure = 1.7320508075689F;
    private static final float PIE = 3.1415926535898F;

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

    public MyClock(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyClock(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

在init函数中定义了一系列的画笔等工具。

private void init() {
        bgPaint = new Paint();
        bgPaint.setStyle(Paint.Style.STROKE);
        bgPaint.setColor(Color.BLACK);
        bgPaint.setStrokeWidth(10);
        bgPaint.setAntiAlias(true);
        boldNumPaint = new Paint();
        boldNumPaint.setStyle(Paint.Style.STROKE);
        boldNumPaint.setColor(Color.BLACK);
        boldNumPaint.setStrokeWidth(20);
        boldNumPaint.setAntiAlias(true);
        thinNumPaint = new Paint();
        thinNumPaint.setStyle(Paint.Style.STROKE);
        thinNumPaint.setColor(Color.BLACK);
        thinNumPaint.setStrokeWidth(10);
        thinNumPaint.setAntiAlias(true);
        secondPaint = new Paint();
        secondPaint.setStyle(Paint.Style.FILL);
        secondPaint.setColor(Color.GREEN);
        secondPaint.setAntiAlias(true);
        secondPaint.setStrokeWidth(10);
        centerPaint = new Paint();
        centerPaint.setStyle(Paint.Style.FILL);
        centerPaint.setColor(Color.BLACK);
        centerPaint.setAntiAlias(true);
        innerPaint = new Paint();
        innerPaint.setStyle(Paint.Style.FILL);
        innerPaint.setColor(Color.WHITE);
        innerPaint.setAntiAlias(true);
    }

此处指明一些需要注意的地方就是setstyle一定要设置好,FILL是填充,画出来的是实心的,STROKE是描边,画出来的是空心的。其实也可以用一个画笔然后再每次绘制的时候不断重新设置也可以。

画笔中定义width等参数的时候一般是以px为单位,但是更多的时候我们需要以dp为单位,此处可以稍微注意一下,px与dp的转换。

我们知道,要想获得view的实际尺寸要在onsizechange方法中。在onsizechange方法中我们获取了一些在绘图中会用到的尺寸,实际需要的是一个正放形,所以取了区域中上边的一个方形。

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.d(TAG, "onSizeChanged");
        super.onSizeChanged(w, h, oldw, oldh);
        this.width = Math.min(w, h);
        this.height = Math.min(w, h);
        inCircle = new RectF(55, 55, width - 55, height - 55);
        outCircle = new RectF(5, 5, width - 5, height - 5);
        radius = (float) ((width - 110) / 2);
        innerCircle = new RectF(100, 100, width - 100, height - 100);

    }

暴露接口

为了让时钟启动,我们需要自定一个外部可以访问的方法来启动时钟:startClock()。

 public void startClock() {
        myTime = new MyTime();
        Log.d(TAG, myTime.toString());
        animatorSecond = ValueAnimator.ofFloat(setSecond(myTime), setSecond(myTime) + 2 * 60 * PIE);
        animatorMinute = ValueAnimator.ofFloat(setMinute(myTime), setMinute(myTime) + 2 * PIE);
        animatorHour = ValueAnimator.ofFloat(setHour(myTime), setHour(myTime) + 6 * PIE / 180);

        animatorSecond.removeAllUpdateListeners();
        animatorMinute.removeAllUpdateListeners();
        animatorHour.removeAllUpdateListeners();

        animatorSecond.setDuration(60 * 1000 * 60);
        animatorMinute.setDuration(60 * 1000 * 60);
        animatorHour.setDuration(60 * 1000 * 60);

        animatorSecond.setInterpolator(new LinearInterpolator());
        animatorMinute.setInterpolator(new LinearInterpolator());
        animatorHour.setInterpolator(new LinearInterpolator());

        animatorSecond.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                passSecondArc = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        animatorMinute.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                passMinuteArc = (float) animation.getAnimatedValue();
            }
        });

        animatorHour.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                passHourArc = (float) animation.getAnimatedValue();
            }
        });
        AnimatorSet set = new AnimatorSet();
        set.removeAllListeners();
        set.playTogether(animatorSecond, animatorMinute, animatorHour);
        set.start();

    }

这个方法中首先定义了一个内部类MyTime,用来获取当前时间的时分秒。内部类的核心方法:

public MyTime() {
            Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
            year = calendar.get(Calendar.YEAR);
            month = calendar.get(Calendar.MONTH);
            day = calendar.get(Calendar.DAY_OF_MONTH);
            hour = calendar.get(Calendar.HOUR_OF_DAY);
            min = calendar.get(Calendar.MINUTE);
            sec = calendar.get(Calendar.SECOND);
        }

计算时间的起始位置

我们定义了三个动画时间引擎,这三个引擎分别负责时针、分针、秒针的运动。设置三个指针的起始值要根据我们获取的当前时间来定义:

private float setSecond(MyTime myTime) {
        float passSecond = myTime.getSec();
        return 6 * passSecond / 180 * PIE + PIE / 2;
    }

此处要复习一下三角函数的相关知识。

我们的起始位置是在屏幕的最左边高度的中点,但是这个位置并不是我们需要的12点起始位置,为了公式计算方便,我们需要的是将他顺时针旋转90度以后的位置,也就是屏幕宽度的中点高度的起点位置。

  • 秒针的计算:
    一周是360度,也就是2π,1分钟60s,每秒经过的角度就是6度。

首先获取当前的秒的时间,计算经过的秒数,然后换算成弧度,最后加上π的一半,就是我们要展现出来的弧度。此处使用的单位是float单精度浮点型。这就是我们设置的时间引擎的起始值。

这个demo中我设定的时间是1个小时的动画,所以一个小时秒针会经过60圈,最后的中点值就设为了起始值+60*2π。

  • 分针的计算
private float setMinute(MyTime myTime) {
        float passMinute = myTime.getMin() * 6 + myTime.getSec() / 10;
        return passMinute / 180 * PIE + PIE / 2;
    }

一小时是60分钟,所以每经过1分钟要经过6度。为了使程序看起来更准确,我们还要计算经过的秒数,而不至于在一开始就在一个不准确的位置。60秒钟经过6度,则每秒钟经过0.1度,粗略计算出经过的分钟角度是myTime.getMin() * 6 + myTime.getSec() / 10,然后换算成弧度并加上π/2。

  • 时针的计算
    时针计算与分针计算相似,只是注意一小时走过的角度是30度,所以在换算的时候要注意经过的小时和经过的分钟的角度关系。

然后我们为每个引擎加上了监听方法,这个方法会将在每一个时刻的具体位置返回给我们。注意默认的插值器是低速-高度-低速这样的速度数值变化,明显不是我们要的结果,我们要用线性插值器来获得一个匀速的变化。然后启动动画引擎集合。

绘制

在onDraw方法中我们要绘制所有的一切图形。

        drawBackGround(canvas);
        draw0369(canvas);
        drawHourGap(canvas);
        drawInnerCircle(canvas);
        drawM(canvas);
        drawS(canvas);
        drawH(canvas);
        drawCenter(canvas);
  1. drawBackGround(canvas)
private void drawBackGround(Canvas canvas) {
        bgPaint.setColor(Color.WHITE);
        bgPaint.setStyle(Paint.Style.FILL);
        canvas.drawRect(0, 0, width, height, bgPaint);
        bgPaint.setColor(Color.BLACK);
        bgPaint.setStyle(Paint.Style.STROKE);
        canvas.drawArc(outCircle, 0, 360, false, bgPaint);
        canvas.drawArc(inCircle, 0, 360, false, bgPaint);
    }

这个是绘制背景圆,效果是这样的

在上一篇文章中已经介绍了如何使用paint来画扇形和弧线,这里就不介绍了,只要设置起点和重点为0-360即可。

  1. draw0369(canvas)
private void draw0369(Canvas canvas) {
        canvas.drawLine(width / 2, 55, width / 2, height - 55, boldNumPaint);
        canvas.drawLine(55, height / 2, width - 55, height / 2, boldNumPaint);
    }

这个是绘制3点6点9点12点的位置。我们使用line加粗实现的。
完成后效果如下


  1. drawHourGap(canvas);
private void drawHourGap(Canvas canvas) {
        canvas.drawLine(radius * (1 - threeSqure / 2) + 55,
                (height - radius) / 2,
                width - 55 - radius * (1 - threeSqure / 2),
                (height + radius) / 2, thinNumPaint);
        canvas.drawLine(radius * (1 - threeSqure / 2) + 55,
                (height + radius) / 2,
                width - 55 - radius * (1 - threeSqure / 2),
                (height - radius) / 2, thinNumPaint);
        canvas.drawLine(radius / 2 + 55,
                height / 2 - radius * threeSqure / 2,
                width - 55 - radius / 2,
                height / 2 + radius * threeSqure / 2, thinNumPaint);
        canvas.drawLine(radius / 2 + 55,
                height / 2 + radius * threeSqure / 2,
                width - 55 - radius / 2,
                height / 2 - radius * threeSqure / 2, thinNumPaint);
    }

这个是绘制其他小时的,用的是细的line实现。注意角度换算关系,因为要计算时间的角度,所以三角函数关系还是要把这些基本的计算掌握。效果如下:


4.drawInnerCircle(canvas)

这个和1是一样的,只是要绘制实心将中间的线挡住,所以paint要设置为FILL。
效果如下:

5.drawM(canvas) drawS(canvas) drawH(canvas);

private void drawM(Canvas canvas) {
        secondPaint.setColor(Color.BLUE);
        secondPaint.setStrokeWidth(20);
        canvas.drawLine(width / 2, height / 2,
                height / 2 - (radius - 80) * (float) Math.cos(passMinuteArc),
                width / 2 - (radius - 80) * (float) Math.sin(passMinuteArc),
                secondPaint);
    }

主要看一下这个计算过程,起始坐标是我们的中心点位置,而终点的x轴是中心点减去经过角度的余弦值,同样可计算得到y。

  1. drawCenter(canvas);
    最后我们做一个改在所有指针中心上的盖子。
    最终效果:

下一节我们将介绍如何绘制一个日历,并介绍为何暴露出来的方法startTime会在所有的重写方法之前执行。

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

推荐阅读更多精彩内容