為了加強對自定義 View 的認知以及開發能力,我計劃這段時間陸續來完成幾個難度從易到難的自定義 View,並簡單的寫幾篇部落格來進行介紹,所有的程式碼也都會開源,也希望讀者能給個 star 哈 GitHub 地址:github.com/leavesC/Cus… 也可以下載 Apk 來體驗下:www.pgyer.com/CustomView
先看下效果圖:
ClockView 的邏輯並不算複雜,重點在於時鐘刻度以及三根指示針的繪製,然後設定一個定時任務每秒重新整理繪製即可
一、確定寬高
為 View 設定其預設大小為 DEFAULT_SIZE
//View的預設大小,dp
private static final int DEFAULT_SIZE = 320;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = dp2px(DEFAULT_SIZE);
int widthSize = getSize(widthMeasureSpec, defaultSize);
int heightSize = getSize(heightMeasureSpec, defaultSize);
widthSize = heightSize = Math.min(widthSize, heightSize);
setMeasuredDimension(widthSize, heightSize);
}
protected int getSize(int measureSpec, int defaultSize) {
int mode = MeasureSpec.getMode(measureSpec);
int size = 0;
switch (mode) {
case MeasureSpec.AT_MOST: {
size = Math.min(MeasureSpec.getSize(measureSpec), defaultSize);
break;
}
case MeasureSpec.EXACTLY: {
size = MeasureSpec.getSize(measureSpec);
break;
}
case MeasureSpec.UNSPECIFIED: {
size = defaultSize;
break;
}
}
return size;
}
複製程式碼
二、初始化畫筆
在建構函式中初始化繪製錶盤以及文字的畫筆
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initClockPaint();
initTextPaint();
time = new Time();
timerHandler = new TimerHandler(this);
}
private void initClockPaint() {
clockPaint = new Paint();
clockPaint.setStyle(Paint.Style.STROKE);
clockPaint.setAntiAlias(true);
clockPaint.setStrokeWidth(aroundStockWidth);
}
private void initTextPaint() {
textPaint = new Paint();
textPaint.setStyle(Paint.Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setStrokeWidth(12);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(textSize);
}
複製程式碼
三、繪製
在此處是通過不斷轉換 Canvas 的座標系來完成刻度以及指示針的繪製,這相比通過數學計算來計算各個刻度的位置要簡單得多
@Override
protected void onDraw(Canvas canvas) {
//中心點的橫縱座標
float pointWH = getWidth() / 2.0f;
//內圓的半徑
float radiusIn = pointWH - aroundStockWidth;
canvas.translate(pointWH, pointWH);
//繪製錶盤
if (aroundStockWidth > 0) {
clockPaint.setStrokeWidth(aroundStockWidth);
clockPaint.setStyle(Paint.Style.STROKE);
clockPaint.setColor(aroundColor);
canvas.drawCircle(0, 0, pointWH - aroundStockWidth / 2.0f, clockPaint);
}
clockPaint.setStyle(Paint.Style.FILL);
clockPaint.setColor(Color.WHITE);
canvas.drawCircle(0, 0, radiusIn, clockPaint);
//繪製小短線
canvas.save();
canvas.rotate(-90);
float longLineLength = radiusIn / 16.0f;
float longStartY = radiusIn - longLineLength;
float longStopY = longStartY - longLineLength;
float longStockWidth = 2;
float temp = longLineLength / 4.0f;
float shortStartY = longStartY - temp;
float shortStopY = longStopY + temp;
float shortStockWidth = longStockWidth / 2.0f;
clockPaint.setColor(Color.BLACK);
float degrees = 6;
for (int i = 0; i <= 360; i += degrees) {
if (i % 30 == 0) {
clockPaint.setStrokeWidth(longStockWidth);
canvas.drawLine(0, longStartY, 0, longStopY, clockPaint);
} else {
clockPaint.setStrokeWidth(shortStockWidth);
canvas.drawLine(0, shortStartY, 0, shortStopY, clockPaint);
}
canvas.rotate(degrees);
}
canvas.restore();
//繪製時鐘數字
if (textSize > 0) {
float x, y;
for (int i = 1; i <= 12; i += 1) {
textPaint.getTextBounds(String.valueOf(i), 0, String.valueOf(i).length(), rect);
float textHeight = rect.height();
float distance = radiusIn - 2 * longLineLength - textHeight;
double tempVa = i * 30.0f * Math.PI / 180.0f;
x = (float) (distance * Math.sin(tempVa));
y = (float) (-distance * Math.cos(tempVa));
canvas.drawText(String.valueOf(i), x, y + textHeight / 3, textPaint);
}
}
canvas.rotate(-90);
clockPaint.setStrokeWidth(2);
//繪製時針
canvas.save();
canvas.rotate(hour / 12.0f * 360.0f);
canvas.drawLine(-30, 0, radiusIn / 2.0f, 0, clockPaint);
canvas.restore();
//繪製分針
canvas.save();
canvas.rotate(minute / 60.0f * 360.0f);
canvas.drawLine(-30, 0, radiusIn * 0.7f, 0, clockPaint);
canvas.restore();
//繪製秒針
clockPaint.setColor(Color.parseColor("#fff2204d"));
canvas.save();
canvas.rotate(second / 60.0f * 360.0f);
canvas.drawLine(-30, 0, radiusIn * 0.85f, 0, clockPaint);
canvas.restore();
//繪製中心小圓點
clockPaint.setStyle(Paint.Style.FILL);
clockPaint.setColor(clockCenterColor);
canvas.drawCircle(0, 0, radiusIn / 20.0f, clockPaint);
}
複製程式碼
四、動畫效果
onDraw 方法只是完成了 View 的繪製,此處還需要思考如何令時鐘“動”起來。本 Demo 是通過 Handler 來設定定時任務的,當 View 處於可見狀態時就每隔一秒主動重新整理介面
為了避免記憶體洩漏問題,此處通過弱引用的形式來引用 ClockView
private static final int MSG_INVALIDATE = 10;
private static final class TimerHandler extends Handler {
private WeakReference<ClockView> clockViewWeakReference;
private TimerHandler(ClockView clockView) {
clockViewWeakReference = new WeakReference<>(clockView);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE: {
Log.e(TAG, "定時任務被觸發...");
ClockView view = clockViewWeakReference.get();
if (view != null) {
view.onTimeChanged();
view.invalidate();
sendEmptyMessageDelayed(MSG_INVALIDATE, 1000);
}
break;
}
}
}
}
private void onTimeChanged() {
time.setToNow();
minute = time.minute;
hour = time.hour + minute / 60.0f;
second = time.second;
}
複製程式碼
五、適用多時區
為了在系統時區改變時能夠進行相應的時間變化,此處還需要監聽系統的 Intent.ACTION_TIMEZONE_CHANGED 廣播
private final BroadcastReceiver timerBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null) {
switch (action) {
//監聽時區的變化
case Intent.ACTION_TIMEZONE_CHANGED: {
time = new Time(TimeZone.getTimeZone(intent.getStringExtra("time-zone")).getID());
break;
}
}
}
}
};
複製程式碼
在 View 可見的時候註冊廣播,不可見的時候就解除註冊
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.e(TAG, "onDetachedFromWindow");
stopTimer();
unregisterTimezoneAction();
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
Log.e(TAG, "onVisibilityChanged visibility: " + visibility);
if (visibility == View.VISIBLE) {
registerTimezoneAction();
startTimer();
} else {
stopTimer();
unregisterTimezoneAction();
}
}
private void startTimer() {
Log.e(TAG, "startTimer 開啟定時任務");
timerHandler.removeMessages(MSG_INVALIDATE);
timerHandler.sendEmptyMessage(MSG_INVALIDATE);
}
private void stopTimer() {
Log.e(TAG, "stopTimer 停止定時任務");
timerHandler.removeMessages(MSG_INVALIDATE);
}
private void registerTimezoneAction() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(timerBroadcast, filter);
}
private void unregisterTimezoneAction() {
getContext().unregisterReceiver(timerBroadcast);
}
複製程式碼