Android原生繪圖之一起畫個表
零、前言
前幾天介紹了一大堆Android的Canvas,Paint,Path的API,接下來將是靈活地使用他們
今天帶來的是一個手錶的繪製,經過本篇的洗禮,相信你會對Canvas的 圖層
概念有更深刻的理解
至於表的美醜不是本文的重點,本文只有一個目的,就是理清Canvas的save和restore的意義

表.gif
一、準備工作
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(),那你始終都在這個圖層,圖層棧始終只有一個

開始繪製時.png
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上將網格和座標系兩張 圖片
繪製出來,如下圖:

繪製座標系時.png
二、繪製邏輯
準備工作做好了,下面要到正題了
1.onDraw裡
canvas.save();//儲存先前狀態(相當於在另一個圖層操作) canvas.translate(mCoo.x, mCoo.y);//將畫布定點平移到繪製的座標系中心 canvas.restore();//合併到root圖層
2.看一下這兩句翻譯在圖上是什麼意思:
一旦canvas.save(),相當於新建了一個圖層(黑色虛線所示),
然後canvas.translate(mCoo.x, mCoo.y)將新建的圖層向左和向下移動
新建的圖層的好處:只有棧頂的圖層才能操作(如Canvas移動時,root圖層並沒有動,這正是我們想要的)

save和translate.png
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了,前面的圖層被鎖定,相當於在另一個圖層操作

繪製碎圓.png
canvas.restore()
呼叫後,
圖層2將它的結果給了圖層1, 揮揮衣袖,不帶走一片雲彩
,出棧了

繪製碎圓2.png
先看i=1時:
由於save了,前面的圖層被鎖定,相當於在另一個圖層操作
這裡canvas.rotate(90 * 1)相當於當前圖層轉了90°,如圖:
注意
:我只將座標軸的第一象限塗色,canvas圖層是一個無限的面,canvas寬高只是限制顯示,
旋轉、平移、縮放等的關鍵在於座標軸的變換,旋轉90°相當於座標軸轉了90°

繪製碎圓3.png
canvas.restore()
呼叫後,
圖層2將它的結果給了圖層1, 揮揮衣袖,不帶走一片雲彩
,出棧了

繪製碎圓4.png
經過這兩個圖層的演示,想必你應該明白圖層的作用了吧。
最後畫完之後,圖層全合併到root

繪製碎圓5.png
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(); } } }

點繪製.png
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 */ private void drawS(Canvas canvas) { mMainPaint.setStyle(Paint.Style.STROKE); mMainPaint.setColor(Color.parseColor("#6B6B6B")); mMainPaint.setStrokeWidth(8); mMainPaint.setStrokeCap(Paint.Cap.SQUARE); canvas.save(); canvas.rotate(240); 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); mMainPath.reset(); mMainPaint.setStrokeWidth(2); mMainPaint.setColor(Color.BLACK); mMainPath.moveTo(0, 0); mMainPath.lineTo(320, 0); canvas.drawPath(mMainPath, mMainPaint); mMainPaint.setStrokeWidth(15); mMainPaint.setColor(Color.parseColor("#8FC552")); canvas.drawPoint(0, 0, mMainPaint); canvas.restore(); }

時針.png
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); }

效果.png
好了,靜態效果實現了,現在讓它動起來吧
三、讓表動起來
1.顯示當前時間:
表的旋轉角度由每個針繪製是的 canvas.rotate(XXX);
決定,
那麼動態改變旋轉的角度不就行了嗎!
看下面一道數學題:
11:12:45秒,時針、分針、秒針的指標各與中心水平線的夾角? 答: 秒針:45 / 60.f * 360 - 90 分針:12 / 60.f * 360 - 90 + 45 / 60.f * 1 時針:11 / 60.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, hour / 60.f * 360 - 90 + min / 60.f * 30 + sec / 3600.f * 30); drawM(canvas, min / 60.f * 360 - 90 + sec / 60.f); drawH(canvas, sec / 60.f * 360 - 90);

時間.png
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); } }

表.gif
ok,完結散花(分析圖畫的真要命...)
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-8 | ofollow,noindex">Android原生繪圖之一起畫個表 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png
附錄:網格+座標系繪製工具:
1.HelpDraw
/** * 作者:張風捷特烈<br/> * 時間:2018/11/5 0005:8:43<br/> * 郵箱:[email protected]<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/> * 郵箱:[email protected]<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; } }