Canvas中的繪圖師講解與實戰——Android高階UI
目錄
一、前言
二、如何畫好一幅圖
三、Canvas的圖形API
四、畫布儲存狀態API
五、實戰——時鐘與指標
六、寫在最後
一、前言
在上一篇文章中,我們只是分享了裁剪型別的API,今天接著分享繪圖部分API。話不多說,老規矩,先上實戰圖。
時鐘與指標

二、如何畫好一幅圖
我們在上一篇文章中講到了,繪製一幅圖的工具和座標系。我們繼續思考,在現實中使用一張紙繪製時,我們會對這張紙進行 旋轉一定角度 來方便自己繪製,有時為了繪製一些細節, 會進行放大 ,有時也會進行 移動 這張紙。而這些操作,在canvas中也有各自對應的操作。
1、rotate 旋轉
(1)第一個rotate函式
public void rotate(float degrees) 複製程式碼
描述:以原點為旋轉中心,旋轉畫布 degrees 角度。正數為順時針旋轉,負數為逆時針旋轉。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.rotate(30); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為旋轉後繪製的圖。

(2)第二個rotate函式
public final void rotate(float degrees, float px, float py) 複製程式碼
描述:以 (px, py) 為旋轉中心,將畫布旋轉 degrees 角度。正數為順時針旋轉,負數為逆時針旋轉。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.rotate(30, 200, 300); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為旋轉後繪製的圖。

2、scale 縮放
(1)第一個scale函式
public void scale(float sx, float sy) 複製程式碼
描述 :以原點進行縮放畫布,x軸縮放 sx 倍,y軸縮放 sy 倍。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.scale(0.5f,0.33f); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為縮放後繪製的圖。

public final void scale(float sx, float sy, float px, float py) 複製程式碼
描述:以 (px, py) 進行縮放畫布,x軸縮放 sx 倍,y軸縮放 sy 倍。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.scale(0.5f, 0.33f, 200, 300); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為縮放後繪製的圖。

3、skew 斜切
public void skew(float sx, float sy) 複製程式碼
描述:進行 x 軸和 y軸 的拉伸。
拉伸規則當一個點為(x, y)時,進行斜切變換(sx, sy),得到的結果 (rx, ry)
- rx = x + sx * y;
- ry = y + sy * x;
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.skew(1, 0.5f); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為斜切後繪製的圖。
可以使用上面的 “拉伸規則” ,將紅色框的點帶入便可得到藍色框對應的點。

4、translate 偏移
public void translate(float dx, float dy) 複製程式碼
描述:將畫布水平移動 dx 個畫素, 垂直移動 dy 個畫素。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); canvas.translate(100, 200); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為移動後繪製的圖。

5、setMatrix 矩陣
public void setMatrix(@Nullable Matrix matrix) 複製程式碼
描述:將矩陣作用於畫布。
舉個例子
mPaint.setColor(Color.RED); canvas.drawRect(mRectF, mPaint); mMatrix.preTranslate(getWidth() / 2, getHeight() / 2); mMatrix.preScale(2, 1); canvas.setMatrix(mMatrix); mPaint.setColor(Color.BLUE); canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖
紅色為原圖,藍色為使用矩陣後繪製的圖。

矩陣的內容比較多,這裡只是略帶一提,如果想見識見識他的真正威力,可以看看在小盆友另一篇博文 放蕩不羈SVG講解與實戰 實戰中的使用,具體程式碼請進 傳送門 。
三、Canvas的圖形API
1、drawCircle 畫圓
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) 複製程式碼
描述:在座標為 (cx,cy) 的地方繪製半徑為 radius 的圓。
舉個例子
// 在 原點處 畫半徑為100的圓 canvas.drawCircle(0, 0, 100, mPaint); 複製程式碼
效果圖

2、drawOval 畫橢圓
(1)第一個drawOval函式
public void drawOval(@NonNull RectF oval, @NonNull Paint paint) 複製程式碼
描述:在 oval 的矩形範圍內,繪製橢圓。
舉個例子
RectF mRectF = new RectF(); mRectF.left = -150; mRectF.top = -150; mRectF.right = 400; mRectF.bottom = 150; canvas.drawOval(mRectF, mPaint); 複製程式碼
效果圖
橘色部分則為我們繪製的橢圓,而紫色框(為了方便觀看而繪製出來)則是我們的 oval 的範圍。

public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) 複製程式碼
描述:在 左上(left,top) 和 右下(right,bottom) 形成的矩形範圍內,繪製橢圓。
值得注意的是,這個方法只能在 API21 以上的版本 才能使用,所以建議使用第一個函式。
舉個例子
canvas.drawOval(-150, -150, 400, 150, mPaint); 複製程式碼
效果圖
橘色部分則為我們繪製的橢圓,而紫色框(為了方便觀看而繪製出來)則是我們的 oval 的範圍。
兩個函式效果完全一樣,只是前一個函式將兩個座標點封裝在 Rect 中,而後一函式展示在函式引數中。

3、drawLine 畫線
(1)drawLine函式
public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) 複製程式碼
描述:在座標 (startX, startY) 和 (stopX, stopY) 中繪製一條直線。
舉個例子
canvas.drawLine(-200, -200,0, 0, mPaint); 複製程式碼
效果圖

(2)第一個drawLines函式
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) 複製程式碼
描述:pts陣列中每四個數構成一條直線,每四個數中前兩個為起始座標,後兩個為終止座標。如果不夠四個數,則這一組不進行繪製。
舉個例子
private float[] pts = new float[]{ 0, -400, 200, -400, // 構成上面的線 -300, 0, -300, 300, // 構成左邊的線 0, 400, 300, 400// 構成右邊的線 }; canvas.drawLines(pts, mPaint); 複製程式碼
效果圖

(3)第二個drawLines函式(帶偏移)
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count, @NonNull Paint paint) 複製程式碼
描述:該方法比上一個方法多加兩個引數,即偏移量和數量。偏移量offset為一時,則從pts的下標為1的地方開始進行讀數,count則決定了多少個數。
舉個例子
private float[] pts = new float[]{ 0, -400, 200, -400, -300, 0, -300, 300, 0, 400, 300, 400 }; canvas.drawLines(pts, 2, 8, mPaint); 複製程式碼
效果圖
pts陣列中,從下標為2的數字開始,每四個數構成一條線,直到下標為 10 (由8+2得來) 的數為止。第一條線為上面的線,第二條線為下面的線。

4、drawArc 畫弧
(1)第一個drawArc函式
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) 複製程式碼
描述:在 oval矩形範圍內,繪製 從startAngle角度 到 sweepAngle角度的圓弧。
引數說明:1)oval:圓弧所繪的矩形範圍區域。 2)startAngle:起始角度。0度時,指向為座標系中的x軸正半軸。 3)sweepAngle:基於 startAngle 角度,掃過的角度範圍,正數則按順時針方向,負數則按逆時針方向。 4)useCenter:弧的兩端是否要連線中心點。true連線中心點,false不連線中心點。 5)paint:畫筆。
舉個例子
RectF mRectF = new RectF(); mRectF.left = -150; mRectF.top = -150; mRectF.right = 400; mRectF.bottom = 150; canvas.drawArc(mRectF, 0, 120, true, mPaint); 複製程式碼
效果圖
橘色部分則為弧線部分,紫色則為矩形範圍(為了方便檢視才繪出)。

public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) 複製程式碼
描述:該方法和上一方法功能完全一樣,只是用四個 float 表示 矩形的端點。
舉個例子
canvas.drawArc(-150, -150, 400, 150, 0, 120, false, mPaint); 複製程式碼
效果圖
橘色為圓弧,紫色為矩形範圍

5、drawPoint 畫點
(1)drawPoint函式
public void drawPoint(float x, float y, @NonNull Paint paint) 複製程式碼
描述:在座標為 (x,y) 處繪製點
舉個例子
mPaint.setColor(mColor1); mPaint.setStrokeWidth(dpToPx(5)); canvas.drawPoint(100, 100, mPaint); 複製程式碼
效果圖

public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) 複製程式碼
描述:pts陣列中每兩個數構成一個座標(前者為x,後者為y),並在該座標處點。
舉個例子
private float[] pts = new float[]{ 0, -400, 200, -400, -300, 0 }; mPaint.setColor(mColor2); mPaint.setStrokeWidth(dpToPx(5)); canvas.drawPoints(pts, mPaint); 複製程式碼
效果圖

public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count, @NonNull Paint paint) 複製程式碼
描述:這個方法和上面的方法大致相同,唯一區別在於從下標為offset開始讀取座標,讀取長度個數為count。
舉個例子
private float[] pts = new float[]{ 0, -400, 200, -400, -300, 0 }; mPaint.setColor(mColor2); mPaint.setStrokeWidth(dpToPx(5)); canvas.drawPoints(pts, 1, pts.length - 1, mPaint); 複製程式碼
效果圖

6、drawRect 畫矩形
(1)drawRect函式
public void drawRect(@NonNull RectF rect, @NonNull Paint paint) public void drawRect(@NonNull Rect r, @NonNull Paint paint) 複製程式碼
描述:在 rect 的範圍內繪製矩形,兩個方法的唯一區別在於第一個引數型別分別為 RectF 和 Rect。
RectF 和 Rect 的區別:
- 精度不同:RectF 四個點為浮點數,Rect 四個點為整型
- 所包含的方法不完全相同。
舉個例子
RectF mRectF = new RectF(); mRectF.left = -150; mRectF.top = -150; mRectF.right = 400; mRectF.bottom = 150; canvas.drawRect(mRectF, mPaint); 複製程式碼
效果圖

(2)drawRect函式
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) 複製程式碼
描述:在 (left,top) 和 (right,bottom) 形成的矩形範圍內繪製矩形。
舉個例子
canvas.drawRect(-150, -150, 400, 150, mPaint); 複製程式碼
效果圖

7、drawRoundRect 畫圓角矩形
(1)第一個drawRoundRect函式
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) 複製程式碼
描述:在 rect 範圍內,繪製圓角矩形。
引數說明:1)rx:水平方向的半徑,下圖中的橘色部分 2)ry:豎直方向的半徑,下圖中的紅色部分

canvas.drawRoundRect(mRectF, 80, 100, mPaint); 複製程式碼
效果圖

(2)第二個drawRoundRect函式
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) 複製程式碼
描述:與上述的方法功能完全相同,只是繪製範圍由四個浮點數進行確定。
舉個例子
canvas.drawRoundRect(-150, -150, 400, 150, 100, 50, mPaint); 複製程式碼
效果圖

8、drawColor 給畫布點顏色
(1)第一個drawColor函式
public void drawColor(@ColorInt int color) 複製程式碼
描述:給畫布繪製color顏色值。
舉個例子
canvas.drawColor(Color.parseColor("#ffffff")); 複製程式碼
比較簡單就不上效果圖了。
(2)第二個drawColor函式
public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) 複製程式碼
描述:給畫布繪製顏色,會與之前的圖形有 mode 的作用。
舉個例子
Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo); Matrix mMatrix = new Matrix(); mMatrix.setScale(0.25f, 0.25f); canvas.drawBitmap(mBitmap, mMatrix, mPaint); canvas.drawColor(Color.parseColor("#88880000"), PorterDuff.Mode.DST_OVER); 複製程式碼
效果圖

我們所介紹的第一個 drawColor(@ColorInt int color)
函式,其實最後使用了 PorterDuff.Mode.SRC_OVER
模式。
至於 PorterDuff.Mode 的具體使用,請看小盆友的另一篇博文: 影象操縱大師Xfermode講解與實戰
9、drawRGB 給畫布點顏色
(1)drawRGB函式
public void drawRGB(int r, int g, int b) 複製程式碼
描述:給畫布繪製顏色,按照 紅(r),綠(g),藍(b) 三原色進行組合
舉個例子
canvas.drawARGB(255, 217, 142); 複製程式碼
(2)drawARGB函式
public void drawARGB(int a, int r, int g, int b) 複製程式碼
描述:給畫布繪製顏色,按照 透明度(a),紅(r),綠(g),藍(b) 三原色進行組合
舉個例子
canvas.drawARGB(200, 255, 217, 142); 複製程式碼
10、drawPath 繪製路徑
public void drawPath(@NonNull Path path, @NonNull Paint paint) 複製程式碼
描述:將 路徑path 繪製在畫布上。
舉個例子這個方法使用的地方非常之多,例如我們繪製一個 “心” 形
mPaint.setColor(mColor1); mPaint.setStyle(Paint.Style.FILL); // 路徑的構建,移步github canvas.drawPath(mPath, mPaint); 複製程式碼
效果圖

心形路徑的構建使用了 貝塞爾曲線 ,對 貝塞爾曲線 有興趣的童鞋,可以移步小盆友的另一篇博文: 自帶美感的貝塞爾曲線原理與實戰
四、畫布儲存狀態API
1、狀態值
在進行 API 講解前,我們需要先說明狀態值,他控制著我們要儲存什麼資訊。
- MATRIX_SAVE_FLAG:儲存圖層的 Matrix矩陣資訊
- CLIP_SAVE_FLAG:儲存裁剪資訊
- HAS_ALPHA_LAYER_SAVE_FLAG:儲存該圖層的透明度
- FULL_COLOR_LAYER_SAVE_FLAG:完全保留該圖層顏色
- CLIP_TO_LAYER_SAVE_FLAG:建立圖層時,會把canvas(所有圖層)裁剪到引數指定的範圍,如果省略這個flag將導致圖層開銷巨大,效能不好。
- ALL_SAVE_FLAG:儲存所有資訊
敲黑板了!!!雖然羅列了這麼多,但1-5的FLAG已經全部被遺棄, 只剩 ALL_SAVE_FLAG
這個FLAG 。
2、save
public int save() 複製程式碼
描述:這個函式用於儲存圖層狀態,儲存此刻的 canvas 畫布的所有狀態(例如:原點位置,旋轉角度,一切我們對canvas的操作都被儲存)。
3、saveLayer
// saveFlags 只能是 Canvas.ALL_SAVE_FLAG public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint, @Saveflags int saveFlags) // saveFlags 只能是 Canvas.ALL_SAVE_FLAG public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) // API21及以上才可使用 public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint) // API21及以上才可使用 public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) 複製程式碼
描述:該方法與 save
一樣會儲存進狀態棧,然後通過 restore
或 restoreToCount
進行恢復。 不同的是該方法會建立一個新的圖層 。
這裡建立的圖層,我們可以類比為PS中的圖層概念,存在意義是不會影響到其他圖層的資料。例如我們在XFermode的博文中的 刮刮卡的實戰 中,就有用到這一概念,否則我們需要看到的圖片也會被一同清除。
4、saveLayerAlpha
// saveFlags 只能是 Canvas.ALL_SAVE_FLAG public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags) // saveFlags 只能是 Canvas.ALL_SAVE_FLAG public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, @Saveflags int saveFlags) // API21及以上才可使用 public int saveLayerAlpha(@Nullable RectF bounds, int alpha) // API21及以上才可使用 public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) 複製程式碼
描述:與 saveLayer
相同的是會存進狀態棧和建立一個圖層,然後通過 restore
或 restoreToCount
進行恢復。 不同的是建立的圖層是具有透明度的,而透明度由 alpha 決定,範圍為 0-255。
5、恢復
// 恢復 public void restore() // 恢復至指定的 狀態棧層數 public void restoreToCount(int saveCount) 複製程式碼
描述:這兩個方法,是將上面三種方法儲存的函式進行恢復。而區別在於 restore
每次從狀態棧中恢復 拿出一個狀態恢復 ,而 restoreToCount
是 恢復到指定的狀態棧層數(該層也會被出棧) ,這個 saveCount 引數在上面三種類型的方法呼叫後都會進行返回各自對應的層數。
6、小結
先舉個例子彙總一下這幾個方法:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); log(canvas); int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG); log(canvas); canvas.save(); log(canvas); canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG); log(canvas); canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 50, Canvas.ALL_SAVE_FLAG); log(canvas); canvas.translate(getWidth() / 2, getHeight() / 2); canvas.drawRect(mRect, mPaint); canvas.restore(); log(canvas); canvas.restoreToCount(layer); log(canvas); } private void log(Canvas canvas) { Log.i("canvas", "canvas count:" + canvas.getSaveCount()); } 複製程式碼
輸出結果

我們從程式碼和輸出結果可以得出以下幾個結論:
restoreToCount(x)
一圖勝千言:
將上面的程式碼轉換成圖,就如下效果

五、實戰——時鐘與指標
1、效果圖

2、程式設計思路
我們先拆解下這幅圖,其實構成的為三部分:
- 一個圓圈
- 刻度
- 指標
我們逐一解決:
(1)一個圓圈
這個我們信手拈來,canvas就有繪製圓的 API,我們在第三節的一小點就講到了
canvas.drawCircle(0, 0, width / 2, mPaint); 複製程式碼
(2)刻度
對於刻度,其實有兩種畫法:
- 第一種:是聽起來比較 “高大上” ,使用三角函式算出每個刻度的起始座標和終止座標,然後進行繪製。
- 第二種:較為機靈,使用我們在 第二小節的第一點 介紹的
rotate
進行一點點的旋轉畫布,然後繪製線。
(3)指標
我們需要先構建下圖中藍色的路徑作為指標,由一段圓弧和兩條線構成。

第一步:在紅色的矩形內,繪製圓弧(使用了第三小節第四點) 第二步:從圓弧的左點繪製線到圖中紅色頂點 第三部:從紅色頂點繪製線到圓弧右點,最後關閉路徑path
具體程式碼如下:
mPointerPath.moveTo(mPointerRadius, 0); // 第一步 mPointerPath.addArc(mPointerRectF, 0, 180); // 第二步 mPointerPath.lineTo(0, -width / 4); // 第三步 mPointerPath.lineTo(mPointerRadius, 0); mPointerPath.close(); 複製程式碼
(4)開啟旋轉
我們只需要通過屬性動畫,讓指標動起來即可。而指標的旋轉只需要通過讓畫布旋轉即可,也就是用到第二小節第一點的 rotate
。
canvas.save(); canvas.rotate(mCurAngle); ... 省略建立指標 mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mPointerColor); canvas.drawPath(mPointerPath, mPaint); canvas.restore(); 複製程式碼
時鐘與指標完整程式碼: 傳送門
六、寫在最後
這次介紹的是canvas最為基礎的API操作,但其實越為基礎的東西,越容易被忽略也越是進階中最需要的部分。這次寫的時間耗時較久,主要是API較多,寫demo和截圖比較頻繁。
如果你覺得文章對你有所幫助,請給我一個贊並關注我吧。如果發現有那些欠妥的地方,請留言區與我討論,我們共同進步。
高階UI系列的Github地址:請進入 傳送門 ,如果喜歡的話給我一個star吧:smile:
歡迎加我微信,我們可以進行更多更有趣的交流
