自定義控件系列的讀書筆記,整理自下列資料,不代表博主個人觀點(diǎn) :
三、Paint
3.1 簡單介紹paint
繪制的基本形狀由Canvas確定,但繪制出來的顏色,具體效果則由Paint確定。
如果你注意到了的話,在一開始我們設(shè)置畫筆樣式的時候是這樣的:
//設(shè)置畫筆模式為填充
mPaint.setStyle(Paint.Style.FILL);
為了展示方便,容易看出效果,之前使用的模式一直為填充模式,實(shí)際上畫筆有三種模式,如下:
STROKE //描邊
FILL //填充
FILL_AND_STROKE //描邊加填充
為了區(qū)分三者效果我們做如下實(shí)驗(yàn):
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);
結(jié)果如圖所示:

3.2 繪制文本的Paint
繪制文字也是需要畫筆的,而且文字的大小,顏色,字體,對齊方式都是由畫筆控制的。
Paint文本相關(guān)常用方法表
| 操作類型 | 相關(guān)API | 備注 |
|---|---|---|
| 色彩 | setColor setARGB setAlpha | 設(shè)置顏色,透明度 |
| 大小 | setTextSize | 設(shè)置文本字體大小 |
| 字體 | setTypeface | 設(shè)置或清除字體樣式 |
| 樣式 | setStyle | 填充(FILL),描邊(STROKE),填充加描邊(FILL_AND_STROKE) |
| 對齊 | setTextAlign | 左對齊(LEFT),居中對齊(CENTER),右對齊(RIGHT) |
| 測量 | measureText | 測量文本大小(注意,請?jiān)谠O(shè)置完文本各項(xiàng)參數(shù)后調(diào)用) |
四、使用Canvas繪制基本形狀
4.1 Canvas簡介
Canvas我們可以稱之為畫布,能夠在上面繪制各種東西,是安卓平臺2D圖形繪制的基礎(chǔ),非常強(qiáng)大。
4.2 Canvas的常用操作速查表
| 操作類型 | 相關(guān)API | 備注 |
|---|---|---|
| 繪制顏色 | drawColor, drawRGB, drawARGB | 使用單一顏色填充整個畫布 |
| 繪制基本形狀 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次為 點(diǎn)、線、矩形、圓角矩形、橢圓、圓、圓弧 |
| 繪制圖片 | drawBitmap, drawPicture | 繪制位圖和圖片 |
| 繪制文本 | drawText, drawPosText, drawTextOnPath | 依次為 繪制文字、繪制文字時指定每個文字位置、根據(jù)路徑繪制文字 |
| 繪制路徑 | drawPath | 繪制路徑,繪制貝塞爾曲線時也需要用到該函數(shù) |
| 頂點(diǎn)操作 | drawVertices, drawBitmapMesh | 通過對頂點(diǎn)操作可以使圖像形變,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 | 實(shí)際畫布的位移,縮放等操作的都是圖像矩陣Matrix,只不過Matrix比較難以理解和使用,故封裝了一些常用的方法。 |
4.3 Canvas基礎(chǔ)操作
(1)繪制顏色
繪制顏色是填充整個畫布,常用于繪制底色。
canvas.drawColor(Color.BLUE); //繪制藍(lán)色
(2)創(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)容了。
(3)繪制點(diǎn)
可以繪制一個點(diǎn),也可以繪制一組點(diǎn),如下:
canvas.drawPoint(200, 200, mPaint); //在坐標(biāo)(200,200)位置繪制一個點(diǎn)
canvas.drawPoints(new float[]{ //繪制一組點(diǎn),坐標(biāo)位置由float數(shù)組指定
500,500,
500,600,
500,700
},mPaint);
關(guān)于坐標(biāo)原點(diǎn)默認(rèn)在左上角,水平向右為x軸增大方向,豎直向下為y軸增大方向。
(3)繪制直線
繪制直線需要兩個點(diǎn),初始點(diǎn)和結(jié)束點(diǎn),同樣繪制直線也可以繪制一條或者繪制一組:
canvas.drawLine(300,300,500,600,mPaint); // 在坐標(biāo)(300,300)(500,600)之間繪制一條直線
canvas.drawLines(new float[]{ // 繪制一組線 每四數(shù)字(兩個點(diǎn)的坐標(biāo))確定一條線
100,200,200,200,
100,300,200,300
},mPaint);
(4)繪制矩形
確定一個矩形最少需要四個數(shù)據(jù),就是對角線的兩個點(diǎn)的坐標(biāo)值,這里一般采用左上角和右下角的兩個點(diǎn)的坐標(biāo)。
關(guān)于繪制矩形,Canvas提供了三種重載方法,第一種就是提供四個數(shù)值(矩形左上角和右下角兩個點(diǎn)的坐標(biāo))來確定一個矩形進(jìn)行繪制。 其余兩種是先將矩形封裝為Rect或RectF(實(shí)際上仍然是用兩個坐標(biāo)點(diǎn)來確定的矩形),然后傳遞給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);
Rect是int(整形)的,而RectF是float(單精度浮點(diǎn)型)的。除了精度不同,兩種提供的方法也稍微存在差別。
(5)繪制圓角矩形
繪制圓角矩形也提供了兩種重載方式,如下:
// 第一種
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
// 第二種
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
與矩形相比,圓角矩形多出來了兩個參數(shù)rx 和 ry,這兩個參數(shù)是干什么的呢?既然是圓角矩形,他的角肯定是圓弧(圓形的一部分),這里的兩個參數(shù)實(shí)際上是橢圓的兩個半徑。當(dāng)rx大于寬度的一半,ry大于高度的一半時,實(shí)際上是無法計(jì)算出圓弧的,所以drawRoundRect對大于該數(shù)值的參數(shù)進(jìn)行了限制(修正),凡是大于一半的參數(shù)均按照一半來處理。如下圖所示:

(6)繪制橢圓
相對于繪制圓角矩形,繪制橢圓就簡單的多了,因?yàn)樗恍枰粋€矩形矩形作為參數(shù):
// 第一種
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
// 第二種
canvas.drawOval(100,100,800,400,mPaint);
(7)繪制圓
繪制圓形也比較簡單, 如下:
// 繪制一個圓心坐標(biāo)在(500,500),半徑為400 的圓。
canvas.drawCircle(500,500,400,mPaint);
繪制圓形有四個參數(shù),前兩個是圓心坐標(biāo),第三個是半徑,最后一個是畫筆。
(8)繪制圓弧
// 第一種
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 // 是否使用中心
下面上代碼:
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);
上述代碼實(shí)際上是繪制了一個起始角度為0度,掃過90度的圓弧,兩者的區(qū)別就是是否使用了中心點(diǎn),結(jié)果如下:

可以發(fā)現(xiàn)使用了中心點(diǎn)之后繪制出來類似于一個扇形,而不使用中心點(diǎn)則是圓弧起始點(diǎn)和結(jié)束點(diǎn)之間的連線加上圓弧圍成的圖形。這樣中心點(diǎn)這個參數(shù)的作用就很明顯了。
4.4 簡單示例
參考:http://android.jobbole.com/83342/
4.5 Canvas進(jìn)階:繪制圖片
繪制有兩種方法,drawPicture(矢量圖) 和 drawBitmap(位圖)。
(1)drawPicture
使用Picture前請關(guān)閉硬件加速,以免引起不必要的問題!?。。。。。。。?!
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
也可以在AndroidMenifest文件中application節(jié)點(diǎn)下添上 android:hardwareAccelerated="false"以關(guān)閉整個應(yīng)用的硬件加速。 更多請參考這里:Android的硬件加速及可能導(dǎo)致的問題
這里的 Picture 并不是我們理解中的Picture,我們把Canvas繪制點(diǎn),線,矩形等諸多操作用Picture錄制下來,下次需要的時候拿來就能用,使用Picture相比于再次調(diào)用繪圖API,開銷是比較小的,也就是說對于重復(fù)的操作可以更加省時省力。
PS:可以把Picture看作是一個錄制Canvas操作的錄像機(jī)。
了解了Picture的概念之后,我們再了解一下Picture的相關(guān)方法:
| 相關(guān)方法 | 簡介 |
|---|---|
| public int getWidth () | 獲取寬度 |
| public int getHeight () | 獲取高度 |
| public Canvas beginRecording (int width, int height) | 開始錄制 (返回一個Canvas,在Canvas中所有的繪制都會存儲在Picture中) |
| public void endRecording () | 結(jié)束錄制 |
| public void draw (Canvas canvas) | 將Picture中內(nèi)容繪制到Canvas中 |
| public static Picture createFromStream (InputStream stream) | (已廢棄)通過輸入流創(chuàng)建一個Picture |
| public void writeToStream (OutputStream stream) | (已廢棄)將Picture中內(nèi)容寫出到輸出流中 |
很明顯,beginRecording 和 endRecording 是成對使用的,一個開始錄制,一個是結(jié)束錄制,兩者之間的操作將會存儲在Picture中。
錄制內(nèi)容,即將一些Canvas操作用Picture存儲起來,錄制的內(nèi)容是不會直接顯示在屏幕上的,只是存儲起來了而已。
錄制的實(shí)例:
// 1.創(chuàng)建Picture
private Picture mPicture = new Picture();
---------------------------------------------------------------
// 2.錄制內(nèi)容方法
private void recording() {
// 開始錄制 (接收返回值Canvas)
Canvas canvas = mPicture.beginRecording(500, 500);
// 創(chuàng)建一個畫筆
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
// 在Canvas中具體操作
// 位移
canvas.translate(250,250);
// 繪制一個圓
canvas.drawCircle(0,0,100,paint);
mPicture.endRecording();
}
---------------------------------------------------------------
// 3.在使用前調(diào)用(我在構(gòu)造函數(shù)中調(diào)用了)
public Canvas3(Context context, AttributeSet attrs) {
super(context, attrs);
recording(); // 調(diào)用錄制
}
具體使用:
Picture雖然方法就那么幾個,但是具體使用起來還是分很多情況的,由于錄制的內(nèi)容不會直接顯示,就像存儲的視頻不點(diǎn)擊播放不會自動播放一樣,同樣,想要將Picture中的內(nèi)容顯示出來就需要手動調(diào)用播放(繪制),將Picture中的內(nèi)容繪制出來可以有以下幾種方法:
| 序號 | 簡介 | 是否對Canvas有影響 |
|---|---|---|
| 1 | 使用Picture提供的draw方法繪制 | 是 |
| 2 | 使用Canvas提供的drawPicture方法繪制 | 否 |
| 3 | 將Picture包裝成為PictureDrawable,使用PictureDrawable的draw方法繪制 | 否 |
使用Picture提供的draw方法繪制:
// 將Picture中的內(nèi)容繪制在Canvas上
mPicture.draw(canvas);
PS:這種方法在比較低版本的系統(tǒng)上繪制后可能會影響Canvas狀態(tài),所以這種方法一般不會使用。
使用Canvas提供的drawPicture方法繪制:
drawPicture有三種方法:
public void drawPicture (Picture picture)
public void drawPicture (Picture picture, Rect dst)
public void drawPicture (Picture picture, RectF dst)
和使用Picture的draw方法不同,Canvas的drawPicture不會影響Canvas狀態(tài)。
canvas.drawPicture(mPicture,new RectF(0,0,mPicture.getWidth(),200));
PS:對照上一張圖片,可以比較明顯的看出,繪制的內(nèi)容根據(jù)選區(qū)進(jìn)行了縮放。
將Picture包裝成為PictureDrawable,使用PictureDrawable的draw方法繪制:
// 包裝成為Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);
// 設(shè)置繪制區(qū)域 -- 注意此處所繪制的實(shí)際內(nèi)容不會縮放
drawable.setBounds(0,0,250,mPicture.getHeight());
// 繪制
drawable.draw(canvas);
PS:此處setBounds是設(shè)置在畫布上的繪制區(qū)域,并非根據(jù)該區(qū)域進(jìn)行縮放,也不是剪裁Picture,每次都從Picture的左上角開始繪制。
(2)drawBitmap
通過BitmapFactory從不同位置獲取Bitmap:
資源文件(drawable/mipmap/raw):
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);
資源文件(assets):
Bitmap bitmap=null;
try {
InputStream is = mContext.getAssets().open("bitmap.png");
bitmap = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
....
}
內(nèi)存卡文件:
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");
網(wǎng)絡(luò)文件:
// 此處省略了獲取網(wǎng)絡(luò)輸入流的代碼
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();
繪制Bitmap:
// 第一種
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 第二種
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
// 第三種
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)
第一種方法中后兩個參數(shù)(matrix, paint)是在繪制的時候?qū)D片進(jìn)行一些改變,如果只是需要將圖片內(nèi)容繪制出來只需要如下操作就可以了:
canvas.drawBitmap(bitmap,new Matrix(),new Paint());

第二種方法就是在繪制時指定了圖片左上角的坐標(biāo)(距離坐標(biāo)原點(diǎn)的距離):
canvas.drawBitmap(bitmap,200,500,new Paint());
注意:此處指定的是與坐標(biāo)原點(diǎn)的距離,并非是與屏幕頂部和左側(cè)的距離, 雖然默認(rèn)狀態(tài)下兩者是重合的,但是也請注意分別兩者的不同。

第三種方法比較有意思,上面多了兩個矩形區(qū)域(src,dst),這兩個矩形選區(qū)是干什么用的?
示例:
// 將畫布坐標(biāo)系移動到畫布中央
canvas.translate(mWidth/2,mHeight/2);
// 指定圖片繪制區(qū)域(左上角的四分之一)
Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight()/2);
// 指定圖片在屏幕上顯示的區(qū)域
Rect dst = new Rect(0,0,200,400);
// 繪制圖片
canvas.drawBitmap(bitmap,src,dst,null);

詳解:
上面是以繪制該圖為例,用src指定了圖片繪制部分的區(qū)域,即下圖中紅色方框標(biāo)注的區(qū)域。

然后用dst指定了繪制在屏幕上的繪制,即下圖中藍(lán)色方框標(biāo)注的區(qū)域,圖片寬高會根據(jù)指定的區(qū)域自動進(jìn)行縮放。

從上面可知,第三種方法可以繪制圖片的一部分到畫布上,這有什么用呢?如果你看過某些游戲的資源文件,你可能會看到如下的圖片:

這樣的話,就可以用一張圖片包含了大量的素材,在繪制的時候每次只截取一部分進(jìn)行繪制,這樣可以大大的減少素材數(shù)量,而且素材管理起來也很方便。
下面是利用drawBitmap第三種方法制作的一個簡單示例:
資源文件如下:

最終效果如下:

源碼如下:點(diǎn)擊此處查看源碼
4.6 Canvas繪制文字
預(yù)覽一下相關(guān)常用方法:
// 第一類
public void drawText (String text, float x, float y, Paint paint)
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)
// 第二類
public void drawPosText (String text, float[] pos, Paint paint)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
// 第三類
public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
public void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)
繪制文字部分大致可以分為三類:
第一類只能指定文本基線位置(基線x默認(rèn)在字符串左側(cè),基線y默認(rèn)在字符串下方)。
第二類可以分別指定每個文字的位置。
第三類是指定一個路徑,根據(jù)路徑繪制文字。
繪制文字也是需要畫筆的,而且文字的大小,顏色,字體,對齊方式都是由畫筆控制的。(這個在第三節(jié)已經(jīng)介紹過)
為了繪制文本,我們先創(chuàng)建一個文本畫筆:
Paint textPaint = new Paint(); // 創(chuàng)建畫筆
textPaint.setColor(Color.BLACK); // 設(shè)置顏色
textPaint.setStyle(Paint.Style.FILL); // 設(shè)置樣式
textPaint.setTextSize(50); // 設(shè)置字體大小
(1)第一類(drawText)
第一類可以指定文本開始的位置,可以截取文本中部分內(nèi)容進(jìn)行繪制。其中x,y兩個參數(shù)是指定文本繪制兩個基線,示例:
// 文本(要繪制的內(nèi)容)
String str = "ABCDEFGHIJK";
// 參數(shù)分別為 (文本 基線x 基線y 畫筆)
canvas.drawText(str,200,500,textPaint);

PS: 圖中字符串下方的紅線是基線y,基線x未在圖中畫出。
當(dāng)然啦,除了能指定繪制文本的起始位置,還能只取出文本中的一部分內(nèi)容進(jìn)行繪制。
// 文本(要繪制的內(nèi)容)
String str = "ABCDEFGHIJK";
// 參數(shù)分別為 (字符串 開始截取位置 結(jié)束截取位置 基線x 基線y 畫筆)
canvas.drawText(str,1,3,200,500,textPaint);
這樣繪制的結(jié)果是:BC
PS:一定要注意接口中參數(shù) String 和 CharSequence 的兩個方法的不同,前者是使用 start和end 來截取字符串,后者是采用 startIndex 和 count 來截取字符數(shù)組。
// 字符數(shù)組(要繪制的內(nèi)容)
char[] chars = "ABCDEFGHIJK".toCharArray();
// 參數(shù)為 (字符數(shù)組 起始坐標(biāo) 截取長度 基線x 基線y 畫筆)
canvas.drawText(chars,1,3,200,500,textPaint);
這樣繪制的結(jié)果是:BCD
(2)第二類(drawPosText)
通過和第一類比較,我們可以發(fā)現(xiàn),第二類中沒有指定x,y坐標(biāo)的參數(shù),而是出現(xiàn)了這樣一個參數(shù)float[] pos。這個名為pos的浮點(diǎn)型數(shù)組就是指定坐標(biāo)的,可以給每個字符都指定一個位置。
String str = "SLOOP";
canvas.drawPosText(str,new float[]{
100,100, // 第一個字符位置
200,200, // 第二個字符位置
300,300, // ...
400,400,
500,500
},textPaint);

不建議用:因?yàn)樾阅懿患?,在大量使用的時候可能導(dǎo)致卡頓;必須指定所有字符位置,否則直接crash掉,反人類設(shè)計(jì);不支持emoji等特殊字符,不支持字形組合與分解。
(3)第三類(drawTextOnPath)
由于需要用到Path,所以放到后續(xù)章節(jié)。