自定義view一直是Android進階路上的一塊石頭,跨過去就是墊腳石,跨不過去就是絆腳石。作爲一個攻城獅,怎麼能被他絆倒,一定要跟它死磕到底,這段時間看到自定義View新手實戰-一步步實現精美的鐘表界面特別漂亮,咱們也來手擼一個。
先看下效果圖
咱們先寫一個類WatchBoard繼承View,並重寫他的構造方法
public class WatchBoard extends View {
public WatchBoard(Context context) {
this(context,null);
}
public WatchBoard(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
/**
* 這是最全面構造方法的寫法,存在一個問題,當minSdkVersion<21的時候,這裏會出現紅線,這裏有兩個解決辦法
* 1.刪除最後一個構造函數,使用前三個就可以,最直接暴力。
* 2.加入 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)或者
* @TargetApi(Build.VERSION_CODES.LOLLIPOP),
* 這裏注意加入這些註解只是不出現紅線,一旦使用這個構造函數在API 21以下還是會出現錯誤。
*/
// 這裏寫內容
}
1. 首先聲明需要的屬性
在res/values包下新建attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定義屬性-->
<declare-styleable name="WatchBoard">
<!--錶盤的內邊距-->
<attr name="wb_padding" format="dimension"/>
<!--錶盤文字大小-->
<attr name="wb_text_size" format="dimension"/>
<!--時針的寬度-->
<attr name="wb_hour_pointer_width" format="dimension"/>
<!--分針的寬度-->
<attr name="wb_minute_pointer_width" format="dimension"/>
<!--秒針的寬度-->
<attr name="wb_second_pointer_width" format="dimension"/>
<!--指針圓角值-->
<attr name="wb_pointer_corner_radius" format="dimension"/>
<!--指針超過中心點的長度-->
<attr name="wb_pointer_end_length" format="dimension"/>
<!--時刻刻度顏色-->
<attr name="wb_scale_long_color" format="color"/>
<!--非時刻刻度顏色-->
<attr name="wb_scale_short_color" format="color"/>
<!--時針顏色-->
<attr name="wb_hour_pointer_color" format="color"/>
<!--分針顏色-->
<attr name="wb_minute_pointer_color" format="color"/>
<!--秒針顏色-->
<attr name="wb_second_pointer_color" format="color"/>
</declare-styleable>
</resources>
我們一氣呵成,在類裏面聲明需要的屬性如下
private float mRadius; // 圓形半徑
private float mPadding; // 邊距
private float mTextSize; // 文字大小
private float mHourPointWidth; // 時針寬度
private float mMinutePointWidth; // 分針寬度
private float mSecondPointWidth; // 秒針寬度
private float mPointRadius; // 指針圓角
private float mPointEndLength; // 指針末尾長度
private int mHourPointColor; // 時針的顏色
private int mMinutePointColor; // 分針的顏色
private int mSecondPointColor; // 秒針的顏色
private int mColorLong; // 長線的顏色
private int mColorShort; // 短線的顏色
private Paint mPaint; // 畫筆
private PaintFlagsDrawFilter mDrawFilter; // 爲畫布設置抗鋸齒
在構造方法中寫一個方法取出我們的屬性
/**
* @param attrs
*/
private void obtainStyledAttrs(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.WatchBoard);
mPadding = typedArray.getDimension(R.styleable.WatchBoard_wb_padding, DptoPx(10));
mTextSize = typedArray.getDimension(R.styleable.WatchBoard_wb_text_size, SptoPx(16));
mHourPointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_hour_pointer_width, DptoPx(5));
mMinutePointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_minute_pointer_width, DptoPx(3));
mSecondPointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_second_pointer_width, DptoPx(2));
mPointRadius = typedArray.getDimension(R.styleable.WatchBoard_wb_pointer_corner_radius, DptoPx(10));
mPointEndLength = typedArray.getDimension(R.styleable.WatchBoard_wb_pointer_end_length, DptoPx(10));
mHourPointColor = typedArray.getColor(R.styleable.WatchBoard_wb_hour_pointer_color, Color.BLACK);
mMinutePointColor = typedArray.getColor(R.styleable.WatchBoard_wb_minute_pointer_color, Color.BLACK);
mSecondPointColor = typedArray.getColor(R.styleable.WatchBoard_wb_second_pointer_color, Color.RED);
mColorLong = typedArray.getColor(R.styleable.WatchBoard_wb_scale_long_color, Color.argb(225, 0, 0, 0));
mColorShort = typedArray.getColor(R.styleable.WatchBoard_wb_scale_short_color, Color.argb(125, 0, 0, 0));
// 一定要回收
typedArray.recycle();
}
我們還需要一個畫筆,也在構造器中初始化
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 獲取屬性
obtainStyledAttrs(attrs);
//初始化畫筆
initPaint();
// 爲畫布實現抗鋸齒
mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
//測量手機的寬度
int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
int heightPixels = context.getResources().getDisplayMetrics().heightPixels;
// 默認和屏幕的寬高最小值相等
width = Math.min(widthPixels, heightPixels);
}
initPaint方法比較簡單
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
我們進行onMeasure()的測量,這裏作者用了一個方法setMeasuredDimension(width,width)來保證控件的寬高相等。這裏我們先給width一個固定值即屏幕的短邊。
//測量手機的寬度
int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
int heightPixels = context.getResources().getDisplayMetrics().heightPixels;
// 默認和屏幕的寬高最小值相等
width = Math.min(widthPixels,heightPixels);
重寫onMeasure()方法,設置鐘錶爲一個正方形
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 傳入相同的數width,height,確保是正方形背景
setMeasuredDimension(measureSize(widthMeasureSpec),measureSize(heightMeasureSpec));
}
// 這裏不用管測量模式是什麼,因爲咱們有屏幕短邊保底,只取其中一個小值即可。測量寬高和屏幕短邊作對比,返回最小值
private int measureSize(int measureSpec) {
int size=MeasureSpec.getSize(measureSpec);
width=Math.min(width,size);
return width;
}
爲了驗證結果,咱們測試一番,先將xml中修改寬高屬性:
<com.example.jmf.timetest.WatchBoard
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/colorPrimary"
app:wb_scale_short_color="@color/colorAccent"/>
我們在onMeasure()方法和measureSize()方法中分別加入Log日誌。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 傳入相同的數width,height,確保是正方形背景
setMeasuredDimension(measureSize(widthMeasureSpec),measureSize(heightMeasureSpec));
}
// 這裏不用管測量模式是什麼,因爲咱們有屏幕短邊保底,只取其中一個小值即可。測量寬高和屏幕短邊作對比,返回最小值
private int measureSize(int measureSpec) {
int size=MeasureSpec.getSize(measureSpec);
width=Math.min(width,size);
return width;
}
運行結果如下:
從鐘錶背景色可以看出,鐘錶依然是正方形。我們再來看下日誌文件:
日誌結果如下,onMeasure進行了兩次測量,最終結果爲width = 900,爲什麼是900呢?因爲我的手機density = 3。
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 1080
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard onMeasure()i+++j ==1080+++900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard onMeasure()i+++j ==900+++900
測量完成之後我們要重寫一個onSizeChanged()來獲取最終測量好的width,順便給錶盤半徑mRadius賦值。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = (Math.min(w, h) - mPadding) / 2;
mPointEndLength = mRadius / 6; // 設置成半徑的六分之一
}
第一步繪製圓盤背景,當然繪製方法是在onDraw()方法中,這裏重寫onDraw()方法併爲canvas設置抗鋸齒
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 爲畫布設置抗鋸齒
canvas.setDrawFilter(mDrawFilter);
// 繪製半徑圓
drawCircle(canvas);
}
// 繪製半徑圓
private void drawCircle(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2, width/2, mRadius, mPaint);
}
相信這裏沒有什麼難度,效果如下
第二步繪製刻度,刻度尺分爲大刻度尺和小刻度尺,對應的顏色不同,我們需要區別對待
private void drawScale(Canvas canvas) {
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
int lineWidth;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
} else {
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
}
canvas.drawLine(width/2, mPadding, width/2, mPadding + lineWidth, mPaint);
canvas.rotate(6,width/2,width/2);
}
}
第三步繪製數字,字和長刻度繪製保持一致,我們直接在繪製長刻度的時候繪製字體即可,算法如下
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
整體代碼如下
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
int lineWidth;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
// 這裏是字體的繪製
mPaint.setTextSize(mTextSize);
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
Rect textBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), textBound);
mPaint.setColor(Color.BLACK);
canvas.drawText(text, width / 2 - textBound.width() / 2, textBound.height() + DptoPx(5) + lineWidth + mPadding, mPaint);
} else {
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
}
canvas.drawLine(width / 2, mPadding, width / 2, mPadding + lineWidth, mPaint);
canvas.rotate(6, width / 2, width / 2);
}
運行效果如下,這裏字體好像沒有完全正過來,先不管了,接着實現指針
第四步繪製指針和圓點
private void drawPointer(Canvas canvas) {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR);// 時
int minute = calendar.get(Calendar.MINUTE);// 分
int second = calendar.get(Calendar.SECOND);// 秒
// 轉過的角度
float angleHour = (hour + (float) minute / 60) * 360 / 12;
float angleMinute = (minute + (float) second / 60) * 360 / 60;
int angleSecond = second * 360 / 60;
// 繪製時針
canvas.save();
canvas.rotate(angleHour,width/2,width/2); // 旋轉到時針的角度
RectF rectHour = new RectF(width/2 -mHourPointWidth / 2, width/2 -mRadius * 3 / 5, width/2 +mHourPointWidth / 2, width/2 + mPointEndLength);
mPaint.setColor(mHourPointColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mHourPointWidth);
canvas.drawRoundRect(rectHour, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 繪製分針
canvas.save();
canvas.rotate(angleMinute,width/2,width/2); // 旋轉到分針的角度
RectF rectMinute = new RectF(width/2-mMinutePointWidth / 2, width/2-mRadius * 3.5f / 5, width/2+mMinutePointWidth / 2, width/2+mPointEndLength);
mPaint.setColor(mMinutePointColor);
mPaint.setStrokeWidth(mMinutePointWidth);
canvas.drawRoundRect(rectMinute, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 繪製分針
canvas.save();
canvas.rotate(angleSecond,width/2,width/2); // 旋轉到分針的角度
RectF rectSecond = new RectF(width/2-mSecondPointWidth / 2, width/2-mRadius + DptoPx(10), width/2+mSecondPointWidth / 2, width/2+mPointEndLength);
mPaint.setStrokeWidth(mSecondPointWidth);
mPaint.setColor(mSecondPointColor);
canvas.drawRoundRect(rectSecond, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 繪製原點
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2, width/2, mSecondPointWidth * 4, mPaint);
}
結果已經有了,現在就是要讓他動起來就OK了,這裏我們可以使用BroadcastReceiver來監聽時間的改變,也可以直接用postInvalidateDelayed(1000)一秒後重繪一句話解決。現在onDerw()中代碼如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.setDrawFilter(mDrawFilter);
// 繪製半徑圓
drawCircle(canvas);
// 繪製刻度尺
drawScale(canvas);
// 繪製指針
drawPointer(canvas);
// 每一秒刷新一次
postInvalidateDelayed(1000);
}
最終效果:
這個自定義view已經搞定了,Demo已經傳到Github,雙手奉上
Github地址: https://github.com/Jmengfei/CustomView
源碼都在裏面,還會有後續的自定義view添加進去。如果對你們有幫助,不妨留個star和fork,如果有任何問題,歡迎留言指正。