在上一篇自定義View分類與流程中我們了解自定義View相關(guān)的基本知識,不過,這些東西依舊還是理論身弊,并不能拿來(zhuang)用(B), 這一次我們就了解一些能(zhaung)用(B)的東西丸边。
在本篇文章中儿普,我們先了解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的常用操作速查表
操作類型 | 相關(guān)API | 備注 |
---|---|---|
繪制顏色 | drawColor, drawRGB, drawARGB | 使用單一顏色填充整個畫布 |
繪制基本形狀 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次為 點篷朵、線勾怒、矩形婆排、圓角矩形、橢圓笔链、圓段只、圓弧 |
繪制圖片 | drawBitmap, drawPicture | 繪制位圖和圖片 |
繪制文本 | drawText, drawPosText, drawTextOnPath | 依次為 繪制文字、繪制文字時指定每個文字位置鉴扫、根據(jù)路徑繪制文字 |
繪制路徑 | drawPath | 繪制路徑赞枕,繪制貝塞爾曲線時也需要用到該函數(shù) |
頂點操作 | drawVertices, drawBitmapMesh | 通過對頂點操作可以使圖像形變,drawVertices直接對畫布作用坪创、 drawBitmapMesh只對繪制的Bitmap作用 |
畫布剪裁 | clipPath, clipRect | 設(shè)置畫布的顯示區(qū)域 |
畫布快照 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 依次為 保存當(dāng)前狀態(tài)炕婶、 回滾到上一次保存的狀態(tài)、 保存圖層狀態(tài)误堡、 回滾到指定狀態(tài)古话、 獲取保存次數(shù) |
畫布變換 | translate, scale, rotate, skew | 依次為 位移、縮放锁施、 旋轉(zhuǎn)陪踩、錯切 |
Matrix(矩陣) | getMatrix, setMatrix, concat | 實際上畫布的位移,縮放等操作的都是圖像矩陣Matrix悉抵, 只不過Matrix比較難以理解和使用肩狂,故封裝了一些常用的方法。 |
PS: Canvas常用方法在上面表格中已經(jīng)全部列出了姥饰,當(dāng)然還存在一些其他的方法未列出傻谁,具體可以參考官方文檔 Canvas
三.Canvas詳解
本篇內(nèi)容主要講解如何利用Canvas繪制基本圖形。
繪制顏色:
繪制顏色是填充整個畫布列粪,常用于繪制底色审磁。
canvas.drawColor(Color.BLUE); //繪制藍(lán)色
關(guān)于顏色的更多資料請參考基礎(chǔ)篇_顏色
創(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軸增大方向。
更多參考這里 基礎(chǔ)篇_坐標(biāo)系
繪制直線:
繪制直線需要兩個點稿黍,初始點和結(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)注震贵,想了解更多參見官方文檔 Rect 和 RectF
繪制圓角矩形:
繪制圓角矩形也提供了兩種重載方式,如下:
// 第一種
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ù)的作用就很明顯了,不必多說想必大家試一下就明白了区匠。 另外可以關(guān)于角度可以參考一下這篇文章: 角度與弧度
相比于使用橢圓干像,我們還是使用正圓比較多的,使用正圓展示一下效果:
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)容會在下一篇文章中講解, 不是本文重點削锰,但以下示例中可能會用到,所以此處簡要介紹一下毕莱。
相關(guān)操作 | 簡要介紹 |
---|---|
save | 保存當(dāng)前畫布狀態(tài) |
restore | 回滾到上一次保存的狀態(tài) |
translate | 相對于當(dāng)前位置位移 |
rotate | 旋轉(zhuǎn) |
制作一個餅狀圖
在展示百分比數(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)該做的事情):
步驟 | 關(guān)鍵字 | 作用 |
---|---|---|
1 | 構(gòu)造函數(shù) | 初始化(初始化畫筆Paint) |
2 | onMeasure | 測量View的大小(暫時不用關(guān)心) |
3 | onSizeChanged | 確定View大小(記錄當(dāng)前View的寬高) |
4 | onLayout | 確定子View布局(無子View先巴,不關(guān)心) |
5 | onDraw | 實際繪制內(nèi)容(繪制餅狀圖) |
6 | 提供接口 | 提供接口(提供設(shè)置數(shù)據(jù)的接口) |
代碼如下:
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);
}
@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; // 當(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;
initDate(mData);
invalidate(); // 刷新
}
// 初始化數(shù)據(jù)
private void initDate(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ù)筹裕,僅作為示例使用。
PieView源碼下載
總結(jié):
其實自定義View只要按照流程一步步的走窄驹,也是比較容易的朝卒。不過里面也有不少坑,這些坑還是自己踩過印象比較深乐埠,建議大家不要直接copy源碼抗斤,自己手打體驗一下。
About
作者微博: GcsSloop