Android自定義Canvas之繪制基本形狀
在本篇文章中,我們先了解Canvas的基本用法,最后用一個小示例來結(jié)束本次教程。
一.Canvas簡介
Canvas我們可以稱之為畫布,能夠在上面繪制各種東西,是安卓平臺2D圖形繪制的基礎(chǔ),非常強(qiáng)大。
一般來說,比較基礎(chǔ)的東西有兩大特點:
1.可操作性強(qiáng):由于這些是構(gòu)成上層的基礎(chǔ),所以可操作性必然十分強(qiáng)大。
2.比較難用:各種方法太過基礎(chǔ),想要完美的將這些操作組合起來有一定難度。
不過不必?fù)?dān)心,本系列文章不僅會介紹到Canvas的操作方法,還會簡單介紹一些設(shè)計思路和技巧。
二.Canvas的常用操作速查表

PS:Canvas常用方法在上面表格中已經(jīng)全部列出了,當(dāng)然還存在一些其他的方法未列出,具體可以參考官方文檔。
三.Canvas詳解
本篇內(nèi)容主要講解如何利用Canvas繪制基本圖形。
繪制顏色:
繪制顏色是填充整個畫布,常用于繪制底色。
canvas.drawColor(Color.BLUE); //繪制藍(lán)色
創(chuàng)建畫筆:
要想繪制內(nèi)容,首先需要先創(chuàng)建一個畫筆,如下:
// 1.創(chuàng)建一個畫筆private Paint mPaint = new Paint();// 2.初始化畫筆private void initPaint() {mPaint.setColor(Color.BLACK); //設(shè)置畫筆顏色mPaint.setStyle(Paint.Style.FILL); //設(shè)置畫筆模式為填充mPaint.setStrokeWidth(10f); //設(shè)置畫筆寬度為10px}// 3.在構(gòu)造函數(shù)中初始化public SloopView(Context context, AttributeSet attrs) {super(context, attrs);initPaint();}
在創(chuàng)建完畫筆之后,就可以在Canvas中繪制各種內(nèi)容了。
繪制點:
可以繪制一個點,也可以繪制一組點,如下:
canvas.drawPoint(200, 200, mPaint); //在坐標(biāo)(200,200)位置繪制一個點canvas.drawPoints(new float[]{ //繪制一組點,坐標(biāo)位置由float數(shù)組指定500,500,500,600,500,700},mPaint);
關(guān)于坐標(biāo)原點默認(rèn)在左上角,水平向右為x軸增大方向,豎直向下為y軸增大方向。

繪制直線:
繪制直線需要兩個點,初始點和結(jié)束點,同樣繪制直線也可以繪制一條或者繪制一組:
canvas.drawLine(300,300,500,600,mPaint); // 在坐標(biāo)(300,300)(500,600)之間繪制一條直線canvas.drawLines(new float[]{ // 繪制一組線 每四數(shù)字(兩個點的坐標(biāo))確定一條線100,200,200,200,100,300,200,300},mPaint);

繪制矩形:
我們都知道,確定一個矩形最少需要四個數(shù)據(jù),就是對角線的兩個點的坐標(biāo)值,這里一般采用左上角和右下角的兩個點的坐標(biāo)。
關(guān)于繪制矩形,Canvas提供了三種重載方法,第一種就是提供四個數(shù)值(矩形左上角和右下角兩個點的坐標(biāo))來確定一個矩形進(jìn)行繪制。其余兩種是先將矩形封裝為Rect或RectF(實際上仍然是用兩個坐標(biāo)點來確定的矩形),然后傳遞給Canvas繪制,如下:
// 第一種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);
以上三種方法所繪制出來的結(jié)果是完全一樣的。

看到這里,相信很多觀眾會產(chǎn)生一個疑問,為什么會有Rect和RectF兩種?兩者有什么區(qū)別嗎?
答案當(dāng)然是存在區(qū)別的,兩者最大的區(qū)別就是精度不同,Rect是int(整形)的,而RectF是float(單精度浮點型)的。除了精度不同,兩種提供的方法也稍微存在差別,在這里我們暫時無需關(guān)注。
繪制圓角矩形:
繪制圓角矩形也提供了兩種重載方式,如下:
// 第一種RectF rectF = new RectF(100,100,800,400);canvas.drawRoundRect(rectF,30,30,mPaint);// 第二種canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
上面兩種方法繪制效果也是一樣的,但鑒于第二種方法在API21的時候才添加上,所以我們一般使用的都是第一種。

下面簡單解析一下圓角矩形的幾個必要的參數(shù)的意思。
很明顯可以看出,第二種方法前四個參數(shù)和第一種方法的RectF作用是一樣的,都是為了確定一個矩形,最后一個參數(shù)Paint是畫筆,無需多說,與矩形相比,圓角矩形多出來了兩個參數(shù)rx 和 ry,這兩個參數(shù)是干什么的呢?
稍微分析一下,既然是圓角矩形,他的角肯定是圓弧(圓形的一部分),我們一般用什么確定一個圓形呢?
答案是圓心 和 半徑,其中圓心用于確定位置,而半徑用于確定大小。
由于矩形位置已經(jīng)確定,所以其邊角位置也是確定的,那么確定位置的參數(shù)就可以省略,只需要用半徑就能描述一個圓弧了。
但是,半徑只需要一個參數(shù),但這里怎么會有兩個呢?
好吧,讓你發(fā)現(xiàn)了,這里圓角矩形的角實際上不是一個正圓的圓弧,而是橢圓的圓弧,這里的兩個參數(shù)實際上是橢圓的兩個半徑,他們看起來個如下圖:

紅線標(biāo)注的 rx 與 ry 就是兩個半徑,也就是相比繪制矩形多出來的那兩個參數(shù)。
我們了解到原理后,就可以為所欲為了,通過計算可知我們上次繪制的矩形寬度為700,高度為300,當(dāng)你讓 rx大于350(寬度的一半), ry大于150(高度的一半) 時奇跡就出現(xiàn)了, 你會發(fā)現(xiàn)圓角矩形變成了一個橢圓, 他們畫出來是這樣的 ( 為了方便確認(rèn)我更改了畫筆顏色, 同時繪制出了矩形和圓角矩形 ):
// 矩形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為高度的一半時,剛好是一個橢圓,通過上面我們分析的原理推算一下就能得到,而當(dāng)rx大于寬度的一半,ry大于高度的一半時,實際上是無法計算出圓弧的,所以drawRoundRect對大于該數(shù)值的參數(shù)進(jìn)行了限制(修正),凡是大于一半的參數(shù)均按照一半來處理。
繪制橢圓:
相對于繪制圓角矩形,繪制橢圓就簡單的多了,因為他只需要一個矩形作為參數(shù):
// 第一種RectF rectF = new RectF(100,100,800,400);canvas.drawOval(rectF,mPaint);// 第二種canvas.drawOval(100,100,800,400,mPaint);
同樣,以上兩種方法效果完全一樣,但一般使用第一種。

繪制橢圓實際上就是繪制一個矩形的內(nèi)切圖形,原理如下,就不多說了:

PS:如果你傳遞進(jìn)來的是一個長寬相等的矩形(即正方形),那么繪制出來的實際上就是一個圓。
繪制圓:
繪制圓形也比較簡單, 如下:
canvas.drawCircle(500,500,400,mPaint); // 繪制一個圓心坐標(biāo)在(500,500),半徑為400 的圓。繪制圓形有四個參數(shù),前兩個是圓心坐標(biāo),第三個是半徑,最后一個是畫筆。

繪制圓弧:
繪制圓弧就比較神奇一點了,為了理解這個比較神奇的東西,我們先看一下它需要的幾個參數(shù):
// 第一種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) {}
從上面可以看出,相比于繪制橢圓,繪制圓弧還多了三個參數(shù):
startAngle // 開始角度sweepAngle // 掃過角度useCenter // 是否使用中心
通過字面意思我們基本能猜測出來前兩個參數(shù)(startAngle, sweepAngel)的作用,就是確定角度的起始位置和掃過角度, 不過第三個參數(shù)是干嘛的?試一下就知道了,上代碼:
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度的圓弧,兩者的區(qū)別就是是否使用了中心點,結(jié)果如下:

可以發(fā)現(xiàn)使用了中心點之后繪制出來類似于一個扇形,而不使用中心點則是圓弧起始點和結(jié)束點之間的連線加上圓弧圍成的圖形。這樣中心點這個參數(shù)的作用就很明顯了,不必多說想必大家試一下就明白了。
相比于使用橢圓,我們還是使用正圓比較多的,使用正圓展示一下效果:
RectF rectF = new RectF(100,100,600,600);// 繪制背景矩形mPaint.setColor(Color.GRAY);canvas.drawRect(rectF,mPaint);// 繪制圓弧mPaint.setColor(Color.BLUE);canvas.drawArc(rectF,0,90,false,mPaint);//-------------------------------------RectF rectF2 = new RectF(100,700,600,1200);// 繪制背景矩形mPaint.setColor(Color.GRAY);canvas.drawRect(rectF2,mPaint);// 繪制圓弧mPaint.setColor(Color.BLUE);canvas.drawArc(rectF2,0,90,true,mPaint);

簡要介紹Paint
看了上面這么多,相信有一部分人會產(chǎn)生一個疑問,如果我想繪制一個圓,只要邊不要里面的顏色怎么辦?
很簡單,繪制的基本形狀由Canvas確定,但繪制出來的顏色,具體效果則由Paint確定。
如果你注意到了的話,在一開始我們設(shè)置畫筆樣式的時候是這樣的:
mPaint.setStyle(Paint.Style.FILL); //設(shè)置畫筆模式為填充為了展示方便,容易看出效果,之前使用的模式一直為填充模式,實際上畫筆有三種模式,如下:
STROKE //描邊FILL //填充FILL_AND_STROKE //描邊加填充
為了區(qū)分三者效果我們做如下實驗:
Paint paint = new Paint();paint.setColor(Color.BLUE);paint.setStrokeWidth(40); //為了實驗效果明顯,特地設(shè)置描邊寬度非常大// 描邊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);

一圖勝千言,通過以上實驗我們可以比較明顯的看出三種模式的區(qū)別,如果只需要邊緣不需要填充內(nèi)容的話只需要設(shè)置模式為描邊(STROKE)即可。
其實關(guān)于Paint的內(nèi)容也是有不少的,這些只是冰山一角,在后續(xù)內(nèi)容中會詳細(xì)的講解Paint。
小示例
簡要介紹畫布的操作:
畫布操作詳細(xì)內(nèi)容會在下一篇文章中講解, 不是本文重點,但以下示例中可能會用到,所以此處簡要介紹一下。

制作一個餅狀圖
在展示百分比數(shù)據(jù)的時候經(jīng)常會用到餅狀圖,像這樣:

簡單分析
其實根據(jù)我們上面的知識已經(jīng)能自己制作一個餅狀圖了。不過制作東西最重要的不是制作結(jié)果,而是制作思路。相信我貼上代碼大家一看就立刻明白了,非常簡單的東西。不過嘛,咱們還是想了解一下制作思路:
先分析餅狀圖的構(gòu)成,非常明顯,餅狀圖就是一個又一個的扇形構(gòu)成的,每個扇形都有不同的顏色,對應(yīng)的有名字,數(shù)據(jù)和百分比。
經(jīng)以上信息可以得出餅狀圖的最基本數(shù)據(jù)應(yīng)包括:名字 數(shù)據(jù)值 百分比 對應(yīng)的角度 顏色。
用戶關(guān)心的數(shù)據(jù) :名字 數(shù)據(jù)值 百分比
需要程序計算的數(shù)據(jù):百分比 對應(yīng)的角度
其中顏色這一項可以用戶指定也可以用程序指定(我們這里采用程序指定)。
封裝數(shù)據(jù):
public class PieData {// 用戶關(guān)心數(shù)據(jù)private String name; // 名字private float value; // 數(shù)值private float percentage; // 百分比// 非用戶關(guān)心數(shù)據(jù)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流程梳理一遍(確定各個步驟應(yīng)該做的事情):

代碼如下:
public class PieView extends View {// 顏色表(注意: 此處定義顏色使用的是ARGB,帶Alpha通道的)private int[] mColors = {0xFFCCFF00, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080,0xFFE6B800, 0xFF7CFC00};// 餅狀圖初始繪制角度private float mStartAngle = 0;// 數(shù)據(jù)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);}protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = w;mHeight = h;}protected void onDraw(Canvas canvas) {super.onDraw(canvas);if (null == mData)return;float currentStartAngle = mStartAngle; // 當(dāng)前起始角度canvas.translate(mWidth / 2, mHeight / 2); // 將畫布坐標(biāo)原點移動到中心位置float r = (float) (Math.min(mWidth, mHeight) / 2 * 0.8); // 餅狀圖半徑RectF rect = new RectF(-r, -r, r, r); // 餅狀圖繪制區(qū)域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();}}// 設(shè)置起始角度public void setStartAngle(int mStartAngle) {this.mStartAngle = mStartAngle;invalidate(); // 刷新}// 設(shè)置數(shù)據(jù)public void setData(ArrayList<PieData> mData) {this.mData = mData;initData(mData);invalidate(); // 刷新}// 初始化數(shù)據(jù)private void initData(ArrayList<PieData> mData) {if (null == mData || mData.size() == 0) // 數(shù)據(jù)有問題 直接返回return;float sumValue = 0;for (int i = 0; i < mData.size(); i++) {PieData pie = mData.get(i);sumValue += pie.getValue(); //計算數(shù)值和int j = i % mColors.length; //設(shè)置顏色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; // 對應(yīng)的角度pie.setPercentage(percentage); // 記錄百分比pie.setAngle(angle); // 記錄角度大小sumAngle += angle;Log.i("angle", "" + pie.getAngle());}}}
PS: 在更改了數(shù)據(jù)需要重繪界面時要調(diào)用invalidate()這個函數(shù)重新繪制。
效果圖

PS: 這個餅狀圖并沒有添加百分比等數(shù)據(jù),僅作為示例使用。
總結(jié):
其實自定義View只要按照流程一步步的走,也是比較容易的。不過里面也有不少坑,這些坑還是自己踩過印象比較深,建議大家不要直接copy源碼,自己手打體驗一下。
