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閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容