Android自定義控件:時(shí)鐘

下面是項(xiàng)目在手機(jī)上運(yùn)行的效果圖

GIF演示圖

效果圖

樣式效果演示圖

效果圖

效果圖

效果圖

實(shí)現(xiàn)原理分析

  • 刻度線繪制:畫一個(gè)刻度線很簡單,就是canvas.drawLine,但是根據(jù)角度每30度繪制一個(gè)刻度線怎么實(shí)現(xiàn)呢,我們一開始想到的可能會(huì)是根據(jù)角度,利用三角函數(shù)等去計(jì)算每個(gè)刻度線的開始坐標(biāo)和結(jié)束坐標(biāo),但這種方式未免過于復(fù)雜,稍有不慎就會(huì)計(jì)算錯(cuò)誤。但是利用畫布的旋轉(zhuǎn)canvas.rotate就會(huì)非常的簡單,刻度線只需按照12點(diǎn)鐘方向繪制即可,每次繪制完一個(gè)刻度線,畫布旋轉(zhuǎn)30度,再按照12點(diǎn)鐘方向繪制即可。
  • 指針繪制:同樣也是通過canvas.drawLine繪制3個(gè)指針,為paint設(shè)置不同的屬性實(shí)現(xiàn)時(shí)針,分針,秒針的顯示樣式,同理,如果我們根據(jù)角度去計(jì)算指針的坐標(biāo),那就很復(fù)雜,這里也是通過畫布的旋轉(zhuǎn),那么旋轉(zhuǎn)的角度怎么確定呢,就是根據(jù)當(dāng)前時(shí)間去確定(具體算法后面代碼中具體分析)。
  • 動(dòng)態(tài):為了實(shí)現(xiàn)時(shí)鐘的動(dòng)態(tài)轉(zhuǎn)動(dòng),我們需要在onDraw中每一秒鐘獲取一次當(dāng)前時(shí)間,然后計(jì)算3個(gè)指針的旋轉(zhuǎn)角度,再繪制就行了。

這樣一分析,其實(shí)自定義時(shí)鐘很簡單,就是繪制圓,然后通過畫布的旋轉(zhuǎn)繪制刻度線和指針。

具體實(shí)現(xiàn)過程

  1. 繪制圓

     //繪制圓
     canvas.drawCircle(centerX, centerY, radius, circlePaint);
    

    其中centerX和centerY為圓心,用當(dāng)前控件的中心點(diǎn)即可,radius為圓的半徑,采用當(dāng)前控件寬高的最小值/2 即可,或者自行設(shè)置。

  2. 繪制刻度線

    12個(gè)刻度線,循環(huán)12次,每3個(gè)刻度線就是一刻鐘的刻度線,可以設(shè)置不同的樣式區(qū)分。然后根據(jù)12點(diǎn)鐘方向繪制刻度線。

    開始x坐標(biāo):圓心x坐標(biāo);

    開始y坐標(biāo):圓心y坐標(biāo)-半徑+間隙;

    結(jié)束x坐標(biāo):圓心x坐標(biāo);

    結(jié)束y坐標(biāo):開始y坐標(biāo)+刻度線長度;

    每繪制完一個(gè)刻度線后,畫布就在之前的基礎(chǔ)上旋轉(zhuǎn)30度,繼續(xù)繪制12點(diǎn)鐘刻度線,這樣,刻度線就基于旋轉(zhuǎn)后的畫布繪制,也就是斜著繪制了刻度線,很方便的實(shí)現(xiàn)了刻度線的繪制。

    這里給出主要的繪制代碼,全部代碼后面貼出

     //刻度線長度
     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. 繪制指針

    繪制時(shí)針,分針,秒針,我們分別用3個(gè)canvas去繪制,最后再將這3個(gè)畫布的bitmap繪制到控件的canvas中,為的是單獨(dú)控制每個(gè)畫布的旋轉(zhuǎn)角度。

    首先分析時(shí)針的指針角度,鐘一圈是12個(gè)小時(shí),360度,那么每小時(shí)就是30度,假設(shè)當(dāng)前時(shí)間的小時(shí)是h(12小時(shí)制),那么時(shí)針的旋轉(zhuǎn)角度就是h*30,同刻度線一樣,我們也不去計(jì)算該角度的指針的各種坐標(biāo),而是直接將時(shí)針的畫布旋轉(zhuǎn)h*30度,然后繪制12點(diǎn)鐘方向的時(shí)針就行了。

    接著是分針角度,鐘一圈是60分鐘,360度,那么每分鐘就是6度,假設(shè)當(dāng)前時(shí)間的分鐘是m,那么分針的旋轉(zhuǎn)角度就是m*6

    最后是秒針角度,鐘一圈是60秒,360度,那么每秒就是6度,假設(shè)當(dāng)前時(shí)間的秒數(shù)是s,那么秒針的旋轉(zhuǎn)角度就是s*6

    分析完了時(shí)針,分針,秒針的角度獲取,那么之后就很簡單了,在onDraw中,我們每過一秒獲取一次當(dāng)前時(shí)間的時(shí)分秒,按照上面的算法計(jì)算角度,然后旋轉(zhuǎn)相應(yīng)的畫布,之后繪制相應(yīng)的指針(當(dāng)然要注意畫布的清空和還原),那么一個(gè)隨著時(shí)間的流逝而旋轉(zhuǎn)的時(shí)鐘就出來了。

    這里給出繪制時(shí)針的主要代碼,其他兩個(gè)指針是類似的,具體代碼后面貼出

     @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);
    
         //保存畫布狀態(tài)
         hourCanvas.save();
         //清空畫布
         hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
         //旋轉(zhuǎn)畫布
         hourCanvas.rotate(hour12 * 30, centerX, centerY);
         //繪制12點(diǎn)鐘方向的時(shí)針
         hourCanvas.drawLine(centerX, centerY,
                 centerX, centerY - hourLineLength, hourPaint);
         //重置畫布狀態(tài),即撤銷之前旋轉(zhuǎn)的角度,回到未旋轉(zhuǎn)之前的狀態(tài)
         hourCanvas.restore();
    
         canvas.drawBitmap(hourBitmap, 0, 0, null); 
    
         //每隔1s重新繪制
         postInvalidateDelayed(1000);
     }
    

    但是我們會(huì)發(fā)現(xiàn)有一點(diǎn)小小的不足,秒針是會(huì)一秒一秒的轉(zhuǎn),但是時(shí)針和分針總是在整數(shù)位置,當(dāng)過了60秒,分針才會(huì)跳到下一分鐘,當(dāng)過了60分鐘,時(shí)針才會(huì)跳到下一個(gè)小時(shí),我們平常看的時(shí)鐘都是隨著秒針的轉(zhuǎn)動(dòng),分針和時(shí)針都是有一定的偏移量的,當(dāng)然我們的時(shí)鐘也要這么炫酷,那么如何計(jì)算呢?

    時(shí)針:前面說過,每小時(shí)時(shí)針旋轉(zhuǎn)30度,假設(shè)當(dāng)前時(shí)間的小時(shí)是h(12小時(shí)制),那么時(shí)針的旋轉(zhuǎn)角度就是h*30。那么每分鐘時(shí)針旋轉(zhuǎn)多少度呢,答案是30/60=0.5度(每小時(shí)60分鐘,每小時(shí)30度),所以時(shí)針的偏移量就是m*0.5,那么假設(shè)當(dāng)前的時(shí)間是1:30,那么時(shí)針旋轉(zhuǎn)的角度就是1*30+30*0.5,就是45度,改成變量公式就是h*30+m*0.5,那么修改下上面的代碼

     hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
    

    分針:假設(shè)當(dāng)前時(shí)間的分鐘是m,那么分針的旋轉(zhuǎn)角度就是m*6,每秒鐘分針旋轉(zhuǎn)6/60(每分鐘60秒,每分鐘6度),所以分針的偏移量是s*0.1,那么分針畫布旋轉(zhuǎn)的的代碼就是

     minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
    

    秒針:秒針就按照每秒鐘6度旋轉(zhuǎn)

     secondCanvas.rotate(second * 6, centerX, centerY);
    

總結(jié)

經(jīng)過上面的3個(gè)步驟,我們就繪制出了一個(gè)會(huì)慢慢移動(dòng)的時(shí)鐘了。

完整的代碼和項(xiàng)目大家可以到我的github中查看,里面有相關(guān)的使用方法,同時(shí)這個(gè)項(xiàng)目上傳到了maven倉庫,可以通過gradle直接使用

compile 'com.don:clockviewlibrary:1.0.1'

github地址:https://github.com/zhijieeeeee/ClockView

完整代碼

public class ClockView extends View {

    //使用wrap_content時(shí)默認(rèn)的尺寸
    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;

    //時(shí)針寬度
    private final static int HOUR_LINE_WIDTH = 10;

    //分針寬度
    private final static int MINUTE_LINE_WIDTH = 6;

    //秒針寬度
    private final static int SECOND_LINE_WIDTH = 4;

    //圓心坐標(biāo)
    private int centerX;
    private int centerY;

    //圓半徑
    private int radius;

    //圓的畫筆
    private Paint circlePaint;

    //刻度線畫筆
    private Paint markPaint;

    //時(shí)針畫筆
    private Paint hourPaint;

    //分針畫筆
    private Paint minutePaint;

    //秒針畫筆
    private Paint secondPaint;

    //時(shí)針長度
    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;
    //時(shí)針的顏色
    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個(gè)指針的圓心
    private boolean isDrawCenterCircle = false;

    //獲取時(shí)間監(jiān)聽
    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;

        //時(shí)針
        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);

        //(方案一)每過一小時(shí)(3600秒)時(shí)針添加30度,所以每秒時(shí)針添加(1/120)度
        //(方案二)每過一小時(shí)(60分鐘)時(shí)針添加30度,所以每分鐘時(shí)針添加(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)//根據(jù)指針的顏色繪制圓心
            hourCanvas.drawCircle(centerX, centerY, 2 * HOUR_LINE_WIDTH, hourPaint);
        hourCanvas.restore();

        //每過一分鐘(60秒)分針添加6度,所以每秒分針添加(1/10)度;當(dāng)minute加1時(shí),正好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)//根據(jù)指針的顏色繪制圓心
            minuteCanvas.drawCircle(centerX, centerY, 2 * MINUTE_LINE_WIDTH, minutePaint);
        minuteCanvas.restore();

        //每過一秒旋轉(zhuǎn)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)//根據(jù)指針的顏色繪制圓心
            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) {
            //小時(shí)采用24小時(shí)制返回
            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);

    }

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

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

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