1. 程式人生 > >Android自定義View之時鐘

Android自定義View之時鐘

效果(gif效果不是很好)

GitHub地址

繪製思路

1. 繪製錶盤和長短刻度

2. 繪製各個時間點的數字

3. 繪製時針,分針,秒針

4. 讓各個指標轉起來,根據時間計算角度

開始繪製

1. 定義引數(attrs檔案)

    <declare-styleable name="Clock">
        <!--數值-->
        <attr name="mClockRingWidth" format="dimension"/>
        <attr name="mDefaultWidth" format="dimension"/>
        <attr name="mDefaultLength" format="dimension"/>
        <attr name="mSpecialWidth" format="dimension"/>
        <attr name="mSpecialLength" format="dimension"/>
        <attr name="mHWidth" format="dimension"/>
        <attr name="mMWidth" format="dimension"/>
        <attr name="mSWidth" format="dimension"/>
        <!--顏色-->
        <attr name="mCircleColor" format="color"/>
        <attr name="mHColor" format="color"/>
        <attr name="mMColor" format="color"/>
        <attr name="mSColor" format="color"/>
        <attr name="mNumColor" format="color"/>

    </declare-styleable>

2. 繼承View,定義引數

//    圓形和刻度的畫筆、指標的畫筆、數字的畫筆
    private Paint mCirclePaint,mPointerPaint,mNumPaint;

//    時鐘的外環寬度、時鐘的半徑、預設刻度的寬度、預設刻度的長度
//    特殊刻度的寬度、特殊刻度的長度、時針的寬度、分針的寬度、秒針的寬度
    private float mClockRingWidth,mClockRadius,mDefaultWidth,mDefaultLength,
                mSpecialWidth,mSpecialLength,mHWidth,mMWidth,mSWidth;

//    圓形和刻度的顏色,時針的顏色,分針的顏色,秒針的顏色,數字的顏色
    private int mCircleColor,mHColor,mMColor,mSColor,mNumColor;

3. 初始化引數以及畫筆

    public Clock(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
        initPaint();
        Calendar mCalendar= Calendar.getInstance();
        //獲取當前小時數
        int hours = mCalendar.get(Calendar.HOUR);
        //獲取當前分鐘數
        int minutes = mCalendar.get(Calendar.MINUTE);
        //獲取當前秒數
        int seconds=mCalendar.get(Calendar.SECOND);
        setTime(hours,minutes,seconds);
        //開啟定時
        start();
    }

    /**
     * 初始化自定義引數
     */
    private void init(Context context,AttributeSet attributeSet){
        TypedArray ta = context.obtainStyledAttributes(attributeSet, R.styleable.Clock);
        mClockRingWidth=ta.getDimension(R.styleable.Clock_mClockRingWidth,SizeUtils.dp2px(context,4));
        mDefaultWidth=ta.getDimension(R.styleable.Clock_mDefaultWidth,SizeUtils.dp2px(context,1));
        mDefaultLength=ta.getDimension(R.styleable.Clock_mDefaultLength,SizeUtils.dp2px(context,8));
        mSpecialWidth=ta.getDimension(R.styleable.Clock_mSpecialWidth,SizeUtils.dp2px(context,2));
        mSpecialLength=ta.getDimension(R.styleable.Clock_mSpecialLength,SizeUtils.dp2px(context,14));
        mHWidth=ta.getDimension(R.styleable.Clock_mHWidth,SizeUtils.dp2px(context,6));
        mMWidth=ta.getDimension(R.styleable.Clock_mMWidth,SizeUtils.dp2px(context,4));
        mSWidth=ta.getDimension(R.styleable.Clock_mSWidth,SizeUtils.dp2px(context,2));
        //顏色
        mCircleColor=ta.getColor(R.styleable.Clock_mCircleColor, Color.RED);
        mHColor=ta.getColor(R.styleable.Clock_mHColor, Color.BLACK);
        mMColor=ta.getColor(R.styleable.Clock_mMColor, Color.BLACK);
        mSColor=ta.getColor(R.styleable.Clock_mSColor, Color.RED);
        mNumColor=ta.getColor(R.styleable.Clock_mNumColor, Color.BLACK);
        //記得釋放
        ta.recycle();

    }

    /**
     * 初始化畫筆
     */
    private void initPaint() {
        //時鐘的畫筆
        mCirclePaint=new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.STROKE);
        //指標的畫筆
        mPointerPaint=new Paint();
        mPointerPaint.setAntiAlias(true);
        mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPointerPaint.setStrokeCap(Paint.Cap.ROUND);
        //數字的畫筆
        mNumPaint=new Paint();
        mNumPaint.setStyle(Paint.Style.FILL);
        mNumPaint.setTextSize(60);
        mNumPaint.setColor(mNumColor);
    }

4. 測量View並取值

在這裡本想著使用 wrap_content 模式的時候拋異常提示一下呢,後來想了想沒有做,因為時鐘這個控制元件必須要給定一個數值的,哪怕是 match_parent 也行,等View測量完畢在 onSizeChanged 方法中取到view的實際寬高,然後進行一個包括半徑的一些引數初始化。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getMeasureSize(true, widthMeasureSpec);
        int height = getMeasureSize(false, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    @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;
        mClockRadius= (float) ((float) (w/2)*0.8);
    }

    /**
     * 獲取View尺寸
     *
     * @param isWidth 是否是width,不是的話,是height
     */
    private int getMeasureSize(boolean isWidth, int measureSpec) {

        int result = 0;

        int specSize = MeasureSpec.getSize(measureSpec);
        int specMode = MeasureSpec.getMode(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                if (isWidth) {
                    result = getSuggestedMinimumWidth();
                } else {
                    result = getSuggestedMinimumHeight();
                }
                break;
            case MeasureSpec.AT_MOST:
                if (isWidth)
                    result = Math.min(specSize, mWidth);
                else
                    result = Math.min(specSize, mHeight);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

5.  onDraw 方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //先設定畫布為view的中心點
        canvas.translate(mCenterX,mCenterY);
        //繪製外圓和刻度
        drawCircle(canvas);
        //繪製數字
        drawNums(canvas);
        //繪製時針,分針,秒針和中間小圓點
        drawPointer(canvas);
    }
  • 5.1  繪製外圓和刻度(分特殊刻度和普通刻度,顏色分別設定)

    /**
     * 繪製外圓和刻度
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        mCirclePaint.setStrokeWidth(mClockRingWidth);
        mCirclePaint.setColor(mCircleColor);
        //先畫出圓環
        canvas.drawCircle(0,0,mClockRadius,mCirclePaint);
        for (int i = 0; i < 60; i++) {
            if (i%5==0){//特殊的刻度
                mCirclePaint.setStrokeWidth(mSpecialWidth);
                mCirclePaint.setColor(mHColor);
                canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mSpecialLength,mCirclePaint);
            }else {//普通刻度
                mCirclePaint.setStrokeWidth(mDefaultWidth);
                mCirclePaint.setColor(mSColor);
                canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mDefaultLength,mCirclePaint);
            }
//            通過旋轉畫布的方式快速設定刻度
            canvas.rotate(6);
        }
    }

   此時的效果

    

  • 5.2 繪製數字

有了繪製刻度的經驗,我們繪製數字也可以這麼幹,對,就是旋轉畫布,省時省力,不用計算每一個數字的位置,你會發現每個數字是從左上角開始繪製的,與我們想要的效果有些偏差,所以我們可以在文字外面包一個矩形,然後我們擺正矩形的位置即可,詳情請看程式碼。(如有好的建議煩請告知)

從12點鐘方向開始,每次旋轉畫布30度,繪製數字,繪製完之後你會發現有的數字是倒著的,這裡需要我們特殊處理一下,詳細請看程式碼。

    /**
     * 繪製文字
     * @param canvas
     */
    private void drawNums(Canvas canvas) {
        for (int i = 0; i < 12; i++) {
            canvas.save();
            if (i==0){ //繪製12點的數字
                Rect textBound = new Rect();
                canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
                String text="12";
                mNumPaint.getTextBounds(text, 0, text.length(), textBound);
                canvas.drawText(text, -textBound.width()/2,
                        textBound.height() / 2, mNumPaint);
            }else { //繪製其他數字
                Rect textBound = new Rect();
                canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
                String text=i+"";
                mNumPaint.getTextBounds(text, 0, text.length(), textBound);
                canvas.rotate(-i*30); //因畫布被旋轉了,所以要把畫布正過來再繪製數字
                canvas.drawText(text, -textBound.width()/2,
                        textBound.height() / 2, mNumPaint);
            }
            canvas.restore();
            canvas.rotate(30);
        }
    }

  此時的效果

   

  • 5.3 繪製時針和分針和秒針

在這裡我們只繪製時針分針和秒針,暫時不根據時間進行弧度計算,後期在啟動定時器的時候會進行計算的,因為我們要實現手動設定時間,所以不可在繪製的時候把弧度計算死值,我們要動態更改。

    /**
     * 繪製指標,每次繪製完恢復畫布狀態,使用 save 和 restore 方法
     * 指標長短根據半徑長度進行計算
     * @param canvas
     */
    private void drawPointer(Canvas canvas) {
        //時針
        canvas.save();
        mPointerPaint.setColor(mHColor);
        mPointerPaint.setStrokeWidth(mHWidth);
        canvas.rotate(mH, 0, 0);
        canvas.drawLine(0, -20, 0,
                (float) (mClockRadius*0.45), mPointerPaint);
        canvas.restore();

//        分針
        canvas.save();
        mPointerPaint.setColor(mMColor);
        mPointerPaint.setStrokeWidth(mMWidth);
        canvas.rotate(mM, 0, 0);
        canvas.drawLine(0, -20, 0,
                (float) (mClockRadius*0.6), mPointerPaint);
        canvas.restore();

        //秒針
        canvas.save();
        mPointerPaint.setColor(mSColor);
        mPointerPaint.setStrokeWidth(mSWidth);
        canvas.rotate(mS, 0, 0);
        canvas.drawLine(0, -40, 0,
                (float) (mClockRadius*0.75), mPointerPaint);
        canvas.restore();
        //最後繪製一個小圓點,要不然沒效果
        mPointerPaint.setColor(mSColor);
        canvas.drawCircle(0,0,mHWidth/2,mPointerPaint);

    }

 此時的效果

6. 根據時間計算指標弧度,讓指標動起來

我們知道秒針走一圈是60秒,而圓的一圈是360度,那麼

秒針一秒鐘旋轉:360 / 60 = 6度

分針一秒鐘旋轉:360 / 60 / 60 = 0.1 度

時針一秒鐘旋轉:360 / (12*3600) = 1 / 120 = 0.0083度

    /**
     * 定時器
     */
    private Timer mTimer=new Timer();
    private TimerTask task = new TimerTask() {
        @Override
        public void run() {
            if (mS == 360) {
                mS = 0;
            }
            if (mM == 360){
                mM = 0;
            }
            if (mH == 360){
                mH = 0;
            }
            //具體計算
            mS = mS + 6;
            mM = mM + 0.1f;
            mH = mH + 1.0f/120;
            //子執行緒用postInvalidate
            postInvalidate();
        }
    };
  • 6.1 開啟定時後時鐘就動起來了

    /**
     *開啟定時器
     */
    public void start() {
        mTimer.schedule(task,0,1000);
    }

7. 開始處理手動設定時間

(注:在此引用了 蛇髮女妖 的簡書裡的解決方案,裡面說的非常好)

在這裡的問題就是分針在30分的時候,時鐘卻還是在1點整,秒針都走了30多秒了,分針確還停在30分鐘的位置上。

如圖: 

蛇髮女妖 的簡書裡的原話是這麼說的:

我們知道30分30秒其實就是30.5分鐘,而我們計算時僅僅只算了30分鐘的角度,少了那0.5分鐘。所以我們還是得把傳入的秒轉換為分鐘,即 分鐘= (分鐘 + 秒 * 1.0f/60f) *6f;同理時針的角度和分針秒針都有關,我們得把傳入的分和秒也都轉換為小時再計算它的角度,即 小時 = (小時 + 分鐘 * 1.0f/60f + 秒 * 1.0f/3600f)*30f;

8. 手動設定時間的最終解決方案

    public void setTime(int h, int m, int s) {
        if (h >= 24 || h < 0 || m >= 60 || m < 0 || s >= 60 || s < 0) {
            Toast.makeText(getContext(), "時間不正確", Toast.LENGTH_SHORT).show();
            return;
        }
        //需要以12點為準,所以統一減去180度
        if (h >= 12) {
            mH = (h + m * 1.0f/60f + s * 1.0f/3600f - 12)*30f-180;
        } else {
            mH = (h + m * 1.0f/60f + s * 1.0f/3600f)*30f-180;
        }
        mM = (m + s * 1.0f/60f) *6f-180;
        mS = s * 6f-180;
    }

9. 總結

自定義View總能讓我們或多或少學到點東西,多動手多實踐,共同進步!

祝:工作順利!