在上一篇自定義View分類與流程中我們瞭解自定義View相關的基本知識,不過,這些東西依舊還是理論,並不能拿來(zhuang)用(B), 這一次我們就瞭解一些能(zhaung)用(B)的東西。
在本篇文章中,我們先了解Canvas的基本用法,最後用一個小示例來結束本次教程。
一.Canvas簡介
Canvas我們可以稱之為畫布,能夠在上面繪製各種東西,是安卓平臺2D圖形繪製的基礎,非常強大。
一般來說,比較基礎的東西有兩大特點:
1.可操作性強:由於這些是構成上層的基礎,所以可操作性必然十分強大。
2.比較難用:各種方法太過基礎,想要完美的將這些操作組合起來有一定難度。
不過不必擔心,本系列文章不僅會介紹到Canvas的操作方法,還會簡單介紹一些設計思路和技巧。
二.Canvas的常用操作速查表
操作型別 | 相關API | 備註 |
---|---|---|
繪製顏色 | drawColor, drawRGB, drawARGB | 使用單一顏色填充整個畫布 |
繪製基本形狀 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次為 點、線、矩形、圓角矩形、橢圓、圓、圓弧 |
繪製圖片 | drawBitmap, drawPicture | 繪製點陣圖和圖片 |
繪製文字 | drawText, drawPosText, drawTextOnPath | 依次為 繪製文字、繪製文字時指定每個文字位置、根據路徑繪製文字 |
繪製路徑 | drawPath | 繪製路徑,繪製貝塞爾曲線時也需要用到該函式 |
頂點操作 | drawVertices, drawBitmapMesh | 通過對頂點操作可以使影象形變,drawVertices直接對畫布作用、 drawBitmapMesh只對繪製的Bitmap作用 |
畫布剪裁 | clipPath, clipRect | 設定畫布的顯示區域 |
畫布快照 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 依次為 儲存當前狀態、 回滾到上一次儲存的狀態、 儲存圖層狀態、 回滾到指定狀態、 獲取儲存次數 |
畫布變換 | translate, scale, rotate, skew | 依次為 位移、縮放、 旋轉、傾斜 |
Matrix(矩陣) | getMatrix, setMatrix, concat | 實際畫布的位移,縮放等操作的都是影象矩陣Matrix,只不過Matrix比較難以理解和使用,故封裝了一些常用的方法。 |
PS: Canvas常用方法在上面表格中已經全部列出了,當然還存在一些其他的方法未列出,具體可以參考官方文件 Canvas
三.Canvas詳解
本篇內容主要講解如何利用Canvas繪製基本圖形。
繪製顏色:
繪製顏色是填充整個畫布,常用於繪製底色。
1 |
canvas.drawColor(Color.BLUE); //繪製藍色 |
關於顏色的更多資料請參考基礎篇_顏色
建立畫筆:
要想繪製內容,首先需要先建立一個畫筆,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 1.建立一個畫筆 private Paint mPaint = new Paint(); // 2.初始化畫筆 private void initPaint() { mPaint.setColor(Color.BLACK); //設定畫筆顏色 mPaint.setStyle(Paint.Style.FILL); //設定畫筆模式為填充 mPaint.setStrokeWidth(10f); //設定畫筆寬度為10px } // 3.在建構函式中初始化 public SloopView(Context context, AttributeSet attrs) { super(context, attrs); initPaint(); } |
在建立完畫筆之後,就可以在Canvas中繪製各種內容了。
繪製點:
可以繪製一個點,也可以繪製一組點,如下:
1 2 3 4 5 6 |
canvas.drawPoint(200, 200, mPaint); //在座標(200,200)位置繪製一個點 canvas.drawPoints(new float[]{ //繪製一組點,座標位置由float陣列指定 500,500, 500,600, 500,700 },mPaint); |
關於座標原點預設在左上角,水平向右為x軸增大方向,豎直向下為y軸增大方向。
更多參考這裡 基礎篇_座標系
繪製直線:
繪製直線需要兩個點,初始點和結束點,同樣繪製直線也可以繪製一條或者繪製一組:
1 2 3 4 5 |
canvas.drawLine(300,300,500,600,mPaint); // 在座標(300,300)(500,600)之間繪製一條直線 canvas.drawLines(new float[]{ // 繪製一組線 每四數字(兩個點的座標)確定一條線 100,200,200,200, 100,300,200,300 },mPaint); |
繪製矩形:
確定確定一個矩形最少需要四個資料,就是對角線的兩個點的座標值,這裡一般採用左上角和右下角的兩個點的座標。
關於繪製矩形,Canvas提供了三種過載方法,第一種就是提供四個數值(矩形左上角和右下角兩個點的座標)來確定一個矩形進行繪製。 其餘兩種是先將矩形封裝為Rect或RectF(實際上仍然是用兩個座標點來確定的矩形),然後傳遞給Canvas繪製,如下:
1 2 3 4 5 6 7 8 9 10 |
// 第一種 canvas.drawRect(100,100,800,400,mPaint); // 第二種 Rect rect = new Rect(100,100,800,400); canvas.drawRect(rect,mPaint); // 第三種 RectF rectF = new RectF(100,100,800,400); canvas.drawRect(rectF,mPaint); |
以上三種方法所繪製出來的結果是完全一樣的。
看到這裡,相信很多觀眾會產生一個疑問,為什麼會有Rect和RectF兩種?兩者有什麼區別嗎?
答案當然是存在區別的,兩者最大的區別就是精度不同,Rect是int(整形)的,而RectF是float(單精度浮點型)的。除了精度不同,兩種提供的方法也稍微存在差別,在這裡我們暫時無需關注,想了解更多參見官方文件 Rect 和 RectF
繪製圓角矩形:
繪製圓角矩形也提供了兩種過載方式,如下:
1 2 3 4 5 6 |
// 第一種 RectF rectF = new RectF(100,100,800,400); canvas.drawRoundRect(rectF,30,30,mPaint); // 第二種 canvas.drawRoundRect(100,100,800,400,30,30,mPaint); |
上面兩種方法繪製效果也是一樣的,但鑑於第二種方法在API21的時候才新增上,所以我們一般使用的都是第一種。
下面簡單解析一下圓角矩形的幾個必要的引數的意思。
很明顯可以看出,第二種方法前四個引數和第一種方法的RectF作用是一樣的,都是為了確定一個矩形,最後一個引數Paint是畫筆,無需多說,與矩形相比,圓角矩形多出來了兩個引數rx 和 ry,這兩個引數是幹什麼的呢?
稍微分析一下,既然是圓角矩形,他的角肯定是圓弧(圓形的一部分),我們一般用什麼確定一個圓形呢?
答案是圓心 和 半徑,其中圓心用於確定位置,而半徑用於確定大小。
由於矩形位置已經確定,所以其邊角位置也是確定的,那麼確定位置的引數就可以省略,只需要用半徑就能描述一個圓弧了。
但是,半徑只需要一個引數,但這裡怎麼會有兩個呢?
好吧,讓你發現了,這裡圓角矩形的角實際上不是一個正圓的圓弧,而是橢圓的圓弧,這裡的兩個引數實際上是橢圓的兩個半徑,他們看起來個如下圖:
紅線標註的 rx 與 ry 就是兩個半徑,也就是相比繪製矩形多出來的那兩個引數。
我們瞭解到原理後,就可以為所欲為了,通過計算可知我們上次繪製的矩形寬度為700,高度為300,當你讓 rx大於350(寬度的一半), ry大於150(高度的一半) 時奇蹟就出現了, 你會發現圓角矩形變成了一個橢圓, 他們畫出來是這樣的 ( 為了方便確認我更改了畫筆顏色, 同時繪製出了矩形和圓角矩形 ):
1 2 3 4 5 6 7 8 9 10 |
// 矩形 RectF rectF = new RectF(100,100,800,400); // 繪製背景矩形 mPaint.setColor(Color.GRAY); canvas.drawRect(rectF,mPaint); // 繪製圓角矩形 mPaint.setColor(Color.BLUE); canvas.drawRoundRect(rectF,700,400,mPaint); |
其中灰色部分是我們所選定的矩形,而裡面的圓角矩形則變成了一個橢圓,實際上在rx為寬度的一半,ry為高度的一半時,剛好是一個橢圓,通過上面我們分析的原理推算一下就能得到,而當rx大於寬度的一半,ry大於高度的一半時,實際上是無法計算出圓弧的,所以drawRoundRect對大於該數值的引數進行了限制(修正),凡是大於一半的引數均按照一半來處理。
繪製橢圓:
相對於繪製圓角矩形,繪製橢圓就簡單的多了,因為他只需要一個矩形矩形作為引數:
1 2 3 4 5 6 |
// 第一種 RectF rectF = new RectF(100,100,800,400); canvas.drawOval(rectF,mPaint); // 第二種 canvas.drawOval(100,100,800,400,mPaint); |
同樣,以上兩種方法效果完全一樣,但一般使用第一種。
繪製橢圓實際上就是繪製一個矩形的內切圖形,原理如下,就不多說了:
PS: 如果你傳遞進來的是一個長寬相等的矩形(即正方形),那麼繪製出來的實際上就是一個圓。
繪製圓:
繪製圓形也比較簡單, 如下:
1 |
canvas.drawCircle(500,500,400,mPaint); // 繪製一個圓心座標在(500,500),半徑為400 的圓。 |
繪製圓形有四個引數,前兩個是圓心座標,第三個是半徑,最後一個是畫筆。
繪製圓弧:
繪製圓弧就比較神奇一點了,為了理解這個比較神奇的東西,我們先看一下它需要的幾個引數:
1 2 3 4 5 6 |
// 第一種 public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){} // 第二種 public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) {} |
從上面可以看出,相比於繪製橢圓,繪製圓弧還多了三個引數:
1 2 3 |
startAngle // 開始角度 sweepAngle // 掃過角度 useCenter // 是否使用中心 |
通過字面意思我們基本能猜測出來前兩個引數(startAngle, sweepAngel)的作用,就是確定角度的起始位置和掃過角度, 不過第三個引數是幹嘛的?試一下就知道了,上程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
RectF rectF = new RectF(100,100,800,400); // 繪製背景矩形 mPaint.setColor(Color.GRAY); canvas.drawRect(rectF,mPaint); // 繪製圓弧 mPaint.setColor(Color.BLUE); canvas.drawArc(rectF,0,90,false,mPaint); //------------------------------------- RectF rectF2 = new RectF(100,600,800,900); // 繪製背景矩形 mPaint.setColor(Color.GRAY); canvas.drawRect(rectF2,mPaint); // 繪製圓弧 mPaint.setColor(Color.BLUE); canvas.drawArc(rectF2,0,90,true,mPaint); |
上述程式碼實際上是繪製了一個起始角度為0度,掃過90度的圓弧,兩者的區別就是是否使用了中心點,結果如下:
可以發現使用了中心點之後繪製出來類似於一個扇形,而不使用中心點則是圓弧起始點和結束點之間的連線加上圓弧圍成的圖形。這樣中心點這個引數的作用就很明顯了,不必多說想必大家試一下就明白了。 另外可以關於角度可以參考一下這篇文章: 角度與弧度
相比於使用橢圓,我們還是使用正圓比較多的,使用正圓展示一下效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
RectF rectF = new RectF(100,100,800,400); // 繪製背景矩形 mPaint.setColor(Color.GRAY); canvas.drawRect(rectF,mPaint); // 繪製圓弧 mPaint.setColor(Color.BLUE); canvas.drawArc(rectF,0,90,false,mPaint); //------------------------------------- RectF rectF2 = new RectF(100,600,800,900); // 繪製背景矩形 mPaint.setColor(Color.GRAY); canvas.drawRect(rectF2,mPaint); // 繪製圓弧 mPaint.setColor(Color.BLUE); canvas.drawArc(rectF2,0,90,true,mPaint); |
簡要介紹Paint
看了上面這麼多,相信有一部分人會產生一個疑問,如果我想繪製一個圓,只要邊不要裡面的顏色怎麼辦?
很簡單,繪製的基本形狀由Canvas確定,但繪製出來的顏色,具體效果則由Paint確定。
如果你注意到了的話,在一開始我們設定畫筆樣式的時候是這樣的:
1 |
mPaint.setStyle(Paint.Style.FILL); //設定畫筆模式為填充 |
為了展示方便,容易看出效果,之前使用的模式一直為填充模式,實際上畫筆有三種模式,如下:
1 2 3 |
STROKE //描邊 FILL //填充 FILL_AND_STROKE //描邊加填充 |
為了區分三者效果我們做如下實驗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Paint paint = new Paint(); paint.setColor(Color.BLUE); paint.setStrokeWidth(40); //為了實驗效果明顯,特地設定描邊寬度非常大 // 描邊 paint.setStyle(Paint.Style.STROKE); canvas.drawCircle(200,200,100,paint); // 填充 paint.setStyle(Paint.Style.FILL); canvas.drawCircle(200,500,100,paint); // 描邊加填充 paint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawCircle(200, 800, 100, paint); |
一圖勝千言,通過以上實驗我們可以比較明顯的看出三種模式的區別,如果只需要邊緣不需要填充內容的話只需要設定模式為描邊(STROKE)即可。
其實關於Paint的內容也是有不少的,這些只是冰山一角,在後續內容中會詳細的講解Paint。
小示例
簡要介紹畫布的操作:
畫布操作詳細內容會在下一篇文章中講解, 不是本文重點,但以下示例中可能會用到,所以此處簡要介紹一下。
相關操作 | 簡要介紹 |
---|---|
save | 儲存當前畫布狀態 |
restore | 回滾到上一次儲存的狀態 |
translate | 相對於當前位置位移 |
rotate | 旋轉 |
製作一個餅狀圖
在展示百分比資料的時候經常會用到餅狀圖,像這樣:
簡單分析
其實根據我們上面的知識已經能自己製作一個餅狀圖了。不過製作東西最重要的不是製作結果,而是製作思路。 相信我貼上程式碼大家一看就立刻明白了,非常簡單的東西。不過嘛,我們們還是想了解一下製作思路:
先分析餅狀圖的構成,非常明顯,餅狀圖就是一個又一個的扇形構成的,每個扇形都有不同的顏色,對應的有名字,資料和百分比。
經以上資訊可以得出餅狀圖的最基本資料應包括:名字 資料值 百分比 對應的角度 顏色。
使用者關心的資料 : 名字 資料值 百分比
需要程式計算的資料: 百分比 對應的角度
其中顏色這一項可以使用者指定也可以用程式指定(我們這裡採用程式指定)。
封裝資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class PieData { // 使用者關心資料 private String name; // 名字 private float value; // 數值 private float percentage; // 百分比 // 非使用者關心資料 private int color = 0; // 顏色 private float angle = 0; // 角度 public PieData(@NonNull String name, @NonNull float value) { this.name = name; this.value = value; } } |
PS: 以上省略了get set方法
自定義View:
先按照自定義View流程梳理一遍(確定各個步驟應該做的事情):
步驟 | 關鍵字 | 作用 |
---|---|---|
1 | 建構函式 | 初始化(初始化畫筆Paint) |
2 | onMeasure | 測量View的大小(暫時不用關心) |
3 | onSizeChanged | 確定View大小(記錄當前View的寬高) |
4 | onLayout | 確定子View佈局(無子View,不關心) |
5 | onDraw | 實際繪製內容(繪製餅狀圖) |
6 | 提供介面 | 提供介面(提供設定資料的介面) |
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
public class PieView extends View { // 顏色表 private int[] mColors = {0xFFCCFF00, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080, 0xFFE6B800, 0xFF7CFC00}; // 餅狀圖初始繪製角度 private float mStartAngle = 0; // 資料 private ArrayList<PieData> mData; // 寬高 private int mWidth, mHeight; // 畫筆 private Paint mPaint = new Paint(); public PieView(Context context) { this(context, null); } public PieView(Context context, AttributeSet attrs) { super(context, attrs); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (null == mData) return; float currentStartAngle = mStartAngle; // 當前起始角度 canvas.translate(mWidth / 2, mHeight / 2); // 將畫布座標原點移動到中心位置 float r = (float) (Math.min(mWidth, mHeight) / 2 * 0.8); // 餅狀圖半徑 RectF rect = new RectF(-r, -r, r, r); // 餅狀圖繪製區域 for (int i = 0; i < mData.size(); i++) { PieData pie = mData.get(i); mPaint.setColor(pie.getColor()); canvas.drawArc(rect, currentStartAngle, pie.getAngle(), true, mPaint); currentStartAngle += pie.getAngle(); } } // 設定起始角度 public void setStartAngle(int mStartAngle) { this.mStartAngle = mStartAngle; invalidate(); // 重新整理 } // 設定資料 public void setData(ArrayList<PieData> mData) { this.mData = mData; initDate(mData); invalidate(); // 重新整理 } // 初始化資料 private void initDate(ArrayList<PieData> mData) { if (null == mData || mData.size() == 0) // 資料有問題 直接返回 return; float sumValue = 0; for (int i = 0; i < mData.size(); i++) { PieData pie = mData.get(i); sumValue += pie.getValue(); //計算數值和 int j = i % mColors.length; //設定顏色 pie.setColor(mColors[j]); } float sumAngle = 0; for (int i = 0; i < mData.size(); i++) { PieData pie = mData.get(i); float percentage = pie.getValue() / sumValue; // 百分比 float angle = percentage * 360; // 對應的角度 pie.setPercentage(percentage); // 記錄百分比 pie.setAngle(angle); // 記錄角度大小 sumAngle += angle; Log.i("angle", "" + pie.getAngle()); } } } |
PS: 在更改了資料需要重繪介面時要呼叫invalidate()這個函式重新繪製。
效果圖
PS: 這個餅狀圖並沒有新增百分比等資料,僅作為示例使用。
總結:
其實自定義View只要按照流程一步步的走,也是比較容易的。不過裡面也有不少坑,這些坑還是自己踩過印象比較深,建議大家不要直接copy原始碼,自己手打體驗一下。
參考資料:
View
Canvas
Android Canvas繪圖詳解
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式