簡介
最近一直在學習自定義控件,為了鞏固所學實現了一個“鐘表”。以此來記錄下,免得以后忘掉。
很多時候我自定義控件都是選擇集成view或者已經存在的原生控件,然后進行擴展重寫其中的方法。今天我們要實現的鐘表,因為考慮到時動態的,所以打算用SurfaceView來實現。
效果圖
鐘表.gif
實現
根據效果圖可以看到,整個view就可以分為幾步走,首先剛才已經說了,是用SurfaceView進行實現,所以需要繼承SurfaceView,然后設置holder回調,并且要實現個線程進行刷新。其次就是鐘表的繪圖,最后就是一些對方的方法擴展。
- 繼承SurfaceView
- 鐘表繪制
- 擴展方法
- 調用展示
一、繼承SurfaceView
廢話就不多說了,和繼承activity一樣,然后設置回調,并實現線程,初始化畫筆等一些初始化操作,直接展示代碼:
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取當前時分秒
hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
minute = Calendar.getInstance().get(Calendar.MINUTE);
second = Calendar.getInstance().get(Calendar.SECOND);
//獲取holder對象,并設置回調
holder = getHolder();
holder.addCallback(this);
//初始化畫筆
paint = new Paint();
pointerPaint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
pointerPaint.setColor(Color.BLACK);
pointerPaint.setAntiAlias(true);
pointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
pointerPaint.setTextSize(22);
pointerPaint.setTextAlign(Paint.Align.CENTER);
//設置不可獲取焦點
setFocusable(false);
setFocusableInTouchMode(false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int desiredWidth, desiredHeight;
if (widthMode == MeasureSpec.EXACTLY) {
desiredWidth = widthSize;
} else {
desiredWidth = radius * 2 + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
desiredWidth = Math.min(widthSize, desiredWidth);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
desiredHeight = heightSize;
} else {
desiredHeight = radius * 2 + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
desiredHeight = Math.min(heightSize, desiredHeight);
}
}
// +4是為了設置默認的2px的內邊距,因為繪制時鐘的圓的畫筆設置的寬度是2px
setMeasuredDimension(canvasWidth = desiredWidth + 4, canvasHeight = desiredHeight + 4);
radius = (int) (Math.min(desiredWidth - getPaddingLeft() - getPaddingRight(), desiredHeight - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);
calculateLengths();
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
flag = true;
new Thread(this).start();
}
二、鐘表的繪制
相信都會這些基本操作,復雜點的就是對角度的計算。下面一起來看看繪制的過程。
- 獲取畫布并鎖定,移動坐標原點到畫布中心,
//獲取畫布,并鎖定
canvas = holder.lockCanvas();
if (canvas != null) {
//開始繪制,刷屏
canvas.drawColor(Color.WHITE);
//將坐標原點移動到去掉內邊距的畫布中心
canvas.translate(canvasWidth * 1.0f / 2 + getPaddingLeft() - getPaddingRight(), canvasHeight * 1.0f / 2 + getPaddingTop() - getPaddingBottom());
- 繪制圓形
//設置畫筆為2個寬度,繪制圓形
paint.setStrokeWidth(2f);canvas.drawCircle(0, 0, radius, paint);
- 繪制時秒刻度,其中繪制秒刻度的時候,時刻度要忽略掉,繪制數字。
//繪制時刻度,0-12個時刻,圓為360度,那么每個為30度
for (int i = 0; i < 12; i++) {
canvas.drawLine(0, radius, 0, radius - hourDegreeLength, paint);
canvas.rotate(30);
}
//繪制秒刻度,一共60秒,那么沒秒6度,并且時刻度已經繪制過了就不需要再繪制了
paint.setStrokeWidth(1.5f);
for (int i = 0; i < 60; i++) {
if (i % 5 != 0) {
canvas.drawLine(0, radius, 0, radius - secondDegreeLength, paint);
}
canvas.rotate(6);
}
//繪制數字
pointerPaint.setColor(Color.BLACK);
for (int i = 0; i < 12; i++) {
String number = (6 + i) < 12 ? String.valueOf(6 + i) : (6 + i) > 12 ? String.valueOf(i - 6) : "12";
canvas.drawText(number, 0, radius * 5.5f / 7, pointerPaint);
canvas.rotate(30);
}
//繪制上下午,判斷當前時間是否大于小于12
canvas.drawText(hour < 12 ? "AM" : "PM", 0, radius * 1.5f / 4, pointerPaint);
- 繪制時分秒指針
//繪制時針
Path path = new Path();
path.moveTo(0, 0);
int[] hourPointerCoordinates = getPointerCoordinates(hourPointerLength);
path.lineTo(hourPointerCoordinates[0], hourPointerCoordinates[1]);
path.lineTo(hourPointerCoordinates[2], hourPointerCoordinates[3]);
path.lineTo(hourPointerCoordinates[4], hourPointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + hour % 12 * 30 + minute * 1.0f / 60 * 30);
canvas.drawPath(path, pointerPaint);
canvas.restore();
//繪制分針
path.reset();
path.moveTo(0, 0);
int[] minutePointerCoordinates = getPointerCoordinates(minutePointerLength);
path.lineTo(minutePointerCoordinates[0], minutePointerCoordinates[1]);
path.lineTo(minutePointerCoordinates[2], minutePointerCoordinates[3]);
path.lineTo(minutePointerCoordinates[4], minutePointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + minute * 6);
canvas.drawPath(path, pointerPaint);
canvas.restore();
//繪制秒針
pointerPaint.setColor(Color.RED);
path.reset();
path.moveTo(0, 0);
int[] secondPointerCoordinates = getPointerCoordinates(secondPointerLength);
path.lineTo(secondPointerCoordinates[0], secondPointerCoordinates[1]);
path.lineTo(secondPointerCoordinates[2], secondPointerCoordinates[3]);
path.lineTo(secondPointerCoordinates[4], secondPointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + (second+1) * 6);
canvas.drawPath(path, pointerPaint);
canvas.restore();
三、擴展方法
public int getHour() {
return hour;
}
public void setHour(int hour) {
hour = Math.abs(hour) % 24;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
minute = Math.abs(minute) % 60;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
second = Math.abs(second) % 60;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public void setTime(Integer... time) {
if (time.length > 3) {
throw new IllegalArgumentException("the length of argument should bo less than 3");
}
if (time.length > 2)
setSecond(time[2]);
if (time.length > 1)
setMinute(time[1]);
if (time.length > 0)
setHour(time[0]);
}
//-----------------Setter and Getter end-------------------//
//************* interface *************
public void setOnTimeChangeListener(OnTimeChangeListener onTimeChangeListener) {
this.onTimeChangeListener = onTimeChangeListener;
}
/**
* 當時間改變的時候提供回調的接口
*/
public interface OnTimeChangeListener {
/**
* 時間發生改變時調用
*
* @param view 時間正在改變的view
* @param hour 改變后的小時時刻
* @param minute 改變后的分鐘時刻
* @param second 改變后的秒時刻
*/
void onTimeChange(View view, int hour, int minute, int second);
}
四、調用展示
xml就不在展示了,調用和其他的控件是一樣的。時間的改變可以通過OnTimeChangeListener 接口進行獲取。
clockView.setOnTimeChangeListener(new ClockView.OnTimeChangeListener() {
@Override
public void onTimeChange(View view, int hour, int minute, int second) {
time.setText(hour+":"+minute+":"+second);
}
});
最后奉上整個View的代碼。
/**
* 作者: Sunshine
* 時間: 2016/10/20.
* 郵箱: 44493547@qq.com
* 描述: 自定義鐘表,因為是動態view所以選擇集成SurfaceView比較好
*/
public class ClockView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private final static int DEFAULT_RADIUS = 200;
/**
* 半徑
*/
private int radius = DEFAULT_RADIUS;
/**
* 當前時間的時、分、秒
*/
private int hour, minute, second;
/**
* holder對象
*/
private SurfaceHolder holder;
/**
* 是否開始繪制
*/
private boolean flag;
/**
* 圓和刻度的畫筆
*/
private Paint paint;
/**
* 指針的畫筆
*/
private Paint pointerPaint;
/**
* 畫布的寬和高
*/
private int canvasWidth, canvasHeight;
/**
* 時、分刻度的長度
*/
private int hourDegreeLength, secondDegreeLength;
/**
* 時分秒指針的長度
*/
private int minutePointerLength, secondPointerLength, hourPointerLength;
private OnTimeChangeListener onTimeChangeListener;
private Canvas canvas;
public ClockView(Context context) {
this(context, null);
}
public ClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取當前時分秒
hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
minute = Calendar.getInstance().get(Calendar.MINUTE);
second = Calendar.getInstance().get(Calendar.SECOND);
//獲取holder對象,并設置回調
holder = getHolder();
holder.addCallback(this);
//初始化畫筆
paint = new Paint();
pointerPaint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
pointerPaint.setColor(Color.BLACK);
pointerPaint.setAntiAlias(true);
pointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
pointerPaint.setTextSize(22);
pointerPaint.setTextAlign(Paint.Align.CENTER);
//設置不可獲取焦點
setFocusable(false);
setFocusableInTouchMode(false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int desiredWidth, desiredHeight;
if (widthMode == MeasureSpec.EXACTLY) {
desiredWidth = widthSize;
} else {
desiredWidth = radius * 2 + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
desiredWidth = Math.min(widthSize, desiredWidth);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
desiredHeight = heightSize;
} else {
desiredHeight = radius * 2 + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
desiredHeight = Math.min(heightSize, desiredHeight);
}
}
// +4是為了設置默認的2px的內邊距,因為繪制時鐘的圓的畫筆設置的寬度是2px
setMeasuredDimension(canvasWidth = desiredWidth + 4, canvasHeight = desiredHeight + 4);
radius = (int) (Math.min(desiredWidth - getPaddingLeft() - getPaddingRight(), desiredHeight - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);
calculateLengths();
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
flag = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
flag = false;
}
@Override
public void run() {
long start, end;
while (flag) {
start = System.currentTimeMillis();
draw();
logic();
end = System.currentTimeMillis();
try {
if (end - start < 1000) {
Thread.sleep(1000 - (end - start));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 時間進度的邏輯
*/
private void logic() {
second++;
if (second == 60) {
second = 0;
minute++;
if (minute == 60) {
minute = 0;
hour++;
if (hour == 24) {
hour = 0;
}
}
}
handler.sendEmptyMessage(0);
}
/**
* 繪制
*/
private void draw() {
try {
//獲取畫布,并鎖定
canvas = holder.lockCanvas();
if (canvas != null) {
//開始繪制,刷屏
canvas.drawColor(Color.WHITE);
//將坐標原點移動到去掉內邊距的畫布中心
canvas.translate(canvasWidth * 1.0f / 2 + getPaddingLeft() - getPaddingRight(), canvasHeight * 1.0f / 2 + getPaddingTop() - getPaddingBottom());
//設置畫筆為2個寬度,繪制圓形
paint.setStrokeWidth(2f);
canvas.drawCircle(0, 0, radius, paint);
//繪制時刻度,0-12個時刻,圓為360度,那么每個為30度
for (int i = 0; i < 12; i++) {
canvas.drawLine(0, radius, 0, radius - hourDegreeLength, paint);
canvas.rotate(30);
}
//繪制秒刻度,一共60秒,那么沒秒6度,并且時刻度已經繪制過了就不需要再繪制了
paint.setStrokeWidth(1.5f);
for (int i = 0; i < 60; i++) {
if (i % 5 != 0) {
canvas.drawLine(0, radius, 0, radius - secondDegreeLength, paint);
}
canvas.rotate(6);
}
//繪制數字
pointerPaint.setColor(Color.BLACK);
for (int i = 0; i < 12; i++) {
String number = (6 + i) < 12 ? String.valueOf(6 + i) : (6 + i) > 12 ? String.valueOf(i - 6) : "12";
canvas.drawText(number, 0, radius * 5.5f / 7, pointerPaint);
canvas.rotate(30);
}
//繪制上下午,判斷當前時間是否大于小于12
canvas.drawText(hour < 12 ? "AM" : "PM", 0, radius * 1.5f / 4, pointerPaint);
//繪制指針
Path path = new Path();
path.moveTo(0, 0);
int[] hourPointerCoordinates = getPointerCoordinates(hourPointerLength);
path.lineTo(hourPointerCoordinates[0], hourPointerCoordinates[1]);
path.lineTo(hourPointerCoordinates[2], hourPointerCoordinates[3]);
path.lineTo(hourPointerCoordinates[4], hourPointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + hour % 12 * 30 + minute * 1.0f / 60 * 30);
canvas.drawPath(path, pointerPaint);
canvas.restore();
//繪制分針
path.reset();
path.moveTo(0, 0);
int[] minutePointerCoordinates = getPointerCoordinates(minutePointerLength);
path.lineTo(minutePointerCoordinates[0], minutePointerCoordinates[1]);
path.lineTo(minutePointerCoordinates[2], minutePointerCoordinates[3]);
path.lineTo(minutePointerCoordinates[4], minutePointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + minute * 6);
canvas.drawPath(path, pointerPaint);
canvas.restore();
//繪制秒針
pointerPaint.setColor(Color.RED);
path.reset();
path.moveTo(0, 0);
int[] secondPointerCoordinates = getPointerCoordinates(secondPointerLength);
path.lineTo(secondPointerCoordinates[0], secondPointerCoordinates[1]);
path.lineTo(secondPointerCoordinates[2], secondPointerCoordinates[3]);
path.lineTo(secondPointerCoordinates[4], secondPointerCoordinates[5]);
path.close();
canvas.save();
canvas.rotate(180 + (second+1) * 6);
canvas.drawPath(path, pointerPaint);
canvas.restore();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0://改變時間
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(ClockView.this, hour, minute, second);
}
break;
}
}
};
/**
* 獲取指針坐標
*
* @param pointerLength 指針長度
* @return int[]{x1,y1,x2,y2,x3,y3}
*/
private int[] getPointerCoordinates(int pointerLength) {
int y = (int) (pointerLength * 3.0f / 4);
int x = (int) (y * Math.tan(Math.PI / 180 * 5));
return new int[]{-x, y, 0, pointerLength, x, y};
}
/**
* 計算指針和刻度的長度
*/
private void calculateLengths() {
hourDegreeLength = (int) (radius * 1.0f / 7);
secondDegreeLength = (int) (hourDegreeLength * 1.0f / 2);
// hour : minute : second = 1 : 1.25 : 1.5
hourPointerLength = (int) (radius * 1.0 / 2);
minutePointerLength = (int) (hourPointerLength * 1.25f);
secondPointerLength = (int) (hourPointerLength * 1.5f);
}
//-----------------Setter and Getter start-----------------//
public int getHour() {
return hour;
}
public void setHour(int hour) {
hour = Math.abs(hour) % 24;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
minute = Math.abs(minute) % 60;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
second = Math.abs(second) % 60;
if (onTimeChangeListener != null) {
onTimeChangeListener.onTimeChange(this, hour, minute, second);
}
}
public void setTime(Integer... time) {
if (time.length > 3) {
throw new IllegalArgumentException("the length of argument should bo less than 3");
}
if (time.length > 2)
setSecond(time[2]);
if (time.length > 1)
setMinute(time[1]);
if (time.length > 0)
setHour(time[0]);
}
//-----------------Setter and Getter end-------------------//
//************* interface *************
public void setOnTimeChangeListener(OnTimeChangeListener onTimeChangeListener) {
this.onTimeChangeListener = onTimeChangeListener;
}
/**
* 當時間改變的時候提供回調的接口
*/
public interface OnTimeChangeListener {
/**
* 時間發生改變時調用
*
* @param view 時間正在改變的view
* @param hour 改變后的小時時刻
* @param minute 改變后的分鐘時刻
* @param second 改變后的秒時刻
*/
void onTimeChange(View view, int hour, int minute, int second);
}
}