關於本文:本文原先在我的 CSDN 部落格釋出(由圖片水印能發現),整理以往部落格過程中,發現當時總結的很仔細,所以將其遷移到這裡,希望對大家在自定義 View 方面,能有所幫助 ?
引言
Android 自定義 View 應用非常廣泛,最近逛 github 是偶然發現一個 Demo 感覺寫的很好,我結合著這個專案的內容,給大家講講如何繪製時鐘錶盤,也算是加深下自己對自定義 View 的理解,涉及內容比較多,大家慢慢吸收。
最後效果:
開始之前,先讓大家看看最後的效果
現在開始
讓我們先搭建這個 View
- 首先,我們定義一個叫做 ClockView 的自定義 View ,讓它繼承自 View 類。
- 然後在 /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() 方法,保證繪製的是圓
包正繪圖是圓形的前提是:
- 保證 RectF 切割的是正方形
- 那麼保證 RextF 圍成的是正方形,就要需要知道正方形四邊距離控制元件邊界的距離
- 也就是我們需要計算四個整型變數 :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 就不要害怕這個問題。
繪製刻度線的準備
開始繪製先前,我們先要準備下一些工具,
- 首先一個 Paint 物件是必不可少的,
- 然後為了方便使用者使用,我們再定義一個顏色,暴露給予設定,
- 最後我們還需要一個 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份 ,
- 360/200 = 1.8f
- 繪製時,我們沒繪製一條邊 將 Canvas 角度旋轉 1.8f
- 起點:每次我們都從畫板頂部開始,下移一個 Padding 再加上 mTextRect 的高度,也就是點鐘文字高度,之後再加上一個
刻度線長度由於將刻度線與圓弧分隔開來,防止它們粘在一起 - 終點:筆起點多一個 刻度線長度即可
/**
* 畫一圈梯度渲染的亮暗色漸變圓弧,重繪時不斷旋轉,上面蓋一圈背景色的刻度線
*/
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
如果有錯歡迎在評論區指出,非常感謝~
祝大家程式設計愉快!