Android 自定義View之: 時鐘控件

學習自定義View辰如,遂動手寫了一個時鐘控件拌牲,歡迎批評指正饵隙。

首先上效果圖:


image

GitHub地址 代碼下載

首先撮珠,按需求分析思路:

繪制步驟

  1. 繪制大圓圈、刻度癞季、
  2. 繪制數字
  3. 繪制指針

其他操作

  • 自動開啟計時
  • 適配wrap_content和固定width劫瞳、height值;
  • 自定義屬性:時鐘顏色绷柒、各個刻度顏色志于、三種指針顏色等

接下來按步驟繪制:

我們先看onDraw方法中我們的操作,這里對應我們上面說的步驟:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 坐標原點移動到View 中心
        canvas.translate(mCenterX, mCenterY);
        drawCircle(canvas);
        drawText(canvas);
        drawPointer(canvas);
    }

繪制圓圈和刻度

/**
     * 繪制時鐘的圓形和刻度
     */
    private void drawCircle(Canvas canvas) {

        mDefaultPaint.setStrokeWidth(mDefaultScaleWidth);
        mDefaultPaint.setColor(mClockColor);

        canvas.drawCircle(0, 0, mRadius, mDefaultPaint);

        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) { // 特殊時刻

                mDefaultPaint.setStrokeWidth(mParticularlyScaleWidth);
                mDefaultPaint.setColor(mColorParticularyScale);

                canvas.drawLine(0, -mRadius, 0, -mRadius + mParticularlyScaleLength, mDefaultPaint);

            } else {          // 一般時刻

                mDefaultPaint.setStrokeWidth(mDefaultScaleWidth);
                mDefaultPaint.setColor(mColorDefaultScale);

                canvas.drawLine(0, -mRadius, 0, -mRadius + mDefaultScaleLength, mDefaultPaint);

            }
            canvas.rotate(6);
        }
    }

這里主要有兩點:

1废睦、表示小時的刻度伺绽,我們作為特殊刻度,其線的長度嗜湃、寬度奈应、顏色,與普通刻度做區(qū)別购披;

2杖挣、每次繪制完一個刻度,我們調用canvas.rotate(6)方法刚陡,讓其旋轉6度(360度/60)惩妇,這樣繪制起來比較方便株汉,避免了計算每一個刻度的坐標值。

繪制文字:

 /**
     * 繪制特殊時刻(12點歌殃、3點乔妈、6點、9點)的文字
     */
    private void drawText(Canvas canvas) {

        setTextPaint();

        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();

        // 文字頂部與基線距離
        float ascent=Math.abs(fontMetrics.ascent);
        // 文字底部與基線距離
        float descent=Math.abs(fontMetrics.descent);
        // 文字高度
        float fontHeight = ascent+descent;
        // 文字豎直中心點距離基線的距離氓皱;
        float offsetY = fontHeight / 2 - Math.abs(fontMetrics.descent);
        // 文字寬度
        float fontWidth;

        // drawText(@NonNull String text, float x, float y, @NonNull Paint paint) 參數:y路召,為基線的y坐標,并非文字左下角的坐標
        // 文字距離圓圈的距離為 特殊刻度長度+寬度

        String h = "12";
        // y軸坐標為: -(半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字頂部距離基線的距離)
        float y=-(mRadius-mParticularlyScaleLength-mParticularlyScaleWidth-ascent);
        canvas.drawText(h, 0,y, mTextPaint);

        h = "3";
        fontWidth = mTextPaint.measureText(h);
        // y軸坐標為: 半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字長度/2(繪制原點在文字橫向中心)
        y=mRadius - mParticularlyScaleLength-mParticularlyScaleWidth - (fontWidth / 2);
        canvas.drawText(h,y, 0 + offsetY, mTextPaint);

        h = "6";
        // y軸坐標為: 半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字底部與基線的距離
        y=mRadius - mParticularlyScaleLength -mParticularlyScaleWidth-descent;
        canvas.drawText(h, 0,y, mTextPaint);

        h = "9";
        fontWidth = mTextPaint.measureText(h);
        // y軸坐標為: -(半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字長度/2(繪制原點在文字橫向中心))
        y= -(mRadius - mParticularlyScaleLength -mParticularlyScaleWidth -(fontWidth/2));
        canvas.drawText(h,y, 0 + offsetY, mTextPaint);
    }

    private void setTextPaint(){
        mTextPaint.setStrokeWidth(mDefaultScaleWidth / 2);
        mTextPaint.setTextSize(mParticularlyScaleWidth * 4);
        // 文字繪制中心點移動到橫向中心
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

這里只繪制了四個時間的文字波材,但是這四個數字坐標規(guī)則都不相同股淡,我們需要分開繪制;

首先,我們了解一下文字繪制方法:

canvas. drawText(String text, float x, float y,Paint paint)

這里的x和y就是文字繪制的坐標各聘,如圖:(圖片來自http://www.gcssloop.com/customview/Canvas_PictureText

image

這里看上去揣非,x和y像是文字左下角的坐標,可是躲因,這里其實是文字的基線(BaseLine )起點的的坐標早敬,什么是基線呢?

我們先了解Paint.FontMetrics 類大脉,他是用來描述文字的上下高度的類搞监,包含屬性如下:


FontMetrics類

圖解(來源見水印):


FontMetrics
FontMetrics

對照著這兩個圖镰矿,再看代碼的時候琐驴,應該就懂了。

繪制指針:

/**
     * 繪制指針
     */
    private void drawPointer(Canvas canvas) {

        drawHourPointer(canvas);
        drawMinutePointer(canvas);
        drawSecondPointer(canvas);

        mPointerPaint.setColor(mClockColor);
        // 繪制中心原點秤标,需要在指針繪制完成后才能繪制
        canvas.drawCircle(0, 0, mPointRadius, mPointerPaint);
    }

首先繪制時針绝淡,我們可以聯(lián)想一下,當現在時間是3:30的時候苍姜,時針應該在那個位置呢牢酵?指在3上嗎?不是的衙猪,因為現在已經是45分了馍乙,時針應該在3到4之間,在二分之一的位置垫释,更精確一點的話丝格,我們還要考慮秒針的狀態(tài)。那么棵譬,我們就可以通過當前時間显蝌,算出指針與x軸的角度,從而通過三角函數订咸,計算出指針的目標坐標值:

/**
     * 繪制時針
     */
    private void drawHourPointer(Canvas canvas) {

        mPointerPaint.setStrokeWidth(mHourPointerWidth);
        mPointerPaint.setColor(mColorHourPointer);

        // 當前時間的總秒數
        float s = mH * 60 * 60 + mM * 60 + mS;
        // 百分比
        float percentage = s / (12 * 60 * 60);
        // 通過角度計算弧度值曼尊,因為時鐘的角度起線是y軸負方向扭屁,而View角度的起線是x軸正方向,所以要加270度
        float angle = 270 + 360 * percentage;

        float x = (float) (mHourPointerLength * Math.cos(Math.PI * 2 / 360 * angle));
        float y = (float) (mHourPointerLength * Math.sin(Math.PI * 2 / 360 * angle));

        canvas.drawLine(0, 0, x, y, mPointerPaint);
    }

    /**
     * 繪制分針
     */
    private void drawMinutePointer(Canvas canvas) {

        mPointerPaint.setStrokeWidth(mMinutePointerWidth);
        mPointerPaint.setColor(mColorMinutePointer);

        float s = mM * 60 + mS;
        float percentage = s / (60 * 60);
        float angle = 270 + 360 * percentage;

        float x = (float) (mMinutePointerLength * Math.cos(Math.PI * 2 / 360 * angle));
        float y = (float) (mMinutePointerLength * Math.sin(Math.PI * 2 / 360 * angle));

        canvas.drawLine(0, 0, x, y, mPointerPaint);
    }

    /**
     * 繪制秒針
     */
    private void drawSecondPointer(Canvas canvas) {

        mPointerPaint.setStrokeWidth(mSecondPointerWidth);
        mPointerPaint.setColor(mColorSecondPointer);

        float s = mS;
        float percentage = s / 60;
        float angle = 270 + 360 * percentage;

        float x = (float) (mSecondPointerLength * Math.cos(Math.PI * 2 / 360 * angle));
        float y = (float) (mSecondPointerLength * Math.sin(Math.PI * 2 / 360 * angle));

        canvas.drawLine(0, 0, x, y, mPointerPaint);
    }

到這里涩禀,自定義View的繪制部分已完成,下面就是屬于一些自定義View中經常用到的操作然眼;

其他操作

開啟計時

當然艾船,時鐘是動態(tài)的,每一秒都在變化高每,我們需要在線程中不斷刷新當前View:

/**
     * 開始計時
     */
    private void startTime() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    getTime();
                }
            }
        }).start();
    }

    /**
     * 獲取當前系統(tǒng)時間
     */
    private void getTime() {
        Calendar calendar = Calendar.getInstance();
        int hour = calendar.get(Calendar.HOUR);
        hour = hour % 12;
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);

        if (hour != mH || minute != mM || second != mS) {
            setTime(hour, minute, second);
            postInvalidate();
        }
    }

    /**
     * 設置時間
     */
    private void setTime(int h, int m, int s) {
        mH = h;
        mM = m;
        mS = s;
    }

startTime()方法需要在構造方法中調用屿岂;

適配不同大小:

我們首先在onMeasure(int widthMeasureSpec, int heightMeasureSpec)中鲸匿,對尺寸進行測量爷怀,并當wrap_content模式下,默認尺寸為48dp带欢。

這部分代碼基本每個自定義View中都會用到运授,比較通用,也比較常見乔煞,代碼就不貼了吁朦,可以clone源碼看一下。

而對于時鐘的尺寸屬性渡贾,我們可以通過與半徑的比例計算逗宜,這樣就比較好的適配了不同尺寸下的時鐘大小:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mCenterX = w / 2;
        mCenterY = h / 2;
        mRadius = (float) (w / 2 * 0.8);

        initClockPointerLength();
    }

    /**
     * 根據控件的大小空骚,初始化時鐘刻度的長度和寬度纺讲、指針的長度和寬度、時鐘中心點的半徑
     */
    private void initClockPointerLength() {

        /*
        * 默認時鐘刻度長=半徑/10;
        * 默認時鐘刻度寬=長/6;
        *
        * */
        mDefaultScaleLength = mRadius / 10;
        mDefaultScaleWidth = mDefaultScaleLength / 6;

        /*
        * 特殊時鐘刻度長=半徑/5;
        * 特殊時鐘刻度寬=長/6;
        *
        * */
        mParticularlyScaleLength = mRadius / 5;
        mParticularlyScaleWidth = mParticularlyScaleLength / 6;

        /*
        * 時針長=半徑/3;
        * 時針寬=特殊時鐘刻度寬;
        *
        * */
        mHourPointerLength = mRadius / 3;
        mHourPointerWidth = mParticularlyScaleWidth;

         /*
        * 分針長=半徑/2;
        * 分針寬=特殊時鐘刻度寬;
        *
        * */
        mMinutePointerLength = mRadius / 2;
        mMinutePointerWidth = mParticularlyScaleWidth;

        /*
        * 秒針長=半徑/3*2;
        * 秒針寬=默認時鐘刻度寬;
        *
        * */
        mSecondPointerLength = mRadius / 3 * 2;
        mSecondPointerWidth = mDefaultScaleWidth;

        // 中心點半徑=(默認刻度寬+特殊刻度寬)/2
        mPointRadius = (mDefaultScaleWidth + mParticularlyScaleWidth) / 2;
    }

自定義屬性值

attr.xml:

<declare-styleable name="ClockView">
        <attr name="clockColor" format="color"/>
        <attr name="defaultScaleColor" format="color"/>
        <attr name="particularlyScaleColor" format="color"/>
        <attr name="hourPointerColor" format="color"/>
        <attr name="minutePointerColor" format="color"/>
        <attr name="secondPointerColor" format="color"/>
    </declare-styleable>

對應:

// 時鐘顏色囤屹、默認刻度顏色熬甚、時刻度顏色、時針顏色牺丙、分針顏色则涯、秒針顏色
    private int mClockColor,mColorDefaultScale,mColorParticularyScale,mColorHourPointer,
            mColorMinutePointer, mColorSecondPointer;

GitHub地址 代碼下載

好了,這是自定義View系列的第一篇博文冲簿,后面會繼續(xù)這方面的博文粟判,共勉!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末峦剔,一起剝皮案震驚了整個濱河市档礁,隨后出現的幾起案子,更是在濱河造成了極大的恐慌吝沫,老刑警劉巖呻澜,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件递礼,死亡現場離奇詭異,居然都是意外死亡羹幸,警方通過查閱死者的電腦和手機脊髓,發(fā)現死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來栅受,“玉大人将硝,你說我怎么就攤上這事∑聊鳎” “怎么了依疼?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長而芥。 經常有香客問我律罢,道長,這世上最難降的妖魔是什么棍丐? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任误辑,我火速辦了婚禮,結果婚禮上歌逢,老公的妹妹穿的比我還像新娘稀余。我一直安慰自己,他們只是感情好趋翻,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布睛琳。 她就那樣靜靜地躺著,像睡著了一般踏烙。 火紅的嫁衣襯著肌膚如雪师骗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天讨惩,我揣著相機與錄音辟癌,去河邊找鬼。 笑死荐捻,一個胖子當著我的面吹牛黍少,可吹牛的內容都是我干的。 我是一名探鬼主播处面,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼厂置,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了魂角?” 一聲冷哼從身側響起昵济,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后访忿,有當地人在樹林里發(fā)現了一具尸體瞧栗,經...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年海铆,在試婚紗的時候發(fā)現自己被綠了迹恐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡卧斟,死狀恐怖系草,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情唆涝,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布唇辨,位于F島的核電站廊酣,受9級特大地震影響,放射性物質發(fā)生泄漏赏枚。R本人自食惡果不足惜亡驰,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饿幅。 院中可真熱鬧凡辱,春花似錦、人聲如沸栗恩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磕秤。三九已至乳乌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間市咆,已是汗流浹背汉操。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蒙兰,地道東北人磷瘤。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像搜变,于是被迫代替她去往敵國和親采缚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容