自定義view之繪制模擬時(shí)鐘

原創(chuàng)發(fā)布地址

之前在自定義view之寫一個(gè)帶刪除按鈕的Edittext中簡單介紹了如何繼承Edittext實(shí)現(xiàn)點(diǎn)擊區(qū)域刪除全部文字。

自定義view之可伸縮的圓弧與扇形中介紹了如何制作帶有動(dòng)畫效果的圓弧和扇形圖。

模擬時(shí)鐘實(shí)現(xiàn)思路

前邊兩篇都是入門文章,這篇算是一個(gè)基礎(chǔ)文章,我們來制作一個(gè)模擬時(shí)鐘,與手機(jī)上的時(shí)間保持同步運(yùn)轉(zhuǎn)。首先看一下我自己的做的效果圖(很low的一個(gè)界面):

可以看到在53分鐘結(jié)束到54分鐘開始的時(shí)候,時(shí)針分針秒針基本保持與時(shí)間同步(實(shí)際在繪制過程中由于三角函數(shù)的double類型轉(zhuǎn)float類型,以及π的位數(shù),還是會(huì)有誤差)。

時(shí)鐘實(shí)現(xiàn)的難點(diǎn)在于如何繪制指針的重點(diǎn)坐標(biāo)并時(shí)刻刷新保持與手機(jī)同步。此處我采用了取巧的方式,后邊會(huì)詳細(xì)介紹。

初始化工作

首先同樣需要繼承view類作為父類,并實(shí)現(xiàn)幾個(gè)構(gòu)造函數(shù)。

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函數(shù)中定義了一系列的畫筆等工具。

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一定要設(shè)置好,F(xiàn)ILL是填充,畫出來的是實(shí)心的,STROKE是描邊,畫出來的是空心的。其實(shí)也可以用一個(gè)畫筆然后再每次繪制的時(shí)候不斷重新設(shè)置也可以。

畫筆中定義width等參數(shù)的時(shí)候一般是以px為單位,但是更多的時(shí)候我們需要以dp為單位,此處可以稍微注意一下,px與dp的轉(zhuǎn)換。

我們知道,要想獲得view的實(shí)際尺寸要在onsizechange方法中。在onsizechange方法中我們獲取了一些在繪圖中會(huì)用到的尺寸,實(shí)際需要的是一個(gè)正放形,所以取了區(qū)域中上邊的一個(gè)方形。

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);

    }

暴露接口

為了讓時(shí)鐘啟動(dòng),我們需要自定一個(gè)外部可以訪問的方法來啟動(dòng)時(shí)鐘: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();

    }

這個(gè)方法中首先定義了一個(gè)內(nèi)部類MyTime,用來獲取當(dāng)前時(shí)間的時(shí)分秒。內(nèi)部類的核心方法:

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);
        }

計(jì)算時(shí)間的起始位置

我們定義了三個(gè)動(dòng)畫時(shí)間引擎,這三個(gè)引擎分別負(fù)責(zé)時(shí)針、分針、秒針的運(yùn)動(dòng)。設(shè)置三個(gè)指針的起始值要根據(jù)我們獲取的當(dāng)前時(shí)間來定義:

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

此處要復(fù)習(xí)一下三角函數(shù)的相關(guān)知識(shí)。

我們的起始位置是在屏幕的最左邊高度的中點(diǎn),但是這個(gè)位置并不是我們需要的12點(diǎn)起始位置,為了公式計(jì)算方便,我們需要的是將他順時(shí)針旋轉(zhuǎn)90度以后的位置,也就是屏幕寬度的中點(diǎn)高度的起點(diǎn)位置。

  • 秒針的計(jì)算:
    一周是360度,也就是2π,1分鐘60s,每秒經(jīng)過的角度就是6度。

首先獲取當(dāng)前的秒的時(shí)間,計(jì)算經(jīng)過的秒數(shù),然后換算成弧度,最后加上π的一半,就是我們要展現(xiàn)出來的弧度。此處使用的單位是float單精度浮點(diǎn)型。這就是我們設(shè)置的時(shí)間引擎的起始值。

這個(gè)demo中我設(shè)定的時(shí)間是1個(gè)小時(shí)的動(dòng)畫,所以一個(gè)小時(shí)秒針會(huì)經(jīng)過60圈,最后的中點(diǎn)值就設(shè)為了起始值+60*2π。

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

一小時(shí)是60分鐘,所以每經(jīng)過1分鐘要經(jīng)過6度。為了使程序看起來更準(zhǔn)確,我們還要計(jì)算經(jīng)過的秒數(shù),而不至于在一開始就在一個(gè)不準(zhǔn)確的位置。60秒鐘經(jīng)過6度,則每秒鐘經(jīng)過0.1度,粗略計(jì)算出經(jīng)過的分鐘角度是myTime.getMin() * 6 + myTime.getSec() / 10,然后換算成弧度并加上π/2。

  • 時(shí)針的計(jì)算
    時(shí)針計(jì)算與分針計(jì)算相似,只是注意一小時(shí)走過的角度是30度,所以在換算的時(shí)候要注意經(jīng)過的小時(shí)和經(jīng)過的分鐘的角度關(guān)系。

然后我們?yōu)槊總€(gè)引擎加上了監(jiān)聽方法,這個(gè)方法會(huì)將在每一個(gè)時(shí)刻的具體位置返回給我們。注意默認(rèn)的插值器是低速-高度-低速這樣的速度數(shù)值變化,明顯不是我們要的結(jié)果,我們要用線性插值器來獲得一個(gè)勻速的變化。然后啟動(dòng)動(dòng)畫引擎集合。

繪制

在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);
    }

這個(gè)是繪制背景圓,效果是這樣的

在上一篇文章中已經(jīng)介紹了如何使用paint來畫扇形和弧線,這里就不介紹了,只要設(shè)置起點(diǎn)和重點(diǎn)為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);
    }

這個(gè)是繪制3點(diǎn)6點(diǎn)9點(diǎn)12點(diǎn)的位置。我們使用line加粗實(shí)現(xiàn)的。
完成后效果如下


  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);
    }

這個(gè)是繪制其他小時(shí)的,用的是細(xì)的line實(shí)現(xiàn)。注意角度換算關(guān)系,因?yàn)橐?jì)算時(shí)間的角度,所以三角函數(shù)關(guān)系還是要把這些基本的計(jì)算掌握。效果如下:


4.drawInnerCircle(canvas)

這個(gè)和1是一樣的,只是要繪制實(shí)心將中間的線擋住,所以paint要設(shè)置為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);
    }

主要看一下這個(gè)計(jì)算過程,起始坐標(biāo)是我們的中心點(diǎn)位置,而終點(diǎn)的x軸是中心點(diǎn)減去經(jīng)過角度的余弦值,同樣可計(jì)算得到y(tǒng)。

  1. drawCenter(canvas);
    最后我們做一個(gè)改在所有指針中心上的蓋子。
    最終效果:

下一節(jié)我們將介紹如何繪制一個(gè)日歷,并介紹為何暴露出來的方法startTime會(huì)在所有的重寫方法之前執(zhí)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容