目錄
- 1.效果圖
- 2.分析
- 3.SurfaceView注意事項(xiàng)
- 4.實(shí)戰(zhàn)
- 5.總結(jié)
- 6.Tips
1.廢話不多說先上圖
錄制的可能略卡,三次時(shí)間圖片是因?yàn)槲曳謩e點(diǎn)了下面三個(gè)按鈕的效果妓羊,并非bug
2.分析
-
觀看上圖咱們需要繪制的有:
- 時(shí)針
- 分針
- 秒針
- 時(shí)刻度
- 秒刻度
- 儀表盤上的數(shù)字
- 上下午區(qū)分標(biāo)識(shí)(AM/PM)
-
View與Surface的取舍:
- View一般用于繪制靜態(tài)頁面或者界面元素跟隨用戶的操作(點(diǎn)擊史翘、拖拽等)而被動(dòng)的改變位置铝穷、大小等
- SurfaceView一般用于無需用戶操作,界面元素就需要不斷的刷新的情況(例如打飛機(jī)游戲不斷移動(dòng)的背景)
- 通過以上兩條可以確定SurfaceView正好符合我們的需求
3.SurfaceView注意事項(xiàng)
-
如何使用SurfaceView
- 1.繼承SurfaceView
- 2.實(shí)現(xiàn)SurfaceHolder.Callback接口
- surfaceCreated:Surface創(chuàng)建后調(diào)用临燃,一般做一些初始化工作
- surfaceChanged:Surface狀態(tài)發(fā)生變化時(shí)調(diào)用(例如大小)
- surfaceDestroyed:Surface銷毀時(shí)調(diào)用,一般在這里結(jié)束繪制線程
- 3.SurfaceHolder:控制Surface的類俱病,得到畫布、提交畫布袱结、回調(diào)等
- 4.繪制和邏輯
SurfaceView的寫法
public class MyView extends SurfaceView implements SurfaceHolder.Callback,Runnable {
private SurfaceHolder mHolder;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHolder = getHolder();
mHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void run() {
while (true) {
logic();
draw();
}
}
/**
* 邏輯操作
*/
private void logic() {
}
/**
* 繪制操作
*/
private void draw() {
}
}
這是一般性寫法亮隙,不過有些人應(yīng)該已經(jīng)發(fā)現(xiàn)了我們線程里跑的是一個(gè)while(true)的無限死循化,因此往往我們會(huì)增加一個(gè)標(biāo)識(shí)位在surface銷毀時(shí)置false垢夹。修改后部分代碼如下:
private boolean flag;
@Override
public void surfaceCreated(SurfaceHolder holder) {
flag = true;
mThread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
flag = false;
}
@Override
public void run() {
while (flag) {
logic();
draw();
}
}
當(dāng)然了這樣還是有缺陷的溢吻,因?yàn)楫?dāng)flag==true時(shí),mThread里面的邏輯操作和繪制操作就在無限運(yùn)行了棚饵∶喝梗可想而知,如果是那樣的話那么我們這里的時(shí)鐘指針在你眼前飛速的轉(zhuǎn)動(dòng)噪漾,由于gif錄制工具不夠強(qiáng)大錄下的動(dòng)圖根本開不出指針飛速旋轉(zhuǎn)的效果這里就不提供圖了硼砰,有興趣的同學(xué)可以自己試一下,也可以腦補(bǔ)欣硼。题翰。。诈胜。豹障。。焦匈。血公。。缓熟。累魔。
因此我們就需要對(duì)線程加以限制摔笤,具體如下:
@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();
}
}
}
這里的1000指的是每隔1S刷新一次界面,因?yàn)槲覀兊氖冀K的最小單位是秒
4.廢話啰嗦完了 咱們開始實(shí)戰(zhàn)
- step1:通過2的分析垦写,定義需要的屬性
// 默認(rèn)半徑
private static final int DEFAULT_RADIUS = 200;
private SurfaceHolder mHolder;
private Canvas mCanvas;
private Thread mThread;
private boolean flag;
// 圓和刻度的畫筆
private Paint mPaint;
// 指針畫筆
private Paint mPointerPaint;
// 畫布的寬高
private int mCanvasWidth, mCanvasHeight;
// 時(shí)鐘半徑
private int mRadius = DEFAULT_RADIUS;
// 秒針長度
private int mSecondPointerLength;
// 分針長度
private int mMinutePointerLength;
// 時(shí)針長度
private int mHourPointerLength;
// 時(shí)刻度長度
private int mHourDegreeLength;
// 秒刻度
private int mSecondDegreeLength;
// 時(shí)鐘顯示的時(shí)吕世、分、秒
private int mHour, mMinute, mSecond;
- step2:初始化操作
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
mMinute = Calendar.getInstance().get(Calendar.MINUTE);
mSecond = Calendar.getInstance().get(Calendar.SECOND);
mHolder = getHolder();
mHolder.addCallback(this);
mThread = new Thread(this);
mPaint = new Paint();
mPointerPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPointerPaint.setColor(Color.BLACK);
mPointerPaint.setAntiAlias(true);
mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPointerPaint.setTextSize(22);
mPointerPaint.setTextAlign(Paint.Align.CENTER);
setFocusable(true);
setFocusableInTouchMode(true);
}
- step3:測(cè)量
@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 = mRadius * 2 + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
desiredWidth = Math.min(widthSize, desiredWidth);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
desiredHeight = heightSize;
} else {
desiredHeight = mRadius * 2 + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
desiredHeight = Math.min(heightSize, desiredHeight);
}
}
// +4是為了設(shè)置默認(rèn)的2px的內(nèi)邊距梯投,因?yàn)槔L制時(shí)鐘的圓的畫筆設(shè)置的寬度是2px
setMeasuredDimension(mCanvasWidth = desiredWidth + 4, mCanvasHeight = desiredHeight + 4);
mRadius = (int) (Math.min(desiredWidth - getPaddingLeft() - getPaddingRight(),
desiredHeight - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);
calculateLengths();
}
/**
* 計(jì)算指針和刻度長度
*/
private void calculateLengths() {
// 這里我們定義時(shí)刻度長度為半徑的1/7
mHourDegreeLength = (int) (mRadius * 1.0f / 7);
// 秒刻度長度為時(shí)刻度長度的一半
mSecondDegreeLength = (int) (mHourDegreeLength * 1.0f / 2);
// 時(shí)針長度為半徑一半
// 指針長度比 hour : minute : second = 1 : 1.25 : 1.5
mHourPointerLength = (int) (mRadius * 1.0 / 2);
mMinutePointerLength = (int) (mHourPointerLength * 1.25f);
mSecondPointerLength = (int) (mHourPointerLength * 1.5f);
}
測(cè)量的前面部分代碼基本都是一個(gè)模式來寫的命辖,需要注意的是當(dāng)測(cè)量模式不是Exectly時(shí)的處理,想要了解這一塊同學(xué)的可以去鴻神的博客學(xué)習(xí)
- step4:繪制
/**
* 繪制分蓖,這部分代碼基本固定
*/
private void draw() {
try {
mCanvas = mHolder.lockCanvas(); // 得到畫布
if (mCanvas != null) {
// 在這里繪制內(nèi)容
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mCanvas != null) {
// 提交畫布尔艇,否則什么都看不見
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
現(xiàn)在開始具體的繪制內(nèi)容(畫什么由畫布決定,怎么畫由畫筆決定咆疗,這也就是我們上面給畫筆設(shè)置一系列屬性的原因):
// 1.將坐標(biāo)系原點(diǎn)移至去除內(nèi)邊距后的畫布中心
// 默認(rèn)在畫布左上角漓帚,這樣做是為了更方便的繪制
mCanvas.translate(mCanvasWidth * 1.0f / 2 + getPaddingLeft() - getPaddingRight(),mCanvasHeight * 1.0f / 2 + getPaddingTop() - getPaddingBottom());
// 2.繪制圓盤
mPaint.setStrokeWidth(2f); // 畫筆設(shè)置2個(gè)像素的寬度
mCanvas.drawCircle(0, 0, mRadius, mPaint); // 到這一步就能知道第一步的好處了,否則害的去計(jì)算園的中心點(diǎn)坐標(biāo)
// 3.繪制時(shí)刻度
for (int i = 0; i < 12; i++) {
mCanvas.drawLine(0, mRadius, 0, mRadius - mHourDegreeLength, mPaint);
mCanvas.rotate(30); // 360°平均分成12份午磁,每份30°
}
// 4.繪制秒刻度
mPaint.setStrokeWidth(1.5f);
for (int i = 0; i < 60; i++) {
//時(shí)刻度繪制過的區(qū)域不在繪制
if (i % 5 != 0) {
mCanvas.drawLine(0, mRadius, 0, mRadius - mSecondDegreeLength, mPaint);
}
mCanvas.rotate(6); // 360°平均分成60份尝抖,每份6°
}
// 5.繪制數(shù)字
mPointerPaint.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";
mCanvas.drawText(number, 0, mRadius * 5.5f / 7, mPointerPaint);
mCanvas.rotate(30);
}
// 6.繪制上下午
mCanvas.drawText(mHour < 12 ? "AM" : "PM", 0, mRadius * 1.5f / 4, mPointerPaint);
// 7.繪制時(shí)針
Path path = new Path();
path.moveTo(0, 0);
int[] hourPointerCoordinates = getPointerCoordinates(mHourPointerLength);
path.lineTo(hourPointerCoordinates[0], hourPointerCoordinates[1]);
path.lineTo(hourPointerCoordinates[2], hourPointerCoordinates[3]);
path.lineTo(hourPointerCoordinates[4], hourPointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mHour % 12 * 30 + mMinute * 1.0f / 60 * 30);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();
// 8.繪制分針
path.reset();
path.moveTo(0, 0);
int[] minutePointerCoordinates = getPointerCoordinates(mMinutePointerLength);
path.lineTo(minutePointerCoordinates[0], minutePointerCoordinates[1]);
path.lineTo(minutePointerCoordinates[2], minutePointerCoordinates[3]);
path.lineTo(minutePointerCoordinates[4], minutePointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mMinute * 6);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();
// 9.繪制秒針
mPointerPaint.setColor(Color.RED);
path.reset();
path.moveTo(0, 0);
int[] secondPointerCoordinates = getPointerCoordinates(mSecondPointerLength);
path.lineTo(secondPointerCoordinates[0], secondPointerCoordinates[1]);
path.lineTo(secondPointerCoordinates[2], secondPointerCoordinates[3]);
path.lineTo(secondPointerCoordinates[4], secondPointerCoordinates[5]);
path.close();
mCanvas.save();
mCanvas.rotate(180 + mSecond * 6);
mCanvas.drawPath(path, mPointerPaint);
mCanvas.restore();
這里比較難的可能就是指針的繪制,因?yàn)槲覀兊闹羔樖莻€(gè)規(guī)則形狀迅皇,其中g(shù)etPointerCoordinates便是得到這個(gè)不規(guī)則形狀的3個(gè)定點(diǎn)坐標(biāo)昧辽,有興趣的同學(xué)可以去研究一下我的邏輯,也可以定義你自己的邏輯登颓。我的邏輯如下(三角函數(shù)學(xué)的號(hào)的同學(xué)應(yīng)該一眼就能看懂):
/**
* 獲取指針坐標(biāo)
*
* @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};
}
-
step5:邏輯
這里邏輯可想而知每個(gè)秒搅荞,秒數(shù)+1到60的時(shí)候歸0,同時(shí)分鐘數(shù)+1框咙,
分鐘數(shù)到60的時(shí)候歸0咕痛,小時(shí)數(shù)+1,小時(shí)數(shù)到24的時(shí)候歸0.
/**
* 邏輯
*/
private void logic() {
mSecond++;
if (mSecond == 60) {
mSecond = 0;
mMinute++;
if (mMinute == 60) {
mMinute = 0;
mHour++;
if (mHour == 24) {
mHour = 0;
}
}
}
}
- step6:裝逼的時(shí)刻到了
運(yùn)行來看一下效果
我靠什么情況為什么畫出來是這么個(gè)東西
這里就是要值得大家注意的了喇嘱,使用surface繪制的時(shí)候一定要刷屏茉贡,所謂的刷屏在每次繪制前畫一個(gè)什么都沒有圖層在上次畫出來的東西上面蓋住再去畫本次實(shí)現(xiàn)也很簡單只需要在繪制的第一行加上一句就行。
我這里是
//刷屏
mCanvas.drawColor(Color.WHITE);
這里的背景也可以通過自定義屬性來自定義的者铜。不知道的同學(xué)仍然可以去鴻神的博客學(xué)習(xí)
5.總結(jié)
主要涉及知識(shí)點(diǎn):
- View和SurfaceView的取舍
- SurfaceView的使用
- Canvas使用
- 坐標(biāo)系的靈活運(yùn)用
6.Tips
1.圖上的點(diǎn)擊按鈕改變時(shí)間和改變textView上的時(shí)候只是定義了一個(gè)回調(diào)接口腔丧,這里不再贅述
2.詳情見源碼
3.本文同步發(fā)布CSDN