今天繼續聊自定義View,當然今天的這個比較麻煩一些,如果沒有自定義View的經歷,建議先看看自定義文字View與水印圖片View
前面的自定義文字View,圖片View都屬於比較簡單的自定義View,今天玩點有難度的,當然目的也是爲了更加熟悉自定義View的各個步驟與座標的計算、畫筆的各種屬性等。話不多聊 ,我們今天實現下如下的效果:
首先進行簡單的分析:
鐘表盤構成屬性如下:
1、外部圓形邊框(寬度、顏色)
2、內部一週的小黑點(寬度、顏色)
3、內部的1-12數字(字號、顏色)
4、時針分針和秒針(規格、顏色)
簡單分析之後就能確定我們需要哪些的屬性值,當然此處全部自定義屬性,有些事沒有必要的。
分析之後進行屬性的自定義:
屬性的自定義如下:
<declare-styleable name="DemoClockView01">
<attr name="borderwidth" format="dimension"/>
<attr name="bordercolor" format="color"/>
<attr name="pointcolor" format="color"/>
<attr name="hourcolor" format="color"/>
<attr name="minutecolor" format="color"/>
<attr name="secondcolor" format="color"/>
<attr name="numsize" format="dimension"/>
<attr name="numcolor" format="color"/>
</declare-styleable>
其中的borderwidth和bordercolor爲邊框的寬度和顏色,pointcolor爲一圈的小黑點的顏色,hourcolor、minutecolor和secondcolor爲時針、分針和秒針的顏色,numsize和numcolor爲一圈數字的字號大小和顏色。
屬性自定義完成之後再Java代碼中拿到相關的屬性值:
代碼如下:
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DemoClockView01, defStyleAttr, 0);
for (int i = 0; i < array.getIndexCount(); i++) {//用getIndexCount 減少循環次數,提高性能 用.length也不能執行所有的case情況
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.DemoClockView01_borderwidth://邊框寬度
mBorderWidth = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
break;
case R.styleable.DemoClockView01_bordercolor://邊框顏色
mBorderColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_numcolor://數字顏色
mNumColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_numsize://數字字號
mNumSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.DemoClockView01_pointcolor://周圍小點顏色
mPointColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_hourcolor://時針顏色
mHourColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_minutecolor://分針顏色
mMinuteColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_secondcolor://秒針顏色
mSecondColor = array.getColor(attr, Color.BLACK);
break;
}
}
array.recycle();
拿到相關的屬性之後進行控件寬和高的測量:
代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
int desire = getPaddingLeft() + getPaddingRight() + (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
mWidth = Math.min(desire, widthSize);
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
int desire = getPaddingTop() + getPaddingBottom() + (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
mHeight = Math.min(desire, heightSize);
}
mWidth = Math.min(mWidth, mHeight);//取最小值 防止繪製內容出錯 以最小的邊來爲基準進行相關的繪製
setMeasuredDimension(mWidth, mWidth);
}
測量完成之後進行最後的繪製,從外到內一個一個來繪製:
1、首先是外部邊框,圓形,計算圓心及半徑,繪製如下:
/**
* 圓心的xy和圓環的寬度
*/
final int cx, cy, width;
cx = getPaddingLeft() + (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / 2;
cy = getPaddingTop() + (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 2;
width = Math.min(getWidth() / 2, getHeight() / 2);//半徑
mPaint.setAntiAlias(true);//去除邊緣鋸齒,優化繪製效果
mPaint.setColor(mBorderColor);
if (mBorderColor == 0){
mPaint.setColor(Color.BLACK);
}
canvas.drawCircle(cx, cy, width, mPaint);//外圓 紅色
mPaint.setColor(Color.WHITE);
canvas.drawCircle(cx, cy, width - mBorderWidth, mPaint);//內圓 白色
此步驟完成之後在佈局文件飲用控件即可看到外部的圓環效果,如下圖:
2、周圍小黑點的繪製:
mPaint.setColor(mPointColor);
if (mPointColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//保存當前的狀態
for (int i = 0; i < 60; i++) {//總共60個點 所以繪製60次 //繪製一圈的小黑點
if (i % 5 == 0) {
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()), mPaint);
} else {
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()), mPaint);
}
canvas.rotate(6, cx, cy);//360度 繪製60次 每次旋轉6度
}
canvas.restore();//將canvas轉回來
此步驟繪製完成之後即可看到圓環加上小圓點的效果,效果如下圖:
3、數字的繪製(此處繪製的數字可能旋轉了,因爲繪製的時候是旋轉畫布繪製的,當然也可以計算每個數字點的座標進行相關的繪製)
mPaint.setColor(mNumColor);
if (mNumColor == 0) {
mPaint.setColor(Color.BLACK);
}
mPaint.setTextSize(mNumSize);
if (mNumSize == 0) {
mPaint.setTextSize((int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
}
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
String[] strs = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",};//繪製數字1-12 (數字角度不對 可以進行相關的處理)
Rect rect = new Rect();
canvas.save();
for (int i = 0; i < 12; i++) {//繪製12次 每次旋轉30度
mPaint.getTextBounds(strs[i], 0, strs[i].length(), rect);
canvas.drawText(strs[i], cx - rect.width() / 2,
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, getResources().getDisplayMetrics()) + rect.height(), mPaint);
canvas.rotate(30, cx, cy);
}
canvas.restore();
此處繪製完成即可看到圓環、黑點和數字效果如下圖:
4、時針、分針、秒針和圓心的繪製:
mPaint.setColor(mHourColor);
if (mHourColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//繪製時針
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()) + rect.width(),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();
mPaint.setColor(mMinuteColor);
if (mMinuteColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//保存後面的狀態
canvas.rotate(60, cx, cy);
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();//撤銷保存的狀態
mPaint.setColor(mSecondColor);
if (mSecondColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();
mPaint.setColor(Color.RED);
canvas.rotate(120, cx, cy);
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();
mPaint.setColor(Color.RED);
canvas.drawCircle(cx, cy, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, getResources().getDisplayMetrics()), mPaint);//圓心,紅色
此時完成即可看到完整的鐘表盤效果。
如下圖:
靜態時鐘功能到此實現。
下面呢的任務就是讓其動起來:
1)時間任務,每隔1s繪製界面。
在構造方法中啓動時間任務,如下:
Timer timer = new Timer("繪製線程");
timer.schedule(new TimerTask() {
@Override
public void run() {
}
}, 0, 1000);
2)計算每個時刻的對應的時針、分針、及秒針的角度。
在構造方法初始化時間如下(這裏的時間類使用Calendar類):
mCalendar = Calendar.getInstance();
在onDraw方法中進行時間及角度的計算:
//關於當前時間的計算,默認爲當前時間 當然是可以設置的
int hour = mCalendar.get(Calendar.HOUR);//HOUR 進製爲12小時 HOUR_OF_DAY 爲24小時
int minute = mCalendar.get(Calendar.MINUTE);//分鐘
int second = mCalendar.get(Calendar.SECOND) + 1;//秒數
if (second == 60) {
minute += 1;
second = 0;
}
if (minute == 60){
hour += 1;
minute = 0;
}
if (hour == 12){
hour = 0;
}
mCalendar.set(Calendar.SECOND, second);
mCalendar.set(Calendar.MINUTE, minute);
mCalendar.set(Calendar.HOUR, hour);
float hourDegree = 360 * hour / 12 + 360 / 12 * minute / 60;//時針轉動的角度 小時對應角度 加上 分鐘對應角度 秒針忽略
float minuteDegree = 360 * minute / 60 + 360 / 60 * second / 60;//分針轉動的角度 分針對應角度 加上 秒數對應角度
float secondDegree = 360 * second / 60;// 秒數對應角度
3)子線程、UI線程的切換繪製。
Handler進行子線程到子線程的轉換。
最終的實現效果如下,也可以自己進行時間的設置:
到此效果實現。
git地址:https://github.com/SnowJun/OwnView