自定義 Android 鐘表盤,這一篇就夠了

水中魚之1999發表於2019-06-21

關於本文:本文原先在我的 CSDN 部落格釋出(由圖片水印能發現),整理以往部落格過程中,發現當時總結的很仔細,所以將其遷移到這裡,希望對大家在自定義 View 方面,能有所幫助 ?

引言

Android 自定義 View 應用非常廣泛,最近逛 github 是偶然發現一個 Demo 感覺寫的很好,我結合著這個專案的內容,給大家講講如何繪製時鐘錶盤,也算是加深下自己對自定義 View 的理解,涉及內容比較多,大家慢慢吸收。


最後效果:

開始之前,先讓大家看看最後的效果

在這裡插入圖片描述


現在開始

讓我們先搭建這個 View

  1. 首先,我們定義一個叫做 ClockView 的自定義 View ,讓它繼承自 View 類。
  2. 然後在 /res/values 目錄下,建立 attrs 檔案,在裡面定義一些屬性 大致如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ClockView">
        <attr name="clock_backgroundColor" format="color" />
        <attr name="clock_lightColor" format="color" />
        <attr name="clock_darkColor" format="color" />
        <attr name="clock_textSize" format="dimension" />
    </declare-styleable>

</resources>

繪製外圍小時圓環的準備工作

小時圓環組成分為外圍的圓弧和四個小時數字,所以我們需要的東西很明確了。

  • 我們首先需要一個 Paint 物件,用於繪製文字,
  • 還需要另一個 Paint 物件,用於繪製圓環。

重寫構造方法:

    /* 暗色,圓弧、刻度線、時針、漸變起始色 */
    private int mDarkColor;
    /* 小時文字字型大小 */
    private float mTextSize;
    private Paint mTextPaint;
    private Paint mCirclePaint;

    public ClockView(Context context) {
        super(context);
    }

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
        mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
        mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
        ta.recycle();
        // ANTI_ALIAS_FLAG 平滑繪製 不帶磕磕絆絆
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(mDarkColor);
        // 居中繪製文字
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setTextSize(mTextSize);

        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setColor(mDarkColor);
        // 官方:使用此樣式繪製的幾何和文字將被描邊,尊重繪畫上與筆劃相關的欄位。
        // 說白了就是,不要吧這塊扇形都上色,只是把最外層的邊描下
        mCirclePaint.setStyle(Paint.Style.STROKE);
        mCirclePaint.setStrokeWidth(mCircleStrokeWidth);// 描邊寬度

    }

別忘了重寫 onMeasure 方法,測量控制元件大小
關於具體的測量方法,請參考自定義 View 的文章,無非就是對 MeasureSpec 的三種 mode 型別進行分類處理罷了。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureResult(widthMeasureSpec), getMeasureResult(heightMeasureSpec));
    }

    private int getMeasureResult(int measureSpec){
        int defaultSize = 800;
        int size = MeasureSpec.getSize(measureSpec);
        int mode = MeasureSpec.getMode(measureSpec);
        switch (mode){
            case MeasureSpec.UNSPECIFIED:
                return defaultSize;
            case MeasureSpec.AT_MOST:
                return Math.max(defaultSize, size);
            case MeasureSpec.EXACTLY:
                return size;
            default:
                return defaultSize;
        }
    }

開始繪製外圍圓環

我們知道,對於繪製圓與橢圓這類圖形,經常需要先用 RectF 設定一個邊界矩形再進行繪製。如果是繪製文字則是 Rect 。

所以繪製外圍圓環,首先要定義一個 RectF 變數用於繪製圓環,在定義一個 Rect 變數,用於繪製文字。

注 mCanvas 繪圖類是 onDraw 中的引數,我們在 onDraw 中將它儲存起來

   // 測量文字大小
    private Rect mTextRect = new Rect();
    private RectF mCircleRectF = new RectF();
    /* 小時圓圈線條寬度 */
    private float mCircleStrokeWidth = 4;

    /**
     * 畫最外圈的時間 12、3、6、9 文字和4段弧線
     */
    private void drawOutSideArc() {
        String[] timeList = new String[]{"12", "3", "6", "9"};
        //計算數字的高度
        mTextPaint.getTextBounds(timeList[0], 0, timeList[0].length(), mTextRect);// 計算後放回一個矩形存在 mTextRect (涉及c++原生方法,會用就行不要深究)
        mCircleRectF.set(mTextRect.width() / 2 + mCircleStrokeWidth / 2,// 畫一個外界小矩形,在矩形裡畫圓
                mTextRect.height() / 2 + mCircleStrokeWidth / 2,
                getWidth() - mTextRect.width() / 2 - mCircleStrokeWidth / 2,
                getHeight() - mTextRect.height() / 2 - mCircleStrokeWidth / 2);
        mCanvas.drawText(timeList[0], getWidth() / 2, mCircleRectF.top + mTextRect.height() / 2, mTextPaint);// 定點寫字,通過 RectF 取得邊界值,由於是頂點在右上方寫字,所以要向下平移
        mCanvas.drawText(timeList[1], mCircleRectF.right, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
        mCanvas.drawText(timeList[2], getWidth() / 2, mCircleRectF.bottom + mTextRect.height() / 2, mTextPaint);
        mCanvas.drawText(timeList[3], mCircleRectF.left, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
        //畫連線數字的4段弧線
        for (int i = 0; i < 4; i++) {
            // 畫四個弧線 sweepAngle 弧線角度(扇形角度)
            mCanvas.drawArc(mCircleRectF, 5 + 90 * i, 80, false, mCirclePaint);
        }
    }

接著,我們重寫 onDraw() 方法,並在 onDraw() 方法中,呼叫上面這個方法繪製圓環

    private Canvas mCanvas;
    @Override
    protected void onDraw(Canvas canvas) {
        mCanvas = canvas;
        drawOutSideArc();
    }

執行一下看看效果

我們看到 圓環和時間是出來了,但是這麼是個橢圓呢,在仔細檢查下我們的程式碼,在繪製過程中,控制我們圓環的 mCircleRectF 物件,是以整個控制元件大小為邊界的,所以原因就很明瞭了,那麼我們只要將 mCircleRectF 物件設定成一個正方形就行。
在這裡插入圖片描述
------------------------

重寫 onSizeChanged() 方法,保證繪製的是圓

包正繪圖是圓形的前提是:

  1. 保證 RectF 切割的是正方形
  2. 那麼保證 RextF 圍成的是正方形,就要需要知道正方形四邊距離控制元件邊界的距離
  3. 也就是我們需要計算四個整型變數 :1.mPaddingLeft | 2.mPaddingTop | 3.mPaddingRight |
    4.mPaddingBottom
    private float mRadius;
    /* 加一個預設的padding值,為了防止用camera旋轉時鐘時造成四周超出view大小 */
    private float mDefaultPadding;
    private float mPaddingLeft;
    private float mPaddingTop;
    private float mPaddingRight;
    private float mPaddingBottom;// 以上4值 均在 onSizechanged()中測量
    
    @Override
    protected void onSizeChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        mRadius = Math.min(l - getPaddingLeft() - getPaddingRight(),
                t - getPaddingTop() - getPaddingBottom()) / 2;// 各個指標長度
        mDefaultPadding = 0.12f * mRadius;
        mPaddingLeft = mDefaultPadding + l / 2 - mRadius + getPaddingLeft();// 鍾離左邊界距離
        mPaddingRight = mDefaultPadding + l / 2 - mRadius + getPaddingRight();// 鍾離右邊界距離
        mPaddingTop = mDefaultPadding + t / 2 - mRadius + getPaddingTop();// 鍾離上邊界距離
        mPaddingBottom = mDefaultPadding + t / 2 - mRadius + getPaddingBottom();// 鍾離下邊界距離
    }

對於圓的半徑 mRadius ,我們就取控制元件長和寬中,短的那個的一半為它的值,除此之外還有一種情況,如果控制元件設定了 padding 那麼,如果知識取長寬中短的,那麼無論 padding 的值怎麼設定,控制元件的半徑始終都是保持長寬中短的那邊的一半不變,這樣取值使得 padding 失去了作用,也就顯得不那麼人性化了,所以真正的半徑應該是長寬中短的那邊,再減去兩個 padding 的值,如下:

mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()) / 2;

那麼這個 mDefaultPadding 又是什麼作用呢?不如我們將其山區看看效果:

在這裡插入圖片描述
試想一下如果我們,沒有這個預設值,那麼使用者在沒有設定 padding 時,畫出的圓弧必然和 View 的邊界相切,圓弧相切到嗨沒啥,關鍵是圓弧上顯示時間的文字也得給截去了一半,但有了這個 mDefaultPadding 就不要害怕這個問題。

在這裡插入圖片描述


繪製刻度線的準備

開始繪製先前,我們先要準備下一些工具,

  1. 首先一個 Paint 物件是必不可少的,
  2. 然後為了方便使用者使用,我們再定義一個顏色,暴露給予設定,
  3. 最後我們還需要一個 int 型的值,用來設定刻度線的長度
    /* 刻度線長度 */
    private float mScaleLength;
    /* 刻度線畫筆 */
    private Paint mScaleLinePaint;
    /* 背景色 */
    private int mBackgroundColor;

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
        mBackgroundColor = ta.getColor(R.styleable.ClockView_clock_backgroundColor, Color.parseColor("#237EAD"));
        mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
        mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
        ta.recycle();
        .
        .
        .
        mScaleLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mScaleLinePaint.setStyle(Paint.Style.STROKE);
        mScaleLinePaint.setColor(mBackgroundColor);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        .
        .
        .
        mScaleLength = 0.12f * mRadius;// 根據比例確定刻度線長度
        mScaleLinePaint.setStrokeWidth(0.012f * mRadius);// 刻度圈的寬度
    }

開始繪製刻度線

繪製國晨反而很簡單,對於我們來說 一小時 60min 一分鐘 60s,最好的情況莫過於分為 360 份,但是這樣一來,由於手機螢幕比較小會直接導致先太密集,密集到了變成圓地步:

在這裡插入圖片描述

所以這裡,我們將 360 度,劃分為 200份 ,

  1. 360/200 = 1.8f
  2. 繪製時,我們沒繪製一條邊 將 Canvas 角度旋轉 1.8f
  3. 起點:每次我們都從畫板頂部開始,下移一個 Padding 再加上 mTextRect 的高度,也就是點鐘文字高度,之後再加上一個
    刻度線長度由於將刻度線與圓弧分隔開來,防止它們粘在一起
  4. 終點:筆起點多一個 刻度線長度即可
    /**
     * 畫一圈梯度渲染的亮暗色漸變圓弧,重繪時不斷旋轉,上面蓋一圈背景色的刻度線
     */
    private void drawScaleLine() {
        mCanvas.save();
        // 畫背景色刻度線
        for (int i = 0; i < 100; i++) {
            mCanvas.drawLine(getWidth() / 2, mPaddingTop + mScaleLength + mTextRect.height() / 2,
                    getWidth() / 2, mPaddingTop + 2 * mScaleLength + mTextRect.height() / 2, mScaleLinePaint);
            mCanvas.rotate(1.8f, getWidth() / 2, getHeight() / 2);
        }
        mCanvas.restore();
    }

大功告成

專案 Demo 地址:
https://github.com/FishInWater-1999/android_view_user_defined_first.git

如果有錯歡迎在評論區指出,非常感謝~

祝大家程式設計愉快!

相關文章