Android自定義ClockView實(shí)現(xiàn)時(shí)鐘效果

目錄

  • 1.效果圖
  • 2.分析
  • 3.SurfaceView注意事項(xiàng)
  • 4.實(shí)戰(zhàn)
  • 5.總結(jié)
  • 6.Tips

1.廢話不多說先上圖




錄制的可能略卡,三次時(shí)間圖片是因?yàn)槲曳謩e點(diǎn)了下面三個(gè)按鈕的效果妓羊,并非bug


2.分析

  • 觀看上圖咱們需要繪制的有:

    • 時(shí)針
    • 分針
    • 秒針
    • 時(shí)刻度
    • 秒刻度
    • 儀表盤上的數(shù)字
    • 上下午區(qū)分標(biāo)識(shí)(AM/PM)
  • View與Surface的取舍:

    • View一般用于繪制靜態(tài)頁面或者界面元素跟隨用戶的操作(點(diǎn)擊史翘、拖拽等)而被動(dòng)的改變位置铝穷、大小等
    • SurfaceView一般用于無需用戶操作,界面元素就需要不斷的刷新的情況(例如打飛機(jī)游戲不斷移動(dòng)的背景)
    • 通過以上兩條可以確定SurfaceView正好符合我們的需求

3.SurfaceView注意事項(xiàng)

  • 如何使用SurfaceView

    • 1.繼承SurfaceView
    • 2.實(shí)現(xiàn)SurfaceHolder.Callback接口
      • surfaceCreated:Surface創(chuàng)建后調(diào)用临燃,一般做一些初始化工作
      • surfaceChanged:Surface狀態(tài)發(fā)生變化時(shí)調(diào)用(例如大小)
      • surfaceDestroyed:Surface銷毀時(shí)調(diào)用,一般在這里結(jié)束繪制線程
    • 3.SurfaceHolder:控制Surface的類俱病,得到畫布、提交畫布袱结、回調(diào)等
    • 4.繪制和邏輯
  • SurfaceView的寫法

public class MyView extends SurfaceView implements SurfaceHolder.Callback,Runnable {

    private SurfaceHolder mHolder;

    public MyView(Context context) {
        this(context, null);
    }

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

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

    @Override
    public void run() {
        while (true) {
            logic();
            draw();
        }
    }

    /**
     * 邏輯操作
     */
    private void logic() {

    }

    /**
     * 繪制操作
     */
    private void draw() {

    }
}

這是一般性寫法亮隙,不過有些人應(yīng)該已經(jīng)發(fā)現(xiàn)了我們線程里跑的是一個(gè)while(true)的無限死循化,因此往往我們會(huì)增加一個(gè)標(biāo)識(shí)位在surface銷毀時(shí)置false垢夹。修改后部分代碼如下:

private boolean flag;

@Override
public void surfaceCreated(SurfaceHolder holder) {
    flag = true;
    mThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        logic();
        draw();
    }
}

當(dāng)然了這樣還是有缺陷的溢吻,因?yàn)楫?dāng)flag==true時(shí),mThread里面的邏輯操作和繪制操作就在無限運(yùn)行了棚饵∶喝梗可想而知,如果是那樣的話那么我們這里的時(shí)鐘指針在你眼前飛速的轉(zhuǎn)動(dòng)噪漾,由于gif錄制工具不夠強(qiáng)大錄下的動(dòng)圖根本開不出指針飛速旋轉(zhuǎn)的效果這里就不提供圖了硼砰,有興趣的同學(xué)可以自己試一下,也可以腦補(bǔ)欣硼。题翰。。诈胜。豹障。。焦匈。血公。。缓熟。累魔。
因此我們就需要對(duì)線程加以限制摔笤,具體如下:

@Override
public void run() {
    long start, end;
    while (flag) {
        start = System.currentTimeMillis();
        draw();
        logic();
        end = System.currentTimeMillis();

        try {
            if (end - start < 1000) {
                Thread.sleep(1000 - (end - start));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

這里的1000指的是每隔1S刷新一次界面,因?yàn)槲覀兊氖冀K的最小單位是秒


4.廢話啰嗦完了 咱們開始實(shí)戰(zhàn)

  • step1:通過2的分析垦写,定義需要的屬性
// 默認(rèn)半徑
private static final int DEFAULT_RADIUS = 200;

private SurfaceHolder mHolder;
private Canvas mCanvas;
private Thread mThread;
private boolean flag;

// 圓和刻度的畫筆
private Paint mPaint;
// 指針畫筆
private Paint mPointerPaint;

// 畫布的寬高
private int mCanvasWidth, mCanvasHeight;
// 時(shí)鐘半徑
private int mRadius = DEFAULT_RADIUS;
// 秒針長度
private int mSecondPointerLength;
// 分針長度
private int mMinutePointerLength;
// 時(shí)針長度
private int mHourPointerLength;
// 時(shí)刻度長度
private int mHourDegreeLength;
// 秒刻度
private int mSecondDegreeLength;

// 時(shí)鐘顯示的時(shí)吕世、分、秒
private int mHour, mMinute, mSecond;
  • step2:初始化操作
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
    mMinute = Calendar.getInstance().get(Calendar.MINUTE);
    mSecond = Calendar.getInstance().get(Calendar.SECOND);

    mHolder = getHolder();
    mHolder.addCallback(this);
    mThread = new Thread(this);

    mPaint = new Paint();
    mPointerPaint = new Paint();

    mPaint.setColor(Color.BLACK);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.STROKE);

    mPointerPaint.setColor(Color.BLACK);
    mPointerPaint.setAntiAlias(true);
    mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mPointerPaint.setTextSize(22);
    mPointerPaint.setTextAlign(Paint.Align.CENTER);

    setFocusable(true);
    setFocusableInTouchMode(true);
}
  • step3:測(cè)量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int desiredWidth, desiredHeight;
    if (widthMode == MeasureSpec.EXACTLY) {
        desiredWidth = widthSize;
    } else {
        desiredWidth = mRadius * 2 + getPaddingLeft() + getPaddingRight();
        if (widthMode == MeasureSpec.AT_MOST) {
            desiredWidth = Math.min(widthSize, desiredWidth);
        }
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        desiredHeight = heightSize;
    } else {
        desiredHeight = mRadius * 2 + getPaddingTop() + getPaddingBottom();
        if (heightMode == MeasureSpec.AT_MOST) {
            desiredHeight = Math.min(heightSize, desiredHeight);
        }
    }

    // +4是為了設(shè)置默認(rèn)的2px的內(nèi)邊距梯投,因?yàn)槔L制時(shí)鐘的圓的畫筆設(shè)置的寬度是2px
    setMeasuredDimension(mCanvasWidth = desiredWidth + 4, mCanvasHeight = desiredHeight + 4);

    mRadius = (int) (Math.min(desiredWidth - getPaddingLeft() - getPaddingRight(),
            desiredHeight - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);
    calculateLengths();
}

/**
 * 計(jì)算指針和刻度長度
 */
private void calculateLengths() {
    // 這里我們定義時(shí)刻度長度為半徑的1/7
    mHourDegreeLength = (int) (mRadius * 1.0f / 7);
    // 秒刻度長度為時(shí)刻度長度的一半
    mSecondDegreeLength = (int) (mHourDegreeLength * 1.0f / 2);

    // 時(shí)針長度為半徑一半
    // 指針長度比 hour : minute : second = 1 : 1.25 : 1.5
    mHourPointerLength = (int) (mRadius * 1.0 / 2);
    mMinutePointerLength = (int) (mHourPointerLength * 1.25f);
    mSecondPointerLength = (int) (mHourPointerLength * 1.5f);
}

測(cè)量的前面部分代碼基本都是一個(gè)模式來寫的命辖,需要注意的是當(dāng)測(cè)量模式不是Exectly時(shí)的處理,想要了解這一塊同學(xué)的可以去鴻神的博客學(xué)習(xí)

  • step4:繪制
/**
 * 繪制分蓖,這部分代碼基本固定
 */
private void draw() {
    try {
        mCanvas = mHolder.lockCanvas(); // 得到畫布
        if (mCanvas != null) {
            // 在這里繪制內(nèi)容
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (mCanvas != null) {
            // 提交畫布尔艇,否則什么都看不見
            mHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}

現(xiàn)在開始具體的繪制內(nèi)容(畫什么由畫布決定,怎么畫由畫筆決定咆疗,這也就是我們上面給畫筆設(shè)置一系列屬性的原因):

// 1.將坐標(biāo)系原點(diǎn)移至去除內(nèi)邊距后的畫布中心
// 默認(rèn)在畫布左上角漓帚,這樣做是為了更方便的繪制
mCanvas.translate(mCanvasWidth * 1.0f / 2 + getPaddingLeft() - getPaddingRight(),mCanvasHeight * 1.0f / 2 + getPaddingTop() - getPaddingBottom());
// 2.繪制圓盤
mPaint.setStrokeWidth(2f); // 畫筆設(shè)置2個(gè)像素的寬度
mCanvas.drawCircle(0, 0, mRadius, mPaint); // 到這一步就能知道第一步的好處了,否則害的去計(jì)算園的中心點(diǎn)坐標(biāo)
// 3.繪制時(shí)刻度
for (int i = 0; i < 12; i++) {
    mCanvas.drawLine(0, mRadius, 0, mRadius - mHourDegreeLength, mPaint);
    mCanvas.rotate(30); // 360°平均分成12份午磁,每份30°
}
// 4.繪制秒刻度
mPaint.setStrokeWidth(1.5f);
for (int i = 0; i < 60; i++) {
    //時(shí)刻度繪制過的區(qū)域不在繪制
    if (i % 5 != 0) {
        mCanvas.drawLine(0, mRadius, 0, mRadius - mSecondDegreeLength, mPaint);
    }
    mCanvas.rotate(6); // 360°平均分成60份尝抖,每份6°
}
// 5.繪制數(shù)字
mPointerPaint.setColor(Color.BLACK);
for (int i = 0; i < 12; i++) {
    String number = 6 + i < 12 ? String.valueOf(6 + i) : (6 + i) > 12
            ? String.valueOf(i - 6) : "12";
    mCanvas.drawText(number, 0, mRadius * 5.5f / 7, mPointerPaint);
    mCanvas.rotate(30);
}
// 6.繪制上下午
mCanvas.drawText(mHour < 12 ? "AM" : "PM", 0, mRadius * 1.5f / 4, mPointerPaint);
// 7.繪制時(shí)針
Path path = new Path();
path.moveTo(0, 0);
int[] hourPointerCoordinates = getPointerCoordinates(mHourPointerLength);
path.lineTo(hourPointerCoordinates[0], hourPointerCoordinates[1]);
path.lineTo(hourPointerCoordinates[2], hourPointerCoordinates[3]);
path.lineTo(hourPointerCoordinates[4], hourPointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mHour % 12 * 30 + mMinute * 1.0f / 60 * 30);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();
// 8.繪制分針
path.reset();
path.moveTo(0, 0);
int[] minutePointerCoordinates = getPointerCoordinates(mMinutePointerLength);
path.lineTo(minutePointerCoordinates[0], minutePointerCoordinates[1]);
path.lineTo(minutePointerCoordinates[2], minutePointerCoordinates[3]);
path.lineTo(minutePointerCoordinates[4], minutePointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mMinute * 6);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();
// 9.繪制秒針
mPointerPaint.setColor(Color.RED);
path.reset();
path.moveTo(0, 0);
int[] secondPointerCoordinates = getPointerCoordinates(mSecondPointerLength);
path.lineTo(secondPointerCoordinates[0], secondPointerCoordinates[1]);
path.lineTo(secondPointerCoordinates[2], secondPointerCoordinates[3]);
path.lineTo(secondPointerCoordinates[4], secondPointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mSecond * 6);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();

這里比較難的可能就是指針的繪制,因?yàn)槲覀兊闹羔樖莻€(gè)規(guī)則形狀迅皇,其中g(shù)etPointerCoordinates便是得到這個(gè)不規(guī)則形狀的3個(gè)定點(diǎn)坐標(biāo)昧辽,有興趣的同學(xué)可以去研究一下我的邏輯,也可以定義你自己的邏輯登颓。我的邏輯如下(三角函數(shù)學(xué)的號(hào)的同學(xué)應(yīng)該一眼就能看懂):

/**
 * 獲取指針坐標(biāo)
 *
 * @param pointerLength 指針長度
 * @return int[]{x1,y1,x2,y2,x3,y3}
 */
private int[] getPointerCoordinates(int pointerLength) {
    int y = (int) (pointerLength * 3.0f / 4);
    int x = (int) (y * Math.tan(Math.PI / 180 * 5));
    return new int[]{-x, y, 0, pointerLength, x, y};
}
  • step5:邏輯
    這里邏輯可想而知每個(gè)秒搅荞,秒數(shù)+1到60的時(shí)候歸0,同時(shí)分鐘數(shù)+1框咙,
    分鐘數(shù)到60的時(shí)候歸0咕痛,小時(shí)數(shù)+1,小時(shí)數(shù)到24的時(shí)候歸0.
/**
 * 邏輯
 */
private void logic() {
    mSecond++;
    if (mSecond == 60) {
        mSecond = 0;
        mMinute++;
        if (mMinute == 60) {
            mMinute = 0;
            mHour++;
            if (mHour == 24) {
                mHour = 0;
            }
        }
    }
}
  • step6:裝逼的時(shí)刻到了

運(yùn)行來看一下效果



我靠什么情況為什么畫出來是這么個(gè)東西

這里就是要值得大家注意的了喇嘱,使用surface繪制的時(shí)候一定要刷屏茉贡,所謂的刷屏在每次繪制前畫一個(gè)什么都沒有圖層在上次畫出來的東西上面蓋住再去畫本次實(shí)現(xiàn)也很簡單只需要在繪制的第一行加上一句就行。
我這里是

//刷屏
mCanvas.drawColor(Color.WHITE);

這里的背景也可以通過自定義屬性來自定義的者铜。不知道的同學(xué)仍然可以去鴻神的博客學(xué)習(xí)


5.總結(jié)

主要涉及知識(shí)點(diǎn):

  • View和SurfaceView的取舍
  • SurfaceView的使用
  • Canvas使用
  • 坐標(biāo)系的靈活運(yùn)用

6.Tips

1.圖上的點(diǎn)擊按鈕改變時(shí)間和改變textView上的時(shí)候只是定義了一個(gè)回調(diào)接口腔丧,這里不再贅述
2.詳情見源碼
3.本文同步發(fā)布CSDN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市作烟,隨后出現(xiàn)的幾起案子愉粤,更是在濱河造成了極大的恐慌,老刑警劉巖拿撩,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衣厘,死亡現(xiàn)場離奇詭異,居然都是意外死亡压恒,警方通過查閱死者的電腦和手機(jī)头滔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門怖亭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坤检,你說我怎么就攤上這事∑谙牛” “怎么了早歇?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長讨勤。 經(jīng)常有香客問我箭跳,道長,這世上最難降的妖魔是什么潭千? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任谱姓,我火速辦了婚禮,結(jié)果婚禮上刨晴,老公的妹妹穿的比我還像新娘屉来。我一直安慰自己,他們只是感情好狈癞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布茄靠。 她就那樣靜靜地躺著,像睡著了一般蝶桶。 火紅的嫁衣襯著肌膚如雪慨绳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天真竖,我揣著相機(jī)與錄音脐雪,去河邊找鬼。 笑死恢共,一個(gè)胖子當(dāng)著我的面吹牛战秋,可吹牛的內(nèi)容都是我干的升筏。 我是一名探鬼主播晒杈,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼漠酿!你這毒婦竟也來了拐袜?” 一聲冷哼從身側(cè)響起吉嚣,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蹬铺,沒想到半個(gè)月后尝哆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡甜攀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年秋泄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了琐馆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恒序,死狀恐怖瘦麸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情歧胁,我是刑警寧澤滋饲,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站喊巍,受9級(jí)特大地震影響屠缭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜崭参,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一呵曹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧何暮,春花似錦奄喂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贰军,卻和暖如春玻蝌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背词疼。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工俯树, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贰盗。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓许饿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舵盈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子陋率,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容