學習自定義View辰如,遂動手寫了一個時鐘控件拌牲,歡迎批評指正饵隙。
首先上效果圖:
首先撮珠,按需求分析思路:
繪制步驟
- 繪制大圓圈、刻度癞季、
- 繪制數字
- 繪制指針
其他操作
- 自動開啟計時
- 適配wrap_content和固定width劫瞳、height值;
- 自定義屬性:時鐘顏色绷柒、各個刻度顏色志于、三種指針顏色等
接下來按步驟繪制:
我們先看onDraw方法中我們的操作,這里對應我們上面說的步驟:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 坐標原點移動到View 中心
canvas.translate(mCenterX, mCenterY);
drawCircle(canvas);
drawText(canvas);
drawPointer(canvas);
}
繪制圓圈和刻度
/**
* 繪制時鐘的圓形和刻度
*/
private void drawCircle(Canvas canvas) {
mDefaultPaint.setStrokeWidth(mDefaultScaleWidth);
mDefaultPaint.setColor(mClockColor);
canvas.drawCircle(0, 0, mRadius, mDefaultPaint);
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) { // 特殊時刻
mDefaultPaint.setStrokeWidth(mParticularlyScaleWidth);
mDefaultPaint.setColor(mColorParticularyScale);
canvas.drawLine(0, -mRadius, 0, -mRadius + mParticularlyScaleLength, mDefaultPaint);
} else { // 一般時刻
mDefaultPaint.setStrokeWidth(mDefaultScaleWidth);
mDefaultPaint.setColor(mColorDefaultScale);
canvas.drawLine(0, -mRadius, 0, -mRadius + mDefaultScaleLength, mDefaultPaint);
}
canvas.rotate(6);
}
}
這里主要有兩點:
1废睦、表示小時的刻度伺绽,我們作為特殊刻度,其線的長度嗜湃、寬度奈应、顏色,與普通刻度做區(qū)別购披;
2杖挣、每次繪制完一個刻度,我們調用canvas.rotate(6)方法刚陡,讓其旋轉6度(360度/60)惩妇,這樣繪制起來比較方便株汉,避免了計算每一個刻度的坐標值。
繪制文字:
/**
* 繪制特殊時刻(12點歌殃、3點乔妈、6點、9點)的文字
*/
private void drawText(Canvas canvas) {
setTextPaint();
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
// 文字頂部與基線距離
float ascent=Math.abs(fontMetrics.ascent);
// 文字底部與基線距離
float descent=Math.abs(fontMetrics.descent);
// 文字高度
float fontHeight = ascent+descent;
// 文字豎直中心點距離基線的距離氓皱;
float offsetY = fontHeight / 2 - Math.abs(fontMetrics.descent);
// 文字寬度
float fontWidth;
// drawText(@NonNull String text, float x, float y, @NonNull Paint paint) 參數:y路召,為基線的y坐標,并非文字左下角的坐標
// 文字距離圓圈的距離為 特殊刻度長度+寬度
String h = "12";
// y軸坐標為: -(半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字頂部距離基線的距離)
float y=-(mRadius-mParticularlyScaleLength-mParticularlyScaleWidth-ascent);
canvas.drawText(h, 0,y, mTextPaint);
h = "3";
fontWidth = mTextPaint.measureText(h);
// y軸坐標為: 半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字長度/2(繪制原點在文字橫向中心)
y=mRadius - mParticularlyScaleLength-mParticularlyScaleWidth - (fontWidth / 2);
canvas.drawText(h,y, 0 + offsetY, mTextPaint);
h = "6";
// y軸坐標為: 半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字底部與基線的距離
y=mRadius - mParticularlyScaleLength -mParticularlyScaleWidth-descent;
canvas.drawText(h, 0,y, mTextPaint);
h = "9";
fontWidth = mTextPaint.measureText(h);
// y軸坐標為: -(半徑-特殊刻度長度-特殊刻度寬度(作為間距)-文字長度/2(繪制原點在文字橫向中心))
y= -(mRadius - mParticularlyScaleLength -mParticularlyScaleWidth -(fontWidth/2));
canvas.drawText(h,y, 0 + offsetY, mTextPaint);
}
private void setTextPaint(){
mTextPaint.setStrokeWidth(mDefaultScaleWidth / 2);
mTextPaint.setTextSize(mParticularlyScaleWidth * 4);
// 文字繪制中心點移動到橫向中心
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
這里只繪制了四個時間的文字波材,但是這四個數字坐標規(guī)則都不相同股淡,我們需要分開繪制;
首先,我們了解一下文字繪制方法:
canvas. drawText(String text, float x, float y,Paint paint)
這里的x和y就是文字繪制的坐標各聘,如圖:(圖片來自http://www.gcssloop.com/customview/Canvas_PictureText)
這里看上去揣非,x和y像是文字左下角的坐標,可是躲因,這里其實是文字的基線(BaseLine )起點的的坐標早敬,什么是基線呢?
我們先了解Paint.FontMetrics 類大脉,他是用來描述文字的上下高度的類搞监,包含屬性如下:
圖解(來源見水印):
對照著這兩個圖镰矿,再看代碼的時候琐驴,應該就懂了。
繪制指針:
/**
* 繪制指針
*/
private void drawPointer(Canvas canvas) {
drawHourPointer(canvas);
drawMinutePointer(canvas);
drawSecondPointer(canvas);
mPointerPaint.setColor(mClockColor);
// 繪制中心原點秤标,需要在指針繪制完成后才能繪制
canvas.drawCircle(0, 0, mPointRadius, mPointerPaint);
}
首先繪制時針绝淡,我們可以聯(lián)想一下,當現在時間是3:30的時候苍姜,時針應該在那個位置呢牢酵?指在3上嗎?不是的衙猪,因為現在已經是45分了馍乙,時針應該在3到4之間,在二分之一的位置垫释,更精確一點的話丝格,我們還要考慮秒針的狀態(tài)。那么棵譬,我們就可以通過當前時間显蝌,算出指針與x軸的角度,從而通過三角函數订咸,計算出指針的目標坐標值:
/**
* 繪制時針
*/
private void drawHourPointer(Canvas canvas) {
mPointerPaint.setStrokeWidth(mHourPointerWidth);
mPointerPaint.setColor(mColorHourPointer);
// 當前時間的總秒數
float s = mH * 60 * 60 + mM * 60 + mS;
// 百分比
float percentage = s / (12 * 60 * 60);
// 通過角度計算弧度值曼尊,因為時鐘的角度起線是y軸負方向扭屁,而View角度的起線是x軸正方向,所以要加270度
float angle = 270 + 360 * percentage;
float x = (float) (mHourPointerLength * Math.cos(Math.PI * 2 / 360 * angle));
float y = (float) (mHourPointerLength * Math.sin(Math.PI * 2 / 360 * angle));
canvas.drawLine(0, 0, x, y, mPointerPaint);
}
/**
* 繪制分針
*/
private void drawMinutePointer(Canvas canvas) {
mPointerPaint.setStrokeWidth(mMinutePointerWidth);
mPointerPaint.setColor(mColorMinutePointer);
float s = mM * 60 + mS;
float percentage = s / (60 * 60);
float angle = 270 + 360 * percentage;
float x = (float) (mMinutePointerLength * Math.cos(Math.PI * 2 / 360 * angle));
float y = (float) (mMinutePointerLength * Math.sin(Math.PI * 2 / 360 * angle));
canvas.drawLine(0, 0, x, y, mPointerPaint);
}
/**
* 繪制秒針
*/
private void drawSecondPointer(Canvas canvas) {
mPointerPaint.setStrokeWidth(mSecondPointerWidth);
mPointerPaint.setColor(mColorSecondPointer);
float s = mS;
float percentage = s / 60;
float angle = 270 + 360 * percentage;
float x = (float) (mSecondPointerLength * Math.cos(Math.PI * 2 / 360 * angle));
float y = (float) (mSecondPointerLength * Math.sin(Math.PI * 2 / 360 * angle));
canvas.drawLine(0, 0, x, y, mPointerPaint);
}
到這里涩禀,自定義View的繪制部分已完成,下面就是屬于一些自定義View中經常用到的操作然眼;
其他操作
開啟計時
當然艾船,時鐘是動態(tài)的,每一秒都在變化高每,我們需要在線程中不斷刷新當前View:
/**
* 開始計時
*/
private void startTime() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
getTime();
}
}
}).start();
}
/**
* 獲取當前系統(tǒng)時間
*/
private void getTime() {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR);
hour = hour % 12;
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
if (hour != mH || minute != mM || second != mS) {
setTime(hour, minute, second);
postInvalidate();
}
}
/**
* 設置時間
*/
private void setTime(int h, int m, int s) {
mH = h;
mM = m;
mS = s;
}
startTime()方法需要在構造方法中調用屿岂;
適配不同大小:
我們首先在onMeasure(int widthMeasureSpec, int heightMeasureSpec)中鲸匿,對尺寸進行測量爷怀,并當wrap_content模式下,默認尺寸為48dp带欢。
這部分代碼基本每個自定義View中都會用到运授,比較通用,也比較常見乔煞,代碼就不貼了吁朦,可以clone源碼看一下。
而對于時鐘的尺寸屬性渡贾,我們可以通過與半徑的比例計算逗宜,這樣就比較好的適配了不同尺寸下的時鐘大小:
@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;
mRadius = (float) (w / 2 * 0.8);
initClockPointerLength();
}
/**
* 根據控件的大小空骚,初始化時鐘刻度的長度和寬度纺讲、指針的長度和寬度、時鐘中心點的半徑
*/
private void initClockPointerLength() {
/*
* 默認時鐘刻度長=半徑/10;
* 默認時鐘刻度寬=長/6;
*
* */
mDefaultScaleLength = mRadius / 10;
mDefaultScaleWidth = mDefaultScaleLength / 6;
/*
* 特殊時鐘刻度長=半徑/5;
* 特殊時鐘刻度寬=長/6;
*
* */
mParticularlyScaleLength = mRadius / 5;
mParticularlyScaleWidth = mParticularlyScaleLength / 6;
/*
* 時針長=半徑/3;
* 時針寬=特殊時鐘刻度寬;
*
* */
mHourPointerLength = mRadius / 3;
mHourPointerWidth = mParticularlyScaleWidth;
/*
* 分針長=半徑/2;
* 分針寬=特殊時鐘刻度寬;
*
* */
mMinutePointerLength = mRadius / 2;
mMinutePointerWidth = mParticularlyScaleWidth;
/*
* 秒針長=半徑/3*2;
* 秒針寬=默認時鐘刻度寬;
*
* */
mSecondPointerLength = mRadius / 3 * 2;
mSecondPointerWidth = mDefaultScaleWidth;
// 中心點半徑=(默認刻度寬+特殊刻度寬)/2
mPointRadius = (mDefaultScaleWidth + mParticularlyScaleWidth) / 2;
}
自定義屬性值
attr.xml:
<declare-styleable name="ClockView">
<attr name="clockColor" format="color"/>
<attr name="defaultScaleColor" format="color"/>
<attr name="particularlyScaleColor" format="color"/>
<attr name="hourPointerColor" format="color"/>
<attr name="minutePointerColor" format="color"/>
<attr name="secondPointerColor" format="color"/>
</declare-styleable>
對應:
// 時鐘顏色囤屹、默認刻度顏色熬甚、時刻度顏色、時針顏色牺丙、分針顏色则涯、秒針顏色
private int mClockColor,mColorDefaultScale,mColorParticularyScale,mColorHourPointer,
mColorMinutePointer, mColorSecondPointer;
好了,這是自定義View系列的第一篇博文冲簿,后面會繼續(xù)這方面的博文粟判,共勉!