零、前言
前幾天介紹了一大堆Android的Canvas,Paint,Path的API,接下來將是靈活地使用他們
今天帶來的是一個手錶的繪製,經過本篇的洗禮,相信你會對Canvas的圖層
概念有更深刻的理解
至於表的美醜不是本文的重點,本文只有一個目的,就是理清Canvas的save和restore的意義
一、準備工作
1.新建類繼承View
public class TolyClockView extends View {
public TolyClockView(Context context) {
this(context, null);
}
public TolyClockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
//TODO 初始化
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//TODO 繪製
}
複製程式碼
2.分析一下
一般我們都會這樣去自定義一個View,但很少人會有
圖層
這個概念,畢竟我們都是敲程式碼的
如下圖,一開始是一個x,y軸在頂點的圖層,如果你不用save(),那你始終都在這個圖層,圖層棧始終只有一個
3.下面在這個介面上繪製本人專用座標系:(已封裝成工具,附在文尾
)
網格和座標系屬於輔助性的工具,繪製起來比較多,所以使用Picture錄製,在init()裡初始化
Picture在onDraw裡繪製高效些,區別就像準備一車磚蓋房子和造一塊才磚蓋一下房子
//成員變數
private Picture mPictureGrid;//網格Canvas元件
private Point mCoo = new Point(500, 800);//座標系原點
private Picture mPictureCoo;//座標系Canvas元件
//init()中
mPictureGrid = HelpDraw.getGrid(getContext());
mPictureCoo = HelpDraw.getCoo(getContext(), mCoo);
//初始化畫筆
mMainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMainPaint.setStyle(Paint.Style.STROKE);
mMainPaint.setStrokeCap(Paint.Cap.ROUND);
//onDraw裡
canvas.drawPicture(mPictureGrid);
canvas.drawPicture(mPictureCoo);
複製程式碼
正如API字面上的意思,在canvas上將網格和座標系兩張
圖片
繪製出來,如下圖:
二、繪製邏輯
準備工作做好了,下面要到正題了
1.onDraw裡
canvas.save();//儲存先前狀態(相當於在另一個圖層操作)
canvas.translate(mCoo.x, mCoo.y);//將畫布定點平移到繪製的座標系中心
canvas.restore();//合併到root圖層
複製程式碼
2.看一下這兩句翻譯在圖上是什麼意思:
一旦canvas.save(),相當於新建了一個圖層(黑色虛線所示),
然後canvas.translate(mCoo.x, mCoo.y)將新建的圖層向右和向下移動
新建的圖層的好處:只有棧頂的圖層才能操作(如Canvas移動時,root圖層並沒有動,這正是我們想要的)
3.繪製外圈破碎的圓:drawBreakCircle(canvas)
/**
* 繪製破碎的圓
* @param canvas
*/
private void drawBreakCircle(Canvas canvas) {
for (int i = 0; i < 4; i++) {
canvas.save();//儲存先前狀態(相當於在另一個圖層操作)
canvas.rotate(90 * i);
mMainPaint.setStrokeWidth(8);
mMainPaint.setColor(Color.parseColor("#D5D5D5"));
//在-350, -350, 350, 350的矩形區域,從10°掃70°繪製圓弧
canvas.drawArc(
-350, -350, 350, 350,
10, 70, false, mMainPaint);
canvas.restore();//恢復先前狀態(相當於將圖層和前一圖層合併)
}
}
複製程式碼
先看i=0時:
由於save了,前面的圖層被鎖定,相當於在另一個圖層操作
canvas.restore()
呼叫後,
圖層2將它的結果給了圖層1,揮揮衣袖,不帶走一片雲彩
,出棧了
先看i=1時:
由於save了,前面的圖層被鎖定,相當於在另一個圖層操作
這裡canvas.rotate(90 * 1)相當於當前圖層轉了90°,如圖:
注意
:我只將座標軸的第一象限塗色,canvas圖層是一個無限的面,canvas寬高只是限制顯示,
旋轉、平移、縮放等的關鍵在於座標軸的變換,旋轉90°相當於座標軸轉了90°
canvas.restore()
呼叫後,
圖層2將它的結果給了圖層1,揮揮衣袖,不帶走一片雲彩
,出棧了
經過這兩個圖層的演示,想必你應該明白圖層的作用了吧。
最後畫完之後,圖層全合併到root
4.繪製小點
畫60個點(小線),每逢5變長,也就是畫直線,每次將畫布旋轉360/60=6°
private void drawDot(Canvas canvas) {
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
canvas.save();
canvas.rotate(30 * i);
mMainPaint.setStrokeWidth(8);
mMainPaint.setColor(ColUtils.randomRGB());
canvas.drawLine(250, 0, 300, 0, mMainPaint);
mMainPaint.setStrokeWidth(10);
mMainPaint.setColor(Color.BLACK);
canvas.drawPoint(250, 0, mMainPaint);
canvas.restore();
} else {
canvas.save();
canvas.rotate(6 * i);
mMainPaint.setStrokeWidth(4);
mMainPaint.setColor(Color.BLUE);
canvas.drawLine(280, 0, 300, 0, mMainPaint);
canvas.restore();
}
}
}
複製程式碼
5.繪製時針:
/**
* 繪製時針
*
* @param canvas
*/
private void drawH(Canvas canvas) {
canvas.save();
canvas.rotate(40);
mMainPaint.setColor(Color.parseColor("#8FC552"));
mMainPaint.setStrokeCap(Paint.Cap.ROUND);
mMainPaint.setStrokeWidth(8);
canvas.drawLine(0, 0, 150, 0, mMainPaint);
canvas.restore();
}
複製程式碼
6.繪製分針:
/**
* 繪製分針
* @param canvas
* @param deg
*/
private void drawM(Canvas canvas) {
canvas.save();
canvas.rotate(120);
mMainPaint.setColor(Color.parseColor("#87B953"));
mMainPaint.setStrokeWidth(8);
canvas.drawLine(0, 0, 200, 0, mMainPaint);
mMainPaint.setColor(Color.GRAY);
mMainPaint.setStrokeWidth(30);
canvas.drawPoint(0, 0, mMainPaint);
canvas.restore();
}
複製程式碼
7.繪製秒針
/**
* 繪製秒針
*
* @param canvas
* @param deg
*/
private void drawS(Canvas canvas, float deg) {
mMainPaint.setStyle(Paint.Style.STROKE);
mMainPaint.setColor(Color.parseColor("#6B6B6B"));
mMainPaint.setStrokeWidth(8);
mMainPaint.setStrokeCap(Paint.Cap.SQUARE);
canvas.save();
canvas.rotate(deg);
canvas.save();
canvas.rotate(45);
//使用path繪製:在init裡初始化一下就行了
mMainPath.addArc(-25, -25, 25, 25, 0, 240);
canvas.drawPath(mMainPath, mMainPaint);
canvas.restore();
mMainPaint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawLine(-25, 0, -50, 0, mMainPaint);
mMainPaint.setStrokeWidth(2);
mMainPaint.setColor(Color.BLACK);
canvas.drawLine(0, 0, 320, 0, mMainPaint);
mMainPaint.setStrokeWidth(15);
mMainPaint.setColor(Color.parseColor("#8FC552"));
canvas.drawPoint(0, 0, mMainPaint);
canvas.restore();
}
複製程式碼
8.新增文字
/**
* 新增文字
* @param canvas
*/
private void drawText(Canvas canvas) {
mMainPaint.setTextSize(60);
mMainPaint.setStrokeWidth(5);
mMainPaint.setStyle(Paint.Style.FILL);
mMainPaint.setTextAlign(Paint.Align.CENTER);
mMainPaint.setColor(Color.BLUE);
canvas.drawText("Ⅲ", 350, 30, mMainPaint);
canvas.drawText("Ⅵ", 0, 350 + 30, mMainPaint);
canvas.drawText("Ⅸ", -350, 30, mMainPaint);
canvas.drawText("Ⅻ", 0, -350 + 30, mMainPaint);
//使用外接字型放在assets目錄下
Typeface myFont = Typeface.createFromAsset(getContext().getAssets(), "CHOPS.TTF");
mMainPaint.setTypeface(myFont);
mMainPaint.setTextSize(70);
canvas.drawText("Toly", 0, -150, mMainPaint);
}
複製程式碼
好了,靜態效果實現了,現在讓它動起來吧
三、讓表動起來
1.顯示當前時間:
表的旋轉角度由每個針繪製是的
canvas.rotate(XXX);
決定,
那麼動態改變旋轉的角度不就行了嗎! 看下面一道數學題:
11:12:45秒,時針、分針、秒針的指標各與中心水平線的夾角?
答:
秒針:45 / 60.f * 360 - 90
分針:12 / 60.f * 360 - 90 + 45 / 60.f * 1
時針:11 / 12.f * 360 - 90 + 12 / 60.f * 30 + 45 / 3600.f * 30
複製程式碼
2.動態更新角度:繪製指標的三個函式,加角度引數
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int min = calendar.get(Calendar.MINUTE);
int sec = calendar.get(Calendar.SECOND);
drawS(canvas, sec / 60.f * 360 - 90);
drawM(canvas, min / 60.f * 360 - 90 + sec / 60.f);
drawH(canvas, hour / 12.f * 360 - 90 + min / 60.f * 30 + sec / 3600.f * 30);
複製程式碼
3.現在每次進來,都會更新時間了,怎麼自動更新呢?
迴圈的黃金搭檔:
Handler + Timer
public class ClockActivity extends AppCompatActivity {
/**
* 新建Handler
*/
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mView.invalidate();//處理:重新整理檢視
}
};
private View mView;
private Timer timer = new Timer();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toly_clock);
ButterKnife.bind(this);
mView = findViewById(R.id.id_toly_clock);
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
mHandler.sendEmptyMessage(0);//傳送訊息
}
};
//定時任務
timer.schedule(timerTask, 0, 1000);
}
}
複製程式碼
ok,完結散花(分析圖畫的真要命...)
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-8 | Android原生繪圖之一起畫個表 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
附錄:網格+座標系繪製工具:
1.HelpDraw
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/5 0005:8:43<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:輔助畫布
*/
public class HelpDraw {
/**
* 獲取螢幕尺寸
*/
public static Point getWinSize(Context context) {
Point point = new Point();
Utils.loadWinSize(context, point);
return point;
}
/**
* 繪製網格
*/
public static Picture getGrid(Context context) {
return getGrid(getWinSize(context));
}
/**
* 繪製座標系
*/
public static Picture getCoo(Context context, Point coo) {
return getCoo(coo, getWinSize(context));
}
/**
* 繪製網格
*
* @param winSize 螢幕尺寸
*/
private static Picture getGrid(Point winSize) {
Picture picture = new Picture();
Canvas recording = picture.beginRecording(winSize.x, winSize.y);
//初始化網格畫筆
Paint paint = new Paint();
paint.setStrokeWidth(2);
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.STROKE);
//設定虛線效果new float[]{可見長度, 不可見長度},偏移值
paint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
recording.drawPath(HelpPath.gridPath(50, winSize), paint);
return picture;
}
/**
* 繪製座標系
*
* @param coo 座標系原點
* @param winSize 螢幕尺寸
*/
private static Picture getCoo(Point coo, Point winSize) {
Picture picture = new Picture();
Canvas recording = picture.beginRecording(winSize.x, winSize.y);
//初始化網格畫筆
Paint paint = new Paint();
paint.setStrokeWidth(4);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
//設定虛線效果new float[]{可見長度, 不可見長度},偏移值
paint.setPathEffect(null);
//繪製直線
recording.drawPath(HelpPath.cooPath(coo, winSize), paint);
//左箭頭
recording.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y - 20, paint);
recording.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y + 20, paint);
//下箭頭
recording.drawLine(coo.x, winSize.y, coo.x - 20, winSize.y - 40, paint);
recording.drawLine(coo.x, winSize.y, coo.x + 20, winSize.y - 40, paint);
//為座標系繪製文字
drawText4Coo(recording, coo, winSize, paint);
return picture;
}
/**
* 為座標系繪製文字
*
* @param canvas 畫布
* @param coo 座標系原點
* @param winSize 螢幕尺寸
* @param paint 畫筆
*/
private static void drawText4Coo(Canvas canvas, Point coo, Point winSize, Paint paint) {
//繪製文字
paint.setTextSize(50);
canvas.drawText("x", winSize.x - 60, coo.y - 40, paint);
canvas.drawText("y", coo.x - 40, winSize.y - 60, paint);
paint.setTextSize(25);
//X正軸文字
for (int i = 1; i < (winSize.x - coo.x) / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(100 * i + "", coo.x - 20 + 100 * i, coo.y + 40, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x + 100 * i, coo.y, coo.x + 100 * i, coo.y - 10, paint);
}
//X負軸文字
for (int i = 1; i < coo.x / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(-100 * i + "", coo.x - 20 - 100 * i, coo.y + 40, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x - 100 * i, coo.y, coo.x - 100 * i, coo.y - 10, paint);
}
//y正軸文字
for (int i = 1; i < (winSize.y - coo.y) / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(100 * i + "", coo.x + 20, coo.y + 10 + 100 * i, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x, coo.y + 100 * i, coo.x + 10, coo.y + 100 * i, paint);
}
//y負軸文字
for (int i = 1; i < coo.y / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(-100 * i + "", coo.x + 20, coo.y + 10 - 100 * i, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x, coo.y - 100 * i, coo.x + 10, coo.y - 100 * i, paint);
}
}
}
複製程式碼
2.HelpPath
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/5 0005:8:05<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:輔助分析路徑
*/
public class HelpPath {
/**
* 繪製網格:注意只有用path才能繪製虛線
*
* @param step 小正方形邊長
* @param winSize 螢幕尺寸
*/
public static Path gridPath(int step, Point winSize) {
Path path = new Path();
for (int i = 0; i < winSize.y / step + 1; i++) {
path.moveTo(0, step * i);
path.lineTo(winSize.x, step * i);
}
for (int i = 0; i < winSize.x / step + 1; i++) {
path.moveTo(step * i, 0);
path.lineTo(step * i, winSize.y);
}
return path;
}
/**
* 座標系路徑
*
* @param coo 座標點
* @param winSize 螢幕尺寸
* @return 座標系路徑
*/
public static Path cooPath(Point coo, Point winSize) {
Path path = new Path();
//x正半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(winSize.x, coo.y);
//x負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x - winSize.x, coo.y);
//y負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x, coo.y - winSize.y);
//y負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x, winSize.y);
return path;
}
}
複製程式碼
3.Utils
public class Utils {
/**
* 獲得螢幕高度
*
* @param ctx 上下文
* @param winSize 螢幕尺寸
*/
public static void loadWinSize(Context ctx, Point winSize) {
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
if (wm != null) {
wm.getDefaultDisplay().getMetrics(outMetrics);
}
winSize.x = outMetrics.widthPixels;
winSize.y = outMetrics.heightPixels;
}
}
複製程式碼