Android 自定義View之繪圖

轉載。 https://blog.csdn.net/moira33/article/details/79111343

【Android 自定義View之繪圖】
Android 自定義View之繪圖
基礎圖形的繪製
一Paint與Canvas
Paint
Paint的基本設置函數
setAntiAliastrue 設置是否抗鋸齒
2setStyle PaintStyle style 設置填充樣式
3setColorColorInt int color 設置畫筆顏色
4setStrokeWidthfloat width 設置畫筆寬度
5setShadowLayerfloat radius float dx float dy int shadowColor 設置陰影
Canvas
二基本幾何圖形繪製
1畫直線drawLine
2多條直線drawLines
3點及多個點drawPointdrawPoints
4矩形drawRectdrawRoundRect
5圓形drawCircle
6橢圓drawOval
7圓弧drawArc
引用
路徑Path
Path常用方法
Path方法使用詳解
moveTo lineTo setLastPoint close
1lineTo
2moveTo 和setLastPoint
3close
quadTocubicTo
1quadTo
2cubicTo
addXxx和arcTo
1添加基本圖形
2addPath
3addArc與arcTo
isEmpty isRect set 和 offset
isEmpty
isRect
set
offset
FillType
setFillType
WINDING
EVEN_ODD
INVERSE_WINDING
INVERSE_EVEN_ODD
isInverseFillType
toggleInverseFillType
引用
文字Text
一文字
1文本繪圖樣式
2setTextAlignPaintAlign align 文字的對齊方式
3文字樣式設置
4文字傾斜度設置
5水平拉伸設置
canvas繪製文字
1drawText
2drawPosText
3drawTextOnPath
4Typeface字體樣式設置
a系統字體
b自定義字體
引用
baseLine和FontMetrics
一baseLine 基線
1canvasdrawText
2mPaintsetTextAlignPaintAlignXXX
1PaintAlignLEFT
2PaintAlignCENTER
3PaintAlignRIGHT
二FontMetrics
1獲取實例
2成員變量
3繪製ascentdescenttopbottom線
4繪製文字最小矩形文字寬度文字高度
1繪製文字最小矩形
2文字高度
3文字寬度
已知中線獲取baseline
結論
引用
Canvas
一什麼是Canvas
二Canvas 繪圖
三Canvas 的變換與操作
1平移translate
屏幕顯示與Canvas的關係
總結
2旋轉Rotate
3縮放scale
第一個構造函數
第二個構造函數
原理
4錯切skew
5裁剪畫布clip系列函數
6畫布的保存與恢復saverestore
7saveLayer
saveFlags
1 MATRIX_SAVE_FLAG
save方法
saveLayer方法
2 CLIP_SAVE_FLAG
3 FULL_COLOR_LAYER_SAVE_FLAG 和 HAS_ALPHA_LAYER_SAVE_FLAG
4 CLIP_TO_LAYER_SAVE_
習題
畫一個錶盤
畫一個正方形螺旋圖
引用
綜合習題
實現圖片圓角帶邊框的效果
引用
基礎圖形的繪製
一、Paint與Canvas
繪圖需要兩個工具,筆和紙。這裏的 Paint相當於筆,而 Canvas相當於紙,不過需要注意的是 Canvas(畫布)無限大,沒有邊界,切記理解成只有屏幕大小。我這裏打個比方, Canvas是整個天空,而屏幕是通過窗戶看到的景色。

那麼我需要改變畫筆大小,粗細,顏色,透明度,字體樣式等都需要在 Paint裏面設置; 
同樣要畫出圓形,矩形,不規則形狀都是在 Canvas裏面操作的。

Paint
Paint的基本設置函數
mPaint.setAntiAlias(true) //設置是否抗鋸齒;
mPaint.setStyle(Paint.Style.FILL_AND_STROKE); //設置填充樣式
mPaint.setColor(Color.GREEN);//設置畫筆顏色
mPaint.setStrokeWidth(2);//設置畫筆寬度
mPaint.setShadowLayer(10,15,15,Color.RED);//設置陰影
1 、setAntiAlias(true) 設置是否抗鋸齒
設置抗鋸齒會使圖像邊緣更清晰一些,鋸齒痕跡不會那麼明顯。

2、setStyle (Paint.Style style) 設置填充樣式
Paint.Style 類型:

Paint.Style.FILL_AND_STROKE 填充且描邊 
Paint.Style.STROKE 描邊 
Paint.Style.FILL 填充

看下上面三種類型,這裏以矩形爲例:

3、setColor(@ColorInt int color) 設置畫筆顏色
4、setStrokeWidth(float width) 設置畫筆寬度
5、setShadowLayer(float radius, float dx, float dy, int shadowColor) 設置陰影
先來看看參數代表的含義:

radius : 表示陰影的傾斜度 
dx : 水平位移 
dy : 垂直位移 
shadowColor : 陰影顏色

看一個簡單的例子:

paint.setShadowLayer(5,10,10,Color.parseColor("#abc133"));
1
效果圖:

這裏你可能有疑問,爲啥我自己演示了一篇卻看不到矩形,圓形等圖形的陰影,只能看到文本的陰影呢?那麼我們需要注意的是:這個方法不支持硬件加速,所以我們要測試時必須先關閉硬件加速。

那麼請加上setLayerType(LAYER_TYPE_SOFTWARE, null); 並且確保你的最小api8以上。

Canvas
下文【Canvas詳細講解】有Canvas進一步說明

畫布背景設置:

canvas.drawColor(Color.BLUE);
canvas.drawRGB(255, 255, 0);  
1
2
這兩個功能一樣,都是用來設置背景顏色的。

我們只需要重寫onDraw(Canvas canvas)方法,就可以繪製你想要的圖形了。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
     //繪製圖形
}
1
2
3
4
5
二、基本幾何圖形繪製
1、畫直線drawLine
方法預覽:

drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint)
1
參數:

startX : 開始點X座標 
startY : 開始點Y座標 
stopX : 結束點X座標 
stopY : 結束點Y座標

paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(5);    
paint.setColor(Color.parseColor("#FF0000"));
canvas.drawLine(100,100,600,600,paint);
1
2
3
4


2、多條直線drawLines
方法預覽:

drawLines(@Size(min=4,multiple=2) @NonNull float[] pts, int offset, int count, Paint paint)
drawLines(@Size(min=4,multiple=2) @NonNull float[] pts, @NonNull Paint paint)
1
2
參數:

pts : 是點的集合且大小最小爲4而且是2的倍數。表示每2個點連接形成一條直線,pts 的組織方式爲{x1,y1,x2,y2….} 
offset : 集合中跳過的數值個數,注意不是點的個數!一個點是兩個數值 
count : 參與繪製的數值的個數,指pts[]裏數值個數,而不是點的個數,因爲一個點是兩個數值

還是來看個例子:

@Override
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStyle(Paint.Style.FILL);
    paint.setStrokeWidth(5);    

    float [] pts={50,100,100,200,200,300,300,400};
    paint.setColor(Color.RED);
    canvas.drawLines(pts,paint);

    paint.setColor(Color.BLUE);
    canvas.drawLines(pts,1,4,paint);//去掉第一個數50,取之後的4個數即100,100,200,200
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
紅線:點(50,100)和點(100,200)連接成一條直線;點(200,300)和點(300,400)連接成直線。 
藍線:點(100,100)和點(200,200)連接成一條直線; 


3、點及多個點drawPoint、drawPoints
方法預覽:

drawPoint(float x, float y, @NonNull Paint paint)

drawPoints(@Size(multiple=2) @NonNull float[] pts, @NonNull Paint paint)
drawPoints(@Size(multiple=2) @NonNull float[] pts, int offset, int count, @NonNull Paint paint)
1
2
3
4
5
點的繪製和上面直線的繪製一樣,我這裏就不再累訴了。

4、矩形drawRect、drawRoundRect
方法預覽:

drawRect(@NonNull RectF rect, @NonNull Paint paint)

drawRect(@NonNull Rect r, @NonNull Paint paint)

drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
1
2
3
4
5
區別RectF 與Rect ,RectF座標系是浮點型;Rect座標系是整形。

圓角矩形方法預覽:

drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint)
1
2
3
參數: 
RectF : 繪製的矩形 
rx : 生成圓角的橢圓X軸半徑 
ry : 生成圓角的橢圓Y軸的半徑

RectF rect = new RectF(100, 10, 500, 300);
canvas.drawRoundRect(rect, 60, 20, paint);
1
2


5、圓形drawCircle
方法預覽:

drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
1
參數:

cx : 圓心X座標 
cy : 圓心Y座標 
radius : 半徑

canvas.drawCircle(400,400,300,paint);
1


6、橢圓drawOval
方法預覽:

drawOval(@NonNull RectF oval, @NonNull Paint paint)

drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
1
2
3


7、圓弧drawArc
方法預覽:

drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle,
            boolean useCenter, @NonNull Paint paint)
1
2
3
4
參數: 
oval : 生成橢圓的矩形 
startAngle : 弧開始的角度 (X軸正方向爲0度,順時針弧度增大) 
sweepAngle : 繪製多少弧度 (注意不是結束弧度) 
useCenter : 是否有弧的兩邊 true有兩邊 false無兩邊

畫筆設置填充:

RectF rect=new RectF(0,0,300,400);

paint.setStyle(Paint.Style.FILL);

paint.setColor(Color.RED);
canvas.drawArc(rect,30,30,false,paint);

paint.setColor(Color.BLUE);
canvas.drawArc(rect,120,30,true,paint);

paint.setStyle(Paint.Style.STROKE);

paint.setColor(Color.GREEN);
canvas.drawArc(rect,0,360,true,paint);

paint.setColor(Color.RED);
canvas.drawArc(rect,-30,30,false,paint);

paint.setColor(Color.BLUE);
canvas.drawArc(rect,-120,30,true,paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20


說明: 


引用:
自定義View之繪圖

路徑(Path)
Path常用方法
方法    作用    備註
moveTo    移動起點    移動下一次操作的起點位置
lineTo    連接直線    連接上一個點到當前點之間的直線
setLastPoint    設置終點    重置最後一個點的位置
close    閉合路勁    從最後一個點連接最初的一個點,形成一個閉合區域
addRect    添加矩形    添加矩形到當前Path
addRoundRect    添加圓角矩形    添加圓角矩形到當前Path
addOval    添加橢圓    添加橢圓到當前Path
addCircle    添加圓    添加圓到當前Path
addPah    添加路勁    添加路勁到當前Path
addArc    添加圓弧    添加圓弧到當前Path
arcTo    圓弧    繪製圓弧,注意和addArc的區別
isEmpty    是否爲空    判定Path是否爲空
isRect    是否爲矩形    判定Path是否是一個矩形
set    替換路勁    用新的路勁替換當前路勁的所有內容
offset    偏移路勁    對當前的路勁進行偏移
quadTo    貝塞爾曲線    二次貝塞爾曲線的方法
cubicTo    貝塞爾曲線    三次貝塞爾曲線的方法
rMoveTo
rlineTo
rQuadTo
rCubicTo    rXxx方法    不帶r的方法是基於原點座標系(偏移量),帶r的基於當前點座標系(偏移量)
op    布爾操作    對兩個Path進行布爾運算(交集,並集)等操作
setFillType    填充模式    設置Path的填充模式
getFillType    填充模式    獲取Path的填充
isInverseFillType    是否逆填充    判斷是否是逆填充模式
toggleInverseFillType    相反模式    切換相反的填充模式
getFillType    填充模式    獲取Path的填充
incReserve    提示方法    提示Path還有多少個點等待加入
computeBounds    計算邊界    計算Path的路勁
reset,rewind    重置路勁    清除Path中的內容(reset相當於new Path , rewind 會保留Path的數據結構)
transform    矩陣操作    矩陣變換
Path方法使用詳解
使用Path不僅可以繪製簡單的圖形(如圓形,矩形,直線等),也可以繪製複雜一些的圖形(如正多邊形,五角星等),還有繪製裁剪和繪製文本都會用到Path。由於方法比較多,我這裏分組來講下。

moveTo , lineTo , setLastPoint , close
先創建畫筆:

paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setColor(Color.parseColor("#FF0000"));
1
2
3
4
5
注意paint.setStyle(Paint.Style.FILL);,設置畫筆爲實心。一些線條將在畫布上看不見。

1、lineTo
首先我們來看看lineTo,如果你直接moveTo 將看不出效果。

Path path = new Path();

path.lineTo(200,200);
path.lineTo(400,0);

canvas.drawPath(path,paint);
1
2
3
4
5
6


爲了方便大家好觀察座標的變化,我在屏幕上畫出了網格,每塊網格的寬高都是100。由於第一次之前沒有過操作,所以默認點就是原點(屏幕左上角),第一次lineTo就是座標原點到(200,200)之間的直線。第二次*lineTo就是上一次結束點*位置(200,200)到(400,0)點之間的直線。

2、moveTo 和setLastPoint
方法預覽:

moveTo(float x, float y) 

setLastPoint(float dx, float dy)
1
2
3
這兩個方法在作用上有相似之處,卻是兩個不同的東西,具體參考下表:

方法名    作用    是否影響之前的操作    是否影響之後的操作
moveTo    移動下一次操作的起點位置    否    是
setLastPoint    改變上一次操作點的位置    是    是
來看看下面的例子:

Path path = new Path();

path.lineTo(200, 200);
path.moveTo(300,300);//moveTo
path.lineTo(400, 0);

canvas.drawPath(path, paint);
1
2
3
4
5
6
7
效果圖: 


Path path = new Path();

path.lineTo(200, 200);
path.setLastPoint(300,100);//setLastPoint
path.lineTo(400, 0);

canvas.drawPath(path, paint);
1
2
3
4
5
6
7
效果圖: 


當我們繪製線條之前,調用moveTo 和 setLastPoint效果是一樣的,都是對座標原點(0,0)進行操作。 
setLastPoint是重置上一次操作的最後一點,在執行完第一次lineTo的時候,最後一個點就是(200,200),setLastPoint更改(200,200)爲(300,100),所以在執行的時候就是(300,100)到(400, 0)之間的連線了。

3、close
方法預覽

public void close()
1
close方法連接最後一個點和最初一個點(如果兩個點不重合)形成一個閉合的圖形。

path.moveTo(100,100);
path.lineTo(500,100);
path.lineTo(300,400);
path.close();
canvas.drawPath(path, paint);
1
2
3
4
5
效果圖: 


上圖中可以看到lineTo(500,100)直線和lineTo(300,400)直線,而close方法就是連接(300,400),(100,100)兩點,形成一個閉合的區域。

注意:close的作用的封閉路徑,如果連接最後一個點和最初一個點任然無法形成閉合的區域,那麼close什麼也不做。

quadTo,cubicTo
二次貝塞爾曲線以及三次貝塞爾曲線。

1、quadTo
方法預覽

public void quadTo(float x1, float y1, float x2, float y2)
1
quadTo方法其中 (x1,y1) 爲控制點,(x2,y2)爲結束點。

path.moveTo(100,400);
path.quadTo(300, 100, 400, 400);

canvas.drawPath(path, paint);
1
2
3
4
效果圖: 


2、cubicTo
方法預覽:

public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
1
cubicTo方法比quadTo方法多了一個點座標,那麼其中(x1,y1) 爲控制點,(x2,y2)爲控制點,(x3,y3) 爲結束點。

path.moveTo(100, 400);
path.cubicTo(100, 400, 300, 100, 400, 400);

canvas.drawPath(path, paint);
1
2
3
4
繪製的圖形和上面的quadTo繪製的圖形是一樣的。我們去掉moveTo來看看運行的效果圖:

如果你想了解貝塞爾曲線公式,請鏈接這裏

addXxx和arcTo
主要是向Path中添加基本圖形以及區分addArc和arcTo

1、添加基本圖形
方法預覽:

//圓形
addCircle(float x, float y, float radius, Path.Direction dir)
//橢圓
addOval(RectF oval, Path.Direction dir)
addOval(float left, float top, float right, float bottom, Path.Direction dir)
//矩形
addRect(RectF rect, Path.Direction dir)
addRect(float left, float top, float right, float bottom, Path.Direction dir)
//圓角矩形
addRoundRect(RectF rect, float rx, float ry, Path.Direction dir) 
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)
addRoundRect(RectF rect, float[] radii, Path.Direction dir)
addRoundRect(float left, float top, float right, float bottom, float[] radii, Path.Direction dir)
1
2
3
4
5
6
7
8
9
10
11
12
13
我們仔細觀察上面的方法,在最後都有一個Path.Direction,這是個什麼東東呢? 
Direction的意思是方向,指導,趨勢。點進去跟一下你會發現Direction是一個枚舉類型(Enum)分別有CW(順時針),CCW(逆時針)兩個常量。那麼它的作用主要有以下兩點:

序號    作用
1    在添加圖形時確定閉合順序(各個點的記錄順序)
2    對自相交圖形的渲染結果有影響
我們先來看看閉合順序的問題,添加一個矩形看看:

path.addRect(100, 200, 500, 400, Path.Direction.CW);

canvas.drawPath(path, paint);
1
2
3


我將上面的代碼CW改成CCW再運行一次,結果一模一樣。 
想看到區別就要用到setLastPoint(重置最後一個點的座標)。我們來這樣變變代碼:

path.addRect(100, 200, 500, 400, Path.Direction.CW);
path.setLastPoint(200,400);

canvas.drawPath(path, paint);
1
2
3
4
效果立馬現行:

爲什麼圖形會發生奇怪的變化呢。我們先來分析一下,繪製一個矩形至少需要對角線的兩個點,根據這兩個點計算出四條邊然後把四條邊按照順序連接起來。上圖的起始座標是(100,200)按着順時針的方向連接(500,200),(500,400),(100,400)最後連接(100,200)形成一個矩形。setLastPoint是重置上一個操作點座標及改變(100,400)爲(200,400),所以出現了上圖的效果。

接下來我們看看逆時針的情況:

path.addRect(100, 200, 500, 400, Path.Direction.CCW);
path.setLastPoint(400,300);

canvas.drawPath(path, paint);
1
2
3
4
效果圖:

我們理清楚了閉合的問題,相交問題與設置填充模式有關。

我以addCircle方法來講解添加圖形

path.addCircle(300,300,200, Path.Direction.CW);//(300,300)點表示圓心座標,200 表示半徑長度

canvas.drawPath(path, paint);
1
2
3


path.addCircle(300, 300, 200, Path.Direction.CCW);//(300,300)點表示圓心座標,200 表示半徑長度
path.setLastPoint(300,400);
canvas.drawPath(path, paint);
1
2
3


2、addPath
方法預覽:

public void addPath(Path src)
public void addPath(Path src, float dx, float dy)//`dx,dy`指的是偏移量
public void addPath(Path src, Matrix matrix)//添加到當前path之前先使用Matrix進行變換
1
2
3
addPath方法就是將兩個路徑合併到一起。

Path path = new Path();
path.addRect(100,100,400,300, Path.Direction.CW);

Path src=new Path();
src.addCircle(300,300,100, Path.Direction.CW);
path.addPath(src,0,100);

canvas.drawPath(path, paint);
1
2
3
4
5
6
7
8
效果圖: 


3、addArc與arcTo
方法預覽:

addArc(RectF oval, float startAngle, float sweepAngle)
addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)

arcTo(RectF oval, float startAngle, float sweepAngle)
arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
1
2
3
4
5
6
從方法名字上面看,這兩個方法都是與圓弧有關,那麼他們之間肯定是有區別的:

名稱    作用    區別
addArc    添加一個圓弧到Path    直接添加一個圓弧到path中,和上一次操作點無關
arcTo    添加一個圓弧到Path    添加一個圓弧到path中,如果圓弧的起點和上次操作點座標不同就連接兩個點
startAngle表示開始圓弧度數(0度與X軸方向對齊,順時針移動,弧度增大)。 
sweepAngle表示運動了多少弧度,並不是結束弧度。

forceMoveTo表示“是否強制使用moveTo”,也就是說是否使用moveTo將上一次操作點移動到圓弧的起點座標。默認是false。

forceMoveTo    含義
true    將最後一個點移動到圓弧起點,即不連接最後一個點與圓弧起點
false    不移動,而是連接最後一個點與圓弧起點(注意之前沒有操作的話,不會連接原點)
示例:

path.lineTo(200, 200);
RectF rectF = new RectF(100, 100, 400, 400);
path.arcTo(rectF, 0, 270, true);
// path.addArc(rectF,0,270);和上面一句等價

canvas.drawPath(path, paint);
1
2
3
4
5
6
效果圖: 


我們把 path.arcTo(rectF, 0, 270, true);改成 path.arcTo(rectF, 0, 270, false);,來看看效果圖: 


從上面兩張圖可以看出明顯的變化。

isEmpty、 isRect、 set 和 offset
isEmpty
判斷path中是否包含內容。

Path path = new Path();
Log.e("-----","----"+path.isEmpty());//-----: ----true
path.lineTo(100,100);
Log.e("-----","----"+path.isEmpty());//-----: ----false

canvas.drawPath(path, paint);
1
2
3
4
5
6
isRect
方法預覽:

isRect(RectF rect)
1
判斷path是否是一個矩形,如果是一個矩形的話,會將矩形的信息存放進參數rect中。

Path path = new Path();
RectF rectF = new RectF();
rectF.left = 100;
rectF.top = 100;
rectF.right = 400;
rectF.bottom = 300;
path.addRect(rectF, Path.Direction.CW);
boolean isRect = path.isRect(rectF);
Log.e("-----","------"+isRect);//-----: ------true
1
2
3
4
5
6
7
8
9
set
方法預覽:

public void set(Path src)
1
將新的path賦值到現有path。相當於運算符中的“=”,如a=b,把b賦值給a

還是一起來看個例子:

Path path = new Path();
path.addRect(100,100,400,300, Path.Direction.CW);
Path src=new Path();
src.addCircle(300,200,100, Path.Direction.CW);
path.set(src);
canvas.drawPath(path, paint);
1
2
3
4
5
6
效果圖: 


offset
方法預覽:

public void offset(float dx, float dy)
public void offset(float dx, float dy, Path dst)
1
2
這個方法就是對Path進行一段平移,正方向和X軸,Y軸方向一致(如果dx爲正數則向右平移,反之向左平移;如果dy爲正則向下平移,反之向上平移)。 
我們看到第二個方法多了一個dst,這個又是一個什麼玩意呢,其實參數dst是存儲平移後的path的。

用例子來說明一下:

Path path = new Path();
path.addCircle(300, 200, 100, Path.Direction.CW);

Path dst = new Path();
dst.addCircle(500, 200, 200, Path.Direction.CW);

path.offset(-100, 100, dst);

paint.setColor(Color.RED);
canvas.drawPath(path, paint);

paint.setColor(Color.BLUE);
canvas.drawPath(dst, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
效果圖: 
 
從運行效果圖可以看出,雖然我們在dst中添加了一個圓形,但是並沒有表現出來,所以,當dst中存在內容時,dst中原有的內容會被清空,而存放平移後的path。 
而原來的path並沒有變化。

FillType
方法預覽:

public void setFillType(Path.FillType ft)
public Path.FillType getFillType()
1
2
setFillType方法中的參數Path.FillType爲枚舉類型:

FillType值    含義
FillType.WINDING    取path所有所在區域 默認值
FillType.EVEN_ODD    取path所在並不相交區域
FillType.INVERSE_WINDING    取path所有未佔區域
FillType.INVERSE_EVEN_ODD    取path未佔或相交區域
setFillType
WINDING
Path path = new Path();
path.addCircle(300,200,100, Path.Direction.CW);
path.addCircle(200,200,100, Path.Direction.CW);
path.setFillType(Path.FillType.WINDING);
canvas.drawPath(path, paint);
1
2
3
4
5
效果圖: 


EVEN_ODD


INVERSE_WINDING


INVERSE_EVEN_ODD


isInverseFillType
是否是逆填充模式: 
WINDING 和 EVEN_ODD 返回false; 
INVERSE_WINDING 和 INVERSE_EVEN_ODD 返回true;

toggleInverseFillType
切換相反的填充模式,如果填充模式爲WINDING則填充模式爲INVERSE_WINDING,反之爲WINDING模式;如果填充模式爲EVEN_ODD則填充模式爲INVERSE_EVEN_ODD,反之爲EVEN_ODD模式。

舉個例子:

Path path = new Path();
path.addCircle(300,200,100, Path.Direction.CW);
path.addCircle(200,200,100, Path.Direction.CW);
path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
path.toggleInverseFillType();
canvas.drawPath(path, paint);
1
2
3
4
5
6
效果圖和上面EVEN_ODD模式一模一樣。 


引用:
自定義View之繪圖篇(二):路徑(Path)

文字(Text)
一、文字
相關方法預覽:

//普通設置
paint.setAntiAlias(true); //指定是否使用抗鋸齒功能  如果使用會使繪圖速度變慢 默認false
setStyle(Paint.Style.FILL);//繪圖樣式  對於設文字和幾何圖形都有效  
setTextAlign(Align.LEFT);//設置文字對齊方式  取值:align.CENTER、align.LEFT或align.RIGHT 默認align.LEFT
paint.setTextSize(12);//設置文字大小

//樣式設置  
paint.setFakeBoldText(true);//設置是否爲粗體文字  
paint.setUnderlineText(true);//設置下劃線  
paint.setTextSkewX((float) -0.25);//設置字體水平傾斜度  普通斜體字是-0.25  
paint.setStrikeThruText(true);//設置帶有刪除線效果 

//其它設置  
paint.setTextScaleX(2);//只會將水平方向拉伸  高度不會變  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、文本繪圖樣式
先來看看下面這個例子:

mPaint.setStrokeWidth(5);
mPaint.setTextSize(80);
//設置繪圖樣式 爲填充
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText("我是一顆小小的石頭", 100,100, mPaint);

//設置繪圖樣式 爲描邊
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawText("我是一顆小小的石頭", 100,300, mPaint);

//設置繪圖樣式 爲填充且描邊
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawText("我是一顆小小的石頭", 100,500, mPaint);
1
2
3
4
5
6
7
8
9
10
11
12
13
效果圖: 


2、setTextAlign(Paint.Align align) 文字的對齊方式
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(80);
//設置對齊方式  左對齊
mPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText("小小的石頭", 500,100, mPaint);//點(500,100)在文本的左邊

//設置對齊方式  中間對齊
mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("小小的石頭", 500,200, mPaint);//點(500,100)在文本的中間

//設置對齊方式  右對齊
mPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText("小小的石頭", 500,300, mPaint);//點(500,100)在文本的右邊
1
2
3
4
5
6
7
8
9
10
11
12
13


3、文字樣式設置
canvas.drawText("小小的石頭", 200, 100, mPaint); //不帶任何效果

mPaint.setFakeBoldText(true);//是否粗體文字
mPaint.setUnderlineText(true);//設置下劃線
mPaint.setStrikeThruText(true);//設置刪除線效果
canvas.drawText("小小的石頭", 200, 200, mPaint);
1
2
3
4
5
6


4、文字傾斜度設置
mPaint.setTextSkewX(-0.25f);
canvas.drawText("小小的石頭", 100, 100, mPaint);

mPaint.setTextSkewX(0.25f);
canvas.drawText("小小的石頭", 100, 200, mPaint);

mPaint.setTextSkewX(-0.5f);
canvas.drawText("小小的石頭", 100, 300, mPaint);
1
2
3
4
5
6
7
8


可見普通斜體字是-0.25f,大於-0.25f 向左傾斜,小於 -0.25f 向右傾斜。

5、水平拉伸設置
mPaint.setTextScaleX(1);//不拉伸
canvas.drawText("小小的石頭", 100, 100, mPaint);

mPaint.setTextScaleX(2);//水平方向拉伸2倍
canvas.drawText("小小的石頭", 100, 200, mPaint);

mPaint.setTextScaleX(3);//水平方向拉伸3倍
canvas.drawText("小小的石頭", 100, 300, mPaint);
1
2
3
4
5
6
7
8


由上可以發現,僅是水平方向拉伸,高度並未改變。

canvas繪製文字
1、drawText
方法預覽:

drawText(String text, float x, float y, Paint paint)

drawText(char[] text, int index, int count, float x, float y, Paint paint)
        //text 字節數組;index 表示第一個要繪製的文字索引;count 需要繪製的文字個數

drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
//text 表示字符;start 開始截取字符的索引號;end 結束截取字符的索引號。[start , end ) 包含 start 但不包含 end

//`drawTextRun`方法是在 **skd23** 才引入的方法
drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)//isRtl 表示排列順序,true 表示正序,false 表示倒序

drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)
1
2
3
4
5
6
7
8
9
10
11
12
第一個構造函數 : 是最普通的。 
第二個構造函數 : text 字節數組;index 表示第一個要繪製的文字索引;count 需要繪製的文字個數。 
第三個構造函數 : text 表示字符 (注意與上面比較);start 開始截取字符的索引號;end 結束截取字符的索引號。(注意和上面的區別) [start , end ) 包含 start 但不包含 end

第四個構造函數和第五個構造函數 : contextIndex 和 index 相同 ; contextCount 大於等於 count ; isRtl 表示排列順序,true 表示正序,false 表示倒序(這裏的倒是指第一個字符變到最後一個字符,最後一個字符變到第一個字符)。 注意了drawTextRun方法是在 skd23 才引入的方法。

canvas.drawText("我是一顆小小的石頭".toCharArray(), 1, 4, 100, 100, mPaint);

canvas.drawText("我是一顆小小的石頭", 1, 4, 100, 200, mPaint);

//最小sdk23
canvas.drawTextRun("我是一顆小小的石頭".toCharArray(), 1, 4, 1, 4, 100, 300, true, mPaint);

canvas.drawTextRun("我是一顆小小的石頭".toCharArray(), 1, 4, 1, 4, 100, 400, false, mPaint);
1
2
3
4
5
6
7
8


2、drawPosText
方法預覽:

drawPosText(String text, float[] pos, Paint paint)

drawPosText(char[] text, int index, int count, float[] pos, Paint paint)
1
2
3
這裏的參數含義和 drawText 方法的參數一樣。我們來看個簡單的例子:

float[] pos = {100, 100, 200, 200, 300, 300, 400, 400, 500, 500, 600, 600};
canvas.drawPosText("我是一顆小小", pos, mPaint);
1
2


3、drawTextOnPath
方法預覽:

drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)

drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)
1
2
3
參數含義:

index,count : 和上面截取參數含義一樣,這裏不再累訴。

hOffset : 與路徑起點的水平偏移量。 
正數向 X 軸正方形移動(右移);負數向 X 軸負方向移動(左移); 
如果是圓弧:正數是順時針的偏移量;反之是逆時針的偏移量

vOffset : 與路徑中心的垂直偏移量。 
正數向 Y 軸正方形移動(下移);負數向 Y 軸負方向移動(上移) 
如果是圓弧:正數是向圓心移動;反之是遠離圓心

mPath.moveTo(100,100);
mPath.lineTo(800,100);
canvas.drawTextOnPath("我是一顆小小的石頭",mPath,10,-10,mPaint);
1
2
3


路徑爲圓弧的例子:

mPath.addCircle(500,500,200, Path.Direction.CW);
canvas.drawTextOnPath("我是一顆小小的石頭",mPath,40,-20,mPaint);
1
2


4、Typeface(字體樣式設置)
方法預覽:

setTypeface(Typeface typeface)
1
Typeface是用來設置字體樣式的,通過paint.setTypeface()來指定。可以指定系統中的字體樣式,也可以指定自定義的樣式文件中獲取。 
要構建Typeface時,可以指定所用樣式的正常體、斜體、粗體等,如果指定樣式中,沒有相關文字的樣式就會用系統默認的樣式來顯示,一般默認是宋體。

參數類型是枚舉類型,枚舉值如下:

Typeface.NORMAL //正常體
Typeface.BOLD //粗體
Typeface.ITALIC //斜體
Typeface.BOLD_ITALIC //粗斜體
a、系統字體
方法預覽:

create(String familyName, int style) //字體名

create(Typeface family, int style)  //類型

defaultFromStyle(int style)       //默認類型
1
2
3
4
5
我們來看一個簡單的例子:

typeface = Typeface.create("宋體", Typeface.NORMAL);
mPaint.setTypeface(typeface);
canvas.drawText("我是一顆小小的石頭", 100, 100, mPaint);

typeface = Typeface.create("楷體", Typeface.NORMAL);
mPaint.setTypeface(typeface);
canvas.drawText("我是一顆小小的石頭", 100, 200, mPaint);
1
2
3
4
5
6
7


從上圖可以看出來,設置楷體根本沒起作用,在系統的字體當中沒有找到楷體。

b、自定義字體
方法預覽:

createFromAsset(AssetManager mgr, String path) //Asset中獲取

createFromFile(File path) //文件路徑獲取

createFromFile(String path) //外部路徑獲取
1
2
3
4
5
由於後面兩個方法比較簡單,主要來看一下第一個方法。

首先在main下創建assets文件夾,然後在assets文件夾創建fonts文件夾,最後在fonts文件夾下放入font1.ttf,如圖:

typeface = Typeface.createFromAsset(mContext.getAssets(), "fonts/font1.ttf");
//Typeface.createFromFile(mContext.getFilesDir()+"/font1.ttf")
mPaint.setTypeface(typeface);
canvas.drawText("我是一顆小小的石頭", 100, 100, mPaint);

typeface = Typeface.createFromAsset(mContext.getAssets(), "fonts/font2.ttf");
mPaint.setTypeface(typeface);
canvas.drawText("我是一顆小小的石頭", 100, 200, mPaint);
1
2
3
4
5
6
7
8
效果圖: 


引用:
自定義View之繪圖篇(三):文字(Text)

baseLine和FontMetrics
瞭解baseLine和FontMetrics有助於我們理解drawText()繪製文字的原理

一、baseLine 基線
記得小時候練習字母用的是四線格本,把字母寫在四線格內,如下:

那麼在canvas中drawText繪製文字時候,也是有規則的,這個規則就是baseLine(基線)。什麼又是基線了,說白了就是一條直線,我們這裏理解的是確定它的位置。我們先來看一下基線:

從上圖看出:基線等同四線格的第三條線,在Android中基線的位置定了,那麼文字的位置也就定了。

1、canvas.drawText()
方法預覽:

drawText(String text, float x, float y, Paint paint)
1
參數: 
text 需要繪製的文字 
x 繪製文字原點X座標 
y 繪製文字原點Y座標 
paint 畫筆

我們先來看一張圖:

需要注意的是x,y並不是文字左上角的座標點,它比較特殊,y所代表的是基線座標y的座標。

我們具體來看看drawText()方法,這裏以一個例子的形式來理解:

      mPaint.setAntiAlias(true);
      mPaint.setColor(Color.RED);
      mPaint.setStyle(Paint.Style.FILL);
      mPaint.setTextSize(120);

      canvas.drawText("abcdefghijk",200,200,mPaint);

      mPaint.setColor(Color.parseColor("#23AC3B"));
      canvas.drawLine(200,200,getWidth(),200,mPaint);
1
2
3
4
5
6
7
8
9
效果圖: 


證實了y是基線y的座標點。

結論: 
1、canvas.drawText()中參數y是基線y的座標 
2、x座標、基線位置、文字大小確定,文字的位置就是確定的了。

2、mPaint.setTextAlign(Paint.Align.XXX);
我們可以從上面的例子看出,x代表的是文字開始繪製的地方。我第一次使用的時候也是這麼認爲的,可是我寫了幾個例子,發現我理解錯了。那麼正確的理解又是什麼呢?

x代表所要繪製文字所在矩形的相對位置。相對位置就是指定點(x,y)在所要繪製矩形的位置。我們知道所繪製矩形的縱座標是由Y值來確定的,而相對x座標的位置,只有左、中、右三個位置了。也就是所繪製矩形可能是在x座標相對於文字的左側,中間或者右邊繪製,而定義在x座標在所繪製矩形相對位置的函數是:

setTextAlign(Paint.Align align)
1
Paint.Align是枚舉類型,值分別爲 : Paint.Align.LEFT,Paint.Align.CENTER和Paint.Align.RIGHT。

我們來分別看一看設置不同值時,繪製的結果是怎麼樣的。

(1)、Paint.Align.LEFT
mPaint.setTextAlign(Paint.Align.LEFT);//主要是這裏的取值不一樣
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(120);

canvas.drawText("abcdefghijk", 200, 200, mPaint);

mPaint.setColor(Color.parseColor("#23AC3B"));
canvas.drawLine(0, 200, getWidth(), 200, mPaint);
canvas.drawLine(200, 0, 200, getHeight(), mPaint);
1
2
3
4
5
6
7
8
9
10
11
效果圖如下:

可以看出(x,y)在文字矩形下邊的左邊。

(2)、Paint.Align.CENTER
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(120);

canvas.drawText("abcdefghijk", 200, 200, mPaint);

mPaint.setColor(Color.parseColor("#23AC3B"));
canvas.drawLine(0, 200, getWidth(), 200, mPaint);
canvas.drawLine(200, 0, 200, getHeight(), mPaint);
1
2
3
4
5
6
7
8
9
10
11
效果圖:

可以看出(x,y)位於文字矩形下邊的中間,換句話說,系統會根據(x,y)的位置和文字矩形大小,會計算出當前開始繪製的點。以使原點(x,y)正好在所要繪製的矩形下邊的中間。

(3)、Paint.Align.RIGHT
mPaint.setTextAlign(Paint.Align.RIGHT);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(120);

canvas.drawText("abcdefghijk", 200, 200, mPaint);

mPaint.setColor(Color.parseColor("#23AC3B"));
canvas.drawLine(0, 200, getWidth(), 200, mPaint);
canvas.drawLine(200, 0, 200, getHeight(), mPaint);
1
2
3
4
5
6
7
8
9
10
11
效果圖: 


可以看出(x,y)在文字矩形下邊的右邊。

二、FontMetrics


從圖中可以知道,除了基線,還有另外的四條線,它們分別是 top,ascent,descent和bottom,它們的含義分別爲:

top:可繪製的最高高度所在線
bottom:可繪製的最低高度所在線
ascent :系統建議的,繪製單個字符時,字符應當的最高高度所在線
descent:系統建議的,繪製單個字符時,字符應當的最低高度所在線
1、獲取實例
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();

Paint.FontMetricsInt fm=  mPaint.getFontMetricsInt();
1
2
3
兩個構造方法的區別是,得到對象的成員變量的值一個爲float類型,一個爲int類型。

2、成員變量
FontMetrics,它裏面有如下五個成員變量:

float ascent = fontMetrics.ascent;
float descent = fontMetrics.descent;
float top = fontMetrics.top;
float bottom = fontMetrics.bottom;
float leading = fontMetrics.leading;
1
2
3
4
5
ascent,descent,top,bottom,leading 這些線的位置要怎麼計算出來呢?我們先來看個圖:

那麼它們的計算方法如下:

ascent = ascent線的y座標 - baseline線的y座標;//負數 
descent = descent線的y座標 - baseline線的y座標;//正數 
top = top線的y座標 - baseline線的y座標;//負數 
bottom = bottom線的y座標 - baseline線的y座標;//正數

leading = top線的y座標 - ascent線的y座標;//負數

FontMetrics的這幾個變量的值都是以baseLine爲基準的。 
對於ascent來說,baseline線在ascent線之下,所以必然baseline的y值要大於ascent線的y值,所以ascent變量的值是負的。其他幾個同理。

同樣我們可以推算出:

ascent線Y座標 = baseline線的y座標 + fontMetric.ascent; 
descent線Y座標 = baseline線的y座標 + fontMetric.descent; 
top線Y座標 = baseline線的y座標 + fontMetric.top; 
bottom線Y座標 = baseline線的y座標 + fontMetric.bottom;

3、繪製ascent,descent,top,bottom線
直接貼代碼:

int baseLineY = 200;
mPaint.setTextSize(120);
canvas.drawText("abcdefghijkl's", 200, baseLineY, mPaint);

Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();

float top = fontMetrics.top + baseLineY;
float ascent = fontMetrics.ascent + baseLineY;
float descent = fontMetrics.descent + baseLineY;
float bottom = fontMetrics.bottom + baseLineY;

//繪製基線
mPaint.setColor(Color.parseColor("#FF1493"));
canvas.drawLine(0, baseLineY, getWidth(), baseLineY, mPaint);

//繪製top直線
mPaint.setColor(Color.parseColor("#FFB90F"));
canvas.drawLine(0, top, getWidth(), top, mPaint);

//繪製ascent直線
mPaint.setColor(Color.parseColor("#b03060"));
canvas.drawLine(0, ascent, getWidth(), ascent, mPaint);

//繪製descent直線
mPaint.setColor(Color.parseColor("#912cee"));
canvas.drawLine(0, descent, getWidth(), descent, mPaint);

//繪製bottom直線
mPaint.setColor(Color.parseColor("#1E90FF"));
canvas.drawLine(0, bottom, getWidth(), bottom, mPaint);
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
在這段代碼中,我們需要注意的是: 
canvas.drawText()中參數y是基線y的位置; 
mPaint.setTextAlign(Paint.Align.LEFT);指點(200,200)在文字矩形的左邊。然後計算各條直線的y座標:

float top = fontMetrics.top + baseLineY;
float ascent = fontMetrics.ascent + baseLineY;
float descent = fontMetrics.descent + baseLineY;
float bottom = fontMetrics.bottom + baseLineY;
1
2
3
4
效果圖: 


4、繪製文字最小矩形、文字寬度、文字高度
(1)繪製文字最小矩形
由drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)需要繪製矩形就需要知道矩形左上角座標點,矩形長和寬。以上面例子爲例:

left = 200 
top = ascent ; 
right= 200+矩形寬度; 
bottom = descent;

這樣我們就可以繪製出最小矩形:

(2)文字高度
float top = fontMetrics.top + baseLineY;
float bottom = fontMetrics.bottom + baseLineY;
//文字高度
float height= bottom - top; //注意top爲負數
//文字中點y座標
float center = (bottom - top) / 2;
1
2
3
4
5
6
當然也可以: float height=Math.abs(top-bottom);

(3)文字寬度
 String text="abcdefghijkl's";
 //文字寬度
 float width = mPaint.measureText(text);
1
2
3
已知中線,獲取baseline
你可能會說這個還不簡單:

Paint mPaint = new Paint();
mPaint.setTextSize(80);
mPaint.setColor(Color.WHITE);
mPaint.setAntiAlias(true);
String text = "FontMetrics的那些猜想";
Paint.FontMetrics fm = mPaint.getFontMetrics();
//獲取文字高度
float fontHeight = fm.bottom - fm.top;
//獲取文字寬度
float fontWidth = mPaint.measureText(text);
//繪製中線
canvas.drawLine(0, centerY, getWidth(), centerY, mPaint);
//繪製文本
canvas.drawText(text, centerX - fontWidth / 2, centerY + fontHeight / 2, mPaint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
效果圖:

怎麼會這樣呢?

我們一起來分析下原因,先來看一張分析圖:

那麼我們就可以得出:

baseline=centerY+A-fm.bottom;
1
如果以:

baseline=centerY + fontHeight / 2;
1
那麼就會以bottom線作爲文字的基線,這樣就會造成文字位於中線之下。

結論
我們最終可知,當給定中間線center位置以後,那麼baseline的位置爲:

baseline = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;
1
FontMetrics.bottom注意這裏爲正數。

效果一覽:

我們還可以這樣獲取文字高度:

public float getFontHeight(Paint paint, String str) {
    Rect rect = new Rect();
    paint.getTextBounds(str, 0, str.length(), rect);
    return rect.height();
}
1
2
3
4
5
經測試得出:

Paint.FontMetrics fm = mPaint.getFontMetrics();
1
注意:fm 值和手機密度沒有關係,並且fm.bottom/fm.top=4(約等於)。

引用:
自定義View之繪圖篇(四):baseLine和FontMetrics

Canvas
一、什麼是Canvas?
什麼是Canvas?官方文檔是這麼介紹的:

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

Canvas 類是用於繪圖的,繪製圖形,你需要4個基本元素:

畫在哪。畫在Bitmap上。(相當於紙張,我們把圖畫在紙張上面)

怎麼畫。(調用canvas執行繪圖操作。比如canvas.drawCircle(),canvas.drawLine(),canvas.drawPath()將我們需要的圖像畫出來。)

畫的內容。(比如我想在紙張畫一朵花,根據自己需求畫圓,畫直線,畫路徑等)

用什麼畫。(在紙張上畫一朵花,肯定是用筆來畫的,這裏的筆指的是 Paint)

Canvas 畫布無限大,它並沒有邊界。怎麼來理解這句話呢?打個比方:畫布就是窗外的景色,而手機屏幕就是窗口,你在窗口看到窗外的景色是有限的。同樣我也可以把圖形畫到屏幕之外,通過對 Canvas 的變換與操作,讓屏幕之外的圖形顯示到屏幕裏面。

二、Canvas 繪圖
Canvas 繪製一些常見的圖形:

mPaint.setColor(Color.RED);
//繪製直線
canvas.drawLine(100,100,600,100,mPaint);

//繪製矩形
canvas.drawRect(100,200,600,400,mPaint);

//繪製文字
mPaint.setTextSize(60);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText("我是一顆石頭",100,500,mPaint);
1
2
3
4
5
6
7
8
9
10
11
12


三、Canvas 的變換與操作
有時候我們還需要對 Canvas 做一些操作,比如旋轉,裁剪,平移等等。

canvas.translate 平移

canvas.rotate 旋轉

canvas.scale 縮放

canvas.skew 錯切

canvas.clipRect 裁剪

canvas.save和canvas.restore 保存和恢復

PorterDuffXfermode 圖像混合 (paint相關方法)

1、平移(translate)
canvas中有一個函數translate()是用來實現畫布平移的,畫布的原狀是以左上角爲原點,向左是X軸正方向,向下是Y軸正方向,如下圖所示

translate函數其實實現的相當於平移座標系,即平移座標系的原點的位置。translate()函數的原型如下:

void translate(float dx, float dy)
1
參數說明: 
float dx:水平方向平移的距離,正數指向正方向(向右)平移的量,負數爲向負方向(向左)平移的量 
flaot dy:垂直方向平移的距離,正數指向正方向(向下)平移的量,負數爲向負方向(向上)平移的量

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);      
    Paint paint = new Paint();  
    paint.setColor(Color.GREEN);  
    paint.setStyle(Style.FILL);  
//translate  平移,即改變座標系原點位置      
//  canvas.translate(100, 100);  
    Rect rect1 = new Rect(0,0,400,220);  
    canvas.drawRect(rect1, paint);  
}  
1
2
3
4
5
6
7
8
9
10
1、上面這段代碼,先把canvas.translate(100, 100);註釋掉,看原來矩形的位置,然後打開註釋,看平移後的位置,對比如下圖: 


屏幕顯示與Canvas的關係
很多童鞋一直以爲顯示所畫東西的改屏幕就是Canvas,其實這是一個非常錯誤的理解,比如下面我們這段代碼:

這段代碼中,同一個矩形,在畫布平移前畫一次,平移後再畫一次,大家會覺得結果會怎樣?

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);  

    //構造兩個畫筆,一個紅色,一個綠色  
    Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 3);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 3);  

    //構造一個矩形  
    Rect rect1 = new Rect(0,0,400,220);  
    //在平移畫布前用綠色畫下邊框  
    canvas.drawRect(rect1, paint_green);  

    //平移畫布後,再用紅色邊框重新畫下這個矩形  
    canvas.translate(100, 100);  
    canvas.drawRect(rect1, paint_red);  
}  
private Paint generatePaint(int color, Paint.Style style, int width) {
    Paint paint = new Paint();
    paint.setColor(color);
    paint.setStyle(style);
    paint.setStrokeWidth(width);
    return paint;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代碼分析: 
這段代碼中,對於同一個矩形,在平移畫布前利用綠色畫下矩形邊框,在平移後,再用紅色畫下矩形邊框。大家是不是會覺得這兩個邊框會重合?實際結果是這樣的。

爲什麼綠色框並沒有移動?

這是由於屏幕顯示與Canvas根本不是一個概念! 
Canvas是一個很虛幻的概念,相當於一個透明圖層(用過PS的同學應該都知道),每次Canvas畫圖時(即調用Draw系列函數),都會產生一個透明圖層,然後在這個圖層上畫圖,畫完之後覆蓋在屏幕上顯示。所以上面的兩個結果是由下面幾個步驟形成的:

1、調用canvas.drawRect(rect1, paint_green);時,產生一個Canvas透明圖層,由於當時還沒有對座標系平移,所以座標原點是(0,0);再在系統在Canvas上畫好之後,覆蓋到屏幕上顯示出來,過程如下圖:

2、然後再第二次調用canvas.drawRect(rect1, paint_red);時,又會重新產生一個全新的Canvas畫布,但此時畫布座標已經改變了,即向右和向下分別移動了100像素,所以此時的繪圖方式爲:(合成視圖,從上往下看的合成方式)

上圖展示了,上層的Canvas圖層與底部的屏幕的合成過程,由於Canvas畫布已經平移了100像素,所以在畫圖時是以新原點來產生視圖的,然後合成到屏幕上,這就是我們上面最終看到的結果了。我們看到屏幕移動之後,有一部分超出了屏幕的範圍,那超出範圍的圖像顯不顯示呢,當然不顯示了!也就是說,Canvas上雖然能畫上,但超出了屏幕的範圍,是不會顯示的。當然,我們這裏也沒有超出顯示範圍,兩框框而已。

總結:
1、每次調用canvas.draw**XXXX系列函數來繪圖進,都會產生一個全新的Canvas畫布**。 
2、如果在DrawXXX前,調用平移、旋轉等函數來對Canvas進行了操作,那麼這個操作是不可逆的!每次產生的畫布的最新位置都是這些操作後的位置。(關於Save()、Restore()的畫布可逆問題的後面再講) 
3、在Canvas與屏幕合成時,超出屏幕範圍的圖像是不會顯示出來的。

2、旋轉(Rotate)
畫布的旋轉是默認是圍繞座標原點來旋轉的,這裏容易產生錯覺,看起來覺得是圖片旋轉了,其實我們旋轉的是畫布,以後在此畫布上畫的東西顯示出來的時候全部看起來都是旋轉的。其實Roate函數有兩個構造函數:

void rotate(float degrees)
void rotate (float degrees, float px, float py) 
1
2
第一個構造函數直接輸入旋轉的度數,正數是順時針旋轉,負數指逆時針旋轉,它的旋轉中心點是原點(0,0) 
第二個構造函數除了度數以外,還可以指定旋轉的中心點座標(px,py)

下面以第一個構造函數爲例,旋轉一個矩形,先畫出未旋轉前的圖形,然後再畫出旋轉後的圖形;

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);  
    Paint paint_green = generatePaint(Color.GREEN, Style.FILL, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  

    Rect rect1 = new Rect(300,10,500,100);  
    canvas.drawRect(rect1, paint_red); //畫出原輪廓  

    canvas.rotate(30);//順時針旋轉畫布  
    canvas.drawRect(rect1, paint_green);//畫出旋轉後的矩形  
}   
1
2
3
4
5
6
7
8
9
10
11
效果圖是這樣的:

這個最終屏幕顯示的構造過程是這樣的:

下圖顯示的是第一次畫圖合成過程,此時僅僅調用canvas.drawRect(rect1, paint_red); 畫出原輪廓

然後是先將Canvas正方向依原點旋轉30度,然後再與上面的屏幕合成,最後顯示出我們的複合效果。

有關Canvas與屏幕的合成關係我覺得我已經講的夠詳細了,後面的幾個操作Canvas的函數,我就不再一一講它的合成過程了。

3、縮放(scale )
public void scale (float sx, float sy) 
public final void scale (float sx, float sy, float px, float py)
1
2
第一個構造函數:
float sx:水平方向伸縮的比例,假設原座標軸的比例爲n,不變時爲1,在變更的X軸密度爲n*sx;所以,sx爲小數爲縮小,sx爲整數爲放大 
float sy:垂直方向伸縮的比例,同樣,小數爲縮小,整數爲放大

注意:這裏有X、Y軸的密度的改變,顯示到圖形上就會正好相同,比如X軸縮小,那麼顯示的圖形也會縮小。一樣的。

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(8);

        Rect rect = new Rect(100, 100, 200, 200);
//原圖
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);
//畫布縮放方法1
        canvas.scale(0.5f, 2f);
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
 
因爲是整個畫布的伸縮,對應連Stroke線的粗細也發生了變化

由圖可知: 
原圖:Rect(100, 100, 200, 200) 
移動後:Rect(50, 200, 100, 400) 
公式:

原圖:(l, t, r, b)
scale (sx,  sy)
移動後:(l*sx, t*sy, r*sx, b*sy)
1
2
3
第二個構造函數:
void scale (float sx, float sy, float px, float py) 
Preconcat the current matrix with the specified scale. 
Parameters 
sx 
float: The amount to scale in X 
sy 
float: The amount to scale in Y 
px 
float: The x-coord for the pivot point (unchanged by the scale) 
py 
float: The y-coord for the pivot point (unchanged by the scale)

px 和 py 分別爲縮放的中心點,不設置的話默認爲畫布原點(0, 0)

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(4);

        Rect rect = new Rect(100, 100, 200, 200);
//原圖
        canvas.save();
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);
//畫布縮放方法2
        canvas.scale(2f, 2f, 0, 0);
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
//畫布縮放方法2(以正方形中心點爲縮放中心)
        canvas.restore();
        canvas.scale(2f, 2f, 150, 150);
        paint.setColor(Color.GREEN);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


原理
源碼如下:

public final void scale(float sx, float sy, float px, float py) {
    translate(px, py);
    scale(sx, sy);
    translate(-px, -py);
}
1
2
3
4
5
步驟: 
先將畫布平移px,py,然後scale,scale結束之後再將畫布平移-px,-py。

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(8);

        Rect rect = new Rect(100, 100, 200, 200);
//原圖
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);
//畫布縮放方法2
        canvas.scale(0.5f, 2f, 100, 100);//px、py
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13


原理演示:

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(8);

        Rect rect = new Rect(100, 100, 200, 200);

//原圖
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);
//以下三步模擬canvas.scale(0.5f, 2f, 100, 100);
//第①步:移動
        canvas.translate(100, 100);
        paint.setColor(Color.GREEN);
        canvas.drawRect(rect, paint);
//第②步:縮放
        canvas.scale(0.5f, 2f);
        paint.setColor(Color.YELLOW);
        canvas.drawRect(rect, paint);
//第③步:反向移動
        canvas.translate(-100, -100);
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
由圖可知: 
原圖:Rect(100, 100, 200, 200) 
移動後:Rect(100, 100, 150, 300) 
公式:

原圖:(l,  t,  r,  b)

scale (sx,  sy,  px,  py)   
    第①步:translate(px,  py);
        原點(px,  py)
        圖①(l+px, t+py, r+px, b+py)
    第②步:scale(sx,  sy);
        原點(px,  py)
        圖②(l*sx+px, t*sy+py, r*sx+px, b*sy+py)
    第③步:translate(-px,  -py);
        原點(px+(-px)*sx,  py+(-py)*sy)即(px*(1-sx), py*(1-sy))
        圖③(l*sx+px*(1-sx), t*py*(1-sy)+(-py)*sy, r*sx+px*(1-sx), b*sy+py*(1-sy))

移動後:(l*sx+px*(1-sx), t*py*(1-sy)+(-py)*sy, r*sx+px*(1-sx), b*sy+py*(1-sy))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Rt總結: 
縮放就是相對於原點距離的縮放, 
移動就是對原點進行移動; 
視覺座標是距離原點的位置加上原點的座標, 
canvas繪畫的座標是相較於原點的座標。

4、錯切(skew)
它的構造函數:

void skew (float sx, float sy)
1
參數說明: 
float sx:將畫布在x方向上傾斜相應的角度,sx傾斜角度的tan值, 
float sy:將畫布在y軸方向上傾斜相應的角度,sy爲傾斜角度的tan值,

注意,這裏全是傾斜角度的tan值,比如我們打算在X軸方向上傾斜30度,tan30=1/√3 約等於 0.56;tan60=根號3,小數對應1.732。

舉例(在X軸方向上傾斜45度,tan45=1):

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);
        Rect rect = new Rect(50, 50, 150, 150);
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);

        canvas.skew(1, 0);//skew
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10


可以從效果圖當中看出來,我們設置 x 方向傾斜,反而 y 方向傾斜了,這就是爲什麼要叫做錯切了。你心中一定又會有疑問?每個點的座標又是怎麼計算的呢?那下面我們一起來分析下:

分析: 
設定在X軸上傾斜45° 
 
設定在Y軸上傾斜30° 


//公式推導,以在Y軸傾斜爲例skew (0, sy)
A點到舊X軸距離AX = 新點A1點到新X軸距離A1X1
XX1=OX * sy
新點A1縱座標 = A1X1 + XX1
            = AX + OX * sy
即:新點A1縱座標 = A點的縱座標 + A點的橫座標*傾斜值
1
2
3
4
5
6
void skew (float sx, float sy) 
Preconcat the current matrix with the specified skew. 
Parameters 
sx 
float: The amount to skew in X 
sy 
float: The amount to skew in Y

在X軸方向傾斜

A 點橫座標傾斜後的值 = A點的橫座標+A點的縱座標*傾斜值 
B 點橫座標傾斜後的值 = B點的橫座標+A點的縱座標*傾斜值

C 點橫座標傾斜後的值 = C點的橫座標+C點的縱座標*傾斜值 
D 點橫座標傾斜後的值 = D點的橫座標+C點的縱座標*傾斜值

在Y軸方向傾斜

A 點縱座標傾斜後的值 = A點的縱座標+A點的橫座標*傾斜值 
D 點縱座標傾斜後的值 = D點的縱座標+A點的橫座標*傾斜值

C 點縱座標傾斜後的值 = C點的縱座標+C點的橫座標*傾斜值 
B 點縱座標傾斜後的值 = B點的縱座標+C點的橫座標*傾斜值

在y方向傾斜30度

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);
        Rect rect = new Rect(50, 100, 150, 200);
        paint.setColor(Color.RED);
        canvas.drawRect(rect, paint);

        canvas.skew(0, 0.56f);
        paint.setColor(Color.BLUE);
        canvas.drawRect(rect, paint);
1
2
3
4
5
6
7
8
9
10


5、裁剪畫布(clip系列函數)
裁剪畫布是利用Clip系列函數,通過與Rect、Path、Region取交、並、差等集合運算來獲得最新的畫布形狀。除了調用Save、Restore函數以外,這個操作是不可逆的,一但Canvas畫布被裁剪,就不能再被恢復! 
Clip系列函數如下: 
boolean clipPath(Path path) 
boolean clipPath(Path path, Region.Op op) 
boolean clipRect(Rect rect, Region.Op op) 
boolean clipRect(RectF rect, Region.Op op) 
boolean clipRect(int left, int top, int right, int bottom) 
boolean clipRect(float left, float top, float right, float bottom) 
boolean clipRect(RectF rect) 
boolean clipRect(float left, float top, float right, float bottom, Region.Op op) 
boolean clipRect(Rect rect) 
boolean clipRegion(Region region) 
boolean clipRegion(Region region, Region.Op op)

以上就是根據Rect、Path、Region來取得最新畫布的函數,難度都不大,就不再一一講述。利用ClipRect() 來稍微一講。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  
    canvas.clipRect(new Rect(100, 100, 200, 200));  
    canvas.drawColor(Color.GREEN);  
}   
1
2
3
4
5
6
7
先把背景色整個塗成紅色。顯示在屏幕上 
然後裁切畫布,最後最新的畫布整個塗成綠色。可見綠色部分,只有一小塊,而不再是整個屏幕了。 
關於兩個畫布與屏幕合成,我就不再畫圖了,跟上面的合成過程是一樣的。 


6、畫布的保存與恢復(save()、restore())
前面我們講的所有對畫布的操作都是不可逆的,這會造成很多麻煩,比如,我們爲了實現一些效果不得不對畫布進行操作,但操作完了,畫布狀態也改變了,這會嚴重影響到後面的畫圖操作。如果我們能對畫布的大小和狀態(旋轉角度、扭曲等)進行實時保存和恢復就最好了。 
這小節就給大家講講畫布的保存與恢複相關的函數——Save()、Restore()。

int save ()
void  restore()
1
2
這兩個函數沒有任何的參數,很簡單。 
Save():每次調用Save()函數,都會把當前的畫布的狀態進行保存,然後放入特定的棧中; 
restore():每當調用Restore()函數,就會把棧中最頂層的畫布狀態取出來,並按照這個狀態恢復當前的畫布,並在這個畫布上做畫。 
爲了更清晰的顯示這兩個函數的作用,下面舉個例子:

    canvas.drawColor(Color.RED);      
    //保存當前畫布大小即整屏  
    canvas.save();   

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);      
    //恢復整屏畫布  
    canvas.restore();

    canvas.drawColor(Color.BLUE);  
1
2
3
4
5
6
7
8
9
10
他圖像的合成過程爲:(最終顯示爲全屏幕藍色) 


下面我通過一個多次利用Save()、Restore()來講述有關保存Canvas畫布狀態的棧的概念:代碼如下:

    canvas.drawColor(Color.RED);  
    //保存的畫布大小爲全屏幕大小  
    canvas.save();  

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //保存畫布大小爲Rect(100, 100, 800, 800)  
    canvas.save();  

    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //保存畫布大小爲Rect(200, 200, 700, 700)  
    canvas.save();  

    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //保存畫布大小爲Rect(300, 300, 600, 600)  
    canvas.save();  

    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
顯示效果爲:

在這段代碼中,總共調用了四次Save操作。上面提到過,每調用一次Save()操作就會將當前的畫布狀態保存到棧中,所以這四次Save()所保存的狀態的棧的狀態如下:

注意在,第四次Save()之後,我們還對畫布進行了canvas.clipRect(new Rect(400, 400, 500, 500));操作,並將當前畫布畫成白色背景。也就是上圖中最小塊的白色部分,是最後的當前的畫布。

如果,現在使用Restor(),會怎樣呢,會把棧頂的畫布取出來,當做當前畫布的畫圖,試一下:

canvas.drawColor(Color.RED);  
//保存的畫布大小爲全屏幕大小  
canvas.save();  

canvas.clipRect(new Rect(100, 100, 800, 800));  
canvas.drawColor(Color.GREEN);  
//保存畫布大小爲Rect(100, 100, 800, 800)  
canvas.save();  

canvas.clipRect(new Rect(200, 200, 700, 700));  
canvas.drawColor(Color.BLUE);  
//保存畫布大小爲Rect(200, 200, 700, 700)  
canvas.save();  

canvas.clipRect(new Rect(300, 300, 600, 600));  
canvas.drawColor(Color.BLACK);  
//保存畫布大小爲Rect(300, 300, 600, 600)  
canvas.save();  

canvas.clipRect(new Rect(400, 400, 500, 500));  
canvas.drawColor(Color.WHITE);  

//將棧頂的畫布狀態取出來,作爲當前畫布,並畫成黃色背景  
canvas.restore();  
canvas.drawColor(Color.YELLOW);  
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
上段代碼中,把棧頂的畫布狀態取出來,作爲當前畫布,然後把當前畫布的背景色填充爲黃色 


那如果我連續Restore()三次,會怎樣呢? 
我們先分析一下,然後再看效果:Restore()三次的話,會連續出棧三次,然後把第三次出來的Canvas狀態當做當前畫布,也就是Rect(100, 100, 800, 800),所以如下代碼:

    canvas.drawColor(Color.RED);  
    //保存的畫布大小爲全屏幕大小  
    canvas.save();  

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //保存畫布大小爲Rect(100, 100, 800, 800)  
    canvas.save();  

    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //保存畫布大小爲Rect(200, 200, 700, 700)  
    canvas.save();  

    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //保存畫布大小爲Rect(300, 300, 600, 600)  
    canvas.save();  

    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  

    //連續出棧三次,將最後一次出棧的Canvas狀態作爲當前畫布,並畫成黃色背景  
    canvas.restore();  
    canvas.restore();  
    canvas.restore();  
    canvas.drawColor(Color.YELLOW);  
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
結果爲:

7、saveLayer
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
            @Saveflags int saveFlags)
            ...
saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
            @Saveflags int saveFlags)
            ...
1
2
3
4
5
6
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) 
Canvas 在一般的情況下可以看作是一張畫布,所有的繪圖操作如drawBitmap, drawCircle都發生在這張畫布上,這張畫板還定義了一些屬性比如Matrix,顏色等等。 
但是如果需要實現一些相對複雜的繪圖操作,比如多層動畫,地圖(地圖可以有多個地圖層疊加而成,比如:政區層,道路層,興趣點層)。Canvas提供了圖層(Layer)支持,缺省情況可以看作是隻有一個圖層Layer。如果需要按層次來繪圖,Android的Canvas可以使用SaveLayerXXX, Restore 來創建一些中間層,對於這些Layer是按照“棧結構“來管理的:

創建一個新的Layer到“棧”中,可以使用saveLayer, savaLayerAlpha; 
從“棧”中推出一個Layer,可以使用restore,restoreToCount。 
但Layer入棧時,後續的DrawXXX操作都發生在這個Layer上,而Layer退棧時,就會把本層繪製的圖像“繪製”到上層或是Canvas上。 
在複製Layer到Canvas上時,可以指定Layer的透明度(Layer),這是在創建Layer時指定的:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)

本例Layers 介紹了圖層的基本用法:Canvas可以看做是由兩個圖層(Layer)構成的,爲了更好的說明問題,我們將代碼稍微修改一下,缺省圖層繪製一個紅色的圓,在新的圖層畫一個藍色的圓,新圖層的透明度爲0×88。 
public class Layers extends Activity {

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint mPaint = new Paint();
    mPaint.setAntiAlias(true);

    canvas.drawColor(Color.RED);

    canvas.saveLayerAlpha(0, 0, 300, 300, 0x88, Canvas.ALL_SAVE_FLAG);//圖層1
    canvas.drawColor(Color.BLUE);

    canvas.restore();//下面兩張圖,圖1註釋掉詞句,圖2沒註釋掉
    canvas.saveLayerAlpha(100, 100, 400, 400, 0xff, Canvas.ALL_SAVE_FLAG);//圖層2
    canvas.drawColor(Color.YELLOW);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
沒有canvas.restore();,所以圖層2是基於圖層1作畫,黃色還是半透明 
 
運行了一次canvas.restore();,去掉棧頂一個圖層即圖層1露出畫布,所以圖層2是直接在畫布上作畫,黃色不透明 


saveFlags
上面我們只是粗暴的使用了ALL_SAVE_FLAG來保存的所有的信息,但是實際使用中,所有信息都保存必然增加了開銷,所以,我們應該根據需要的動作,儘量的精確的保存少量的信息。這裏就需要了解各個flag的意義。

首先需要知道的是,使用flag的方法除了saveFlayer還有save方法,他們都可以使用flag來指定需要保存的信息。那麼來看看6中flag所對應的意義:

Flag    意義    適用方法
MATRIX_SAVE_FLAG    只保存圖層的matrix矩陣    save,saveLayer
CLIP_SAVE_FLAG    只保存大小信息    save,saveLayer
HAS_ALPHA_LAYER_SAVE_FLAG    表明該圖層有透明度,和下面的標識衝突,都設置時以下面的標誌爲準    saveLayer
FULL_COLOR_LAYER_SAVE_FLAG    完全保留該圖層顏色(和上一圖層合併時,清空上一圖層的重疊區域,保留該圖層的顏色)    saveLayer
CLIP_TO_LAYER_SAVE_    創建圖層時,會把canvas(所有圖層)裁剪到參數指定的範圍,如果省略這個flag將導致圖層開銷巨大(實際上圖層沒有裁剪,與原圖層一樣大)    
ALL_SAVE_FLAG    保存所有信息    save,saveLayer
(1) MATRIX_SAVE_FLAG
只保存圖層的matrix矩陣。 
canvas中的哪些方法是利用matrix完成的,這裏需要明確,其實我們知道,canvas的繪製,最終是發生在bitmap上的,從canvas的構造函數中也可以看出。

在Bitmap的構造函數中可以看出bitmap的操作也是通過matrix來進行的:

Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter)
1
那麼我們可以知道canvas的canvas.translate(平移)、canvas.rotate(旋轉)、canvas.scale(縮放)、canvas.skew(扭曲)其實都是通過matrix來達到的,這一點可以在代碼中使用MATRIX_SAVE_FLAG來進行驗證。

save方法
這裏舉例平移:

paint.setColor(Color.BLUE);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(200, 200);
canvas.drawRect(100, 100, 300, 300, paint);
canvas.restore();

paint.setColor(Color.RED);
canvas.drawRect(100, 100, 300, 300, paint);
1
2
3
4
5
6
7
8


可以看到平移效果得到了保存,並且可以恢復。

saveLayer方法
paint.setColor(Color.BLUE);
int count=canvas.saveLayer(0,0,1000,1000,paint,Canvas.MATRIX_SAVE_FLAG|Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
canvas.translate(200, 200);
canvas.drawRect(100, 100, 300, 300, paint);
canvas.restoreToCount(count);

paint.setColor(Color.RED);
canvas.drawRect(100, 100, 300, 300, paint);
1
2
3
4
5
6
7
8
 
圖中看出效果相同

如果這裏不使用MATRIX_SAVE_FLAG標誌位,那麼是否會出現不同的效果呢,使用CLIP_SAVE_FLAG標誌來試試:

paint.setColor(Color.BLUE);
int count=canvas.saveLayer(0,0,1000,1000,paint,Canvas.CLIP_SAVE_FLAG|Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
canvas.translate(200, 200);
canvas.drawRect(100, 100, 300, 300, paint);
canvas.restoreToCount(count);

paint.setColor(Color.RED);
canvas.drawRect(100, 100, 300, 300, paint);
1
2
3
4
5
6
7
8


代碼和上面基本相同,只是標誌位改變了,這裏可以看到兩個圖重疊了,也就是說CLIP_SAVE_FLAG標誌位並沒有保存相關的位移信息,導致restore的時候沒能恢復。

(2) CLIP_SAVE_FLAG
看了上面的MATRIX_SAVE_FLAG,這裏的意義基本知道,主要就是保存裁剪相關的信息。 
由於和上面的示例基本類似,這裏就不再做講解了。

(3) FULL_COLOR_LAYER_SAVE_FLAG 和 HAS_ALPHA_LAYER_SAVE_FLAG
這兩個方法是saveLayer專用的方法,HAS_ALPHA_LAYER_SAVE_FLAG爲layer添加一個透明通道,這樣一來沒有繪製的地方就是透明的,覆蓋到上一個layer的時候,就會顯示出上一層的圖像。而FULL_COLOR_LAYER_SAVE_FLAG 則會完全展示當前layer的圖像,清除掉上一層的重合圖像。

來看看FULL_COLOR_LAYER_SAVE_FLAG 的示例:

canvas.drawColor(Color.RED);

canvas.saveLayer(200,200,700,700,mPaint,Canvas.FULL_COLOR_LAYER_SAVE_FLAG);
mPaint.setColor(Color.GREEN);
canvas.drawRect(300,300,600,600,mPaint);
canvas.restore();
1
2
3
4
5
6


可以看到,綠色的方塊周圍有白色的一圈,整個白色加上綠色區域是這個layer的區域,由於這裏使用了FULL_COLOR_LAYER_SAVE_FLAG標誌,所以這塊區域的紅色被layer層完全覆蓋(即使是透明),由於綠色周圍的顏色是透明的,所以在清除了紅色並覆蓋後,就顯示出了activity的背景顏色,所以顯示了白色。

如果activity背景是黑色,這一塊自然變爲黑色:

那麼其他代碼不變,只是將標誌位替換成HAS_ALPHA_LAYER_SAVE_FLAG會發生什麼:

可以看到,綠色周圍的白色不見了,可見,這就是區別。使用這個標誌位不會清空上一圖層的內容。

(4) CLIP_TO_LAYER_SAVE_
這個標誌比較重要,官方的建議是,最好不要忽略這個標識,這個標識如果不設置將會帶來很大的性能問題。

這個標識的作用是將canvas裁剪到指定的大小,並且無法回覆。看下面一個例子:

canvas.drawColor(Color.RED);
canvas.saveLayer(200,200,700,700,mPaint,Canvas.CLIP_TO_LAYER_SAVE_FLAG);
canvas.drawColor(Color.GREEN);
canvas.restore();
canvas.drawColor(Color.BLACK);
1
2
3
4
5


這裏看,先將底色繪製爲紅色,然後開啓新圖層,再繪製爲綠色,最後將canvas繪製爲黑色,爲什麼最後不是全屏黑色呢,這裏明明restore了,這是因爲使用了CLIP_TO_LAYER_SAVE_FLAG標誌,這樣一來,canvas被裁剪了,並且無法回覆了。這樣也就減少了處理的區域,增加了性能。

習題
畫一個錶盤
 
鬧鐘表盤其實就是在一個圓周上繪製;

既然是圓周,最簡單的方式莫過於在鬧鐘的12點鐘處劃線,通過canvas的旋轉繪製到對應圓周處,我們一起實現一下:

整個圓周是360 度,每隔 30 度爲一個整時間刻度,整刻度與刻度之間有四個短刻度,劃分出5個小段,每個段爲6度,有了這些分析,我們則可以採用如下代碼進行繪製:

    /* 繪製刻度 */
    private void drawLines(Canvas canvas) {
        for (int degree = 0; degree <= 360; degree++) {
            if (degree % 30 == 0) {
                //時針
                mLineBottom = mLineTop + mHourLineHeight;
                mLinePaint.setStrokeWidth(mHourLineWidth);
            } else {
                mLineBottom = mLineTop + mMinuteLineHeight;
                mLinePaint.setStrokeWidth(mMinuteLineWidth);
            }

            if (degree % 6 == 0) {
                canvas.save();
                canvas.rotate(degree, mCenterX, mCenterY);
                canvas.drawLine(mLineLeft, mLineTop, mLineLeft, mLineBottom, mLinePaint);
                canvas.restore();
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
整體代碼如下:

/* 錶盤 */
public class Dial extends View {
    private static final int HOUR_LINE_HEIGHT = 35;
    private static final int MINUTE_LINE_HEIGHT = 25;
    private Paint mCirclePaint, mLinePaint;
    private DrawFilter mDrawFilter;
    //圓心(錶盤中心)
    private int mCenterX, mCenterY, mCenterRadius;

    // 圓環線寬度
    private int mCircleLineWidth;
    // 直線刻度線寬度
    private int mHourLineWidth, mMinuteLineWidth;
    // 時針長度
    private int mHourLineHeight;
    // 分針長度
    private int mMinuteLineHeight;
    // 刻度線的左、上位置
    private int mLineLeft, mLineTop;

    // 刻度線的下邊位置
    private int mLineBottom;
    // 用於控制刻度線位置
    private int mFixLineHeight;

    public Dial(Context context) {
        this(context, null);
    }

    public Dial(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Dial(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
                | Paint.FILTER_BITMAP_FLAG);

        mCircleLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
                getResources().getDisplayMetrics());
        mHourLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4,
                getResources().getDisplayMetrics());
        mMinuteLineWidth = mHourLineWidth / 2;

        mFixLineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4,
                getResources().getDisplayMetrics());

        mHourLineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                HOUR_LINE_HEIGHT,
                getResources().getDisplayMetrics());
        mMinuteLineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                MINUTE_LINE_HEIGHT,
                getResources().getDisplayMetrics());
        initPaint();
    }

    private void initPaint() {
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setColor(Color.RED);
        mCirclePaint.setStyle(Paint.Style.STROKE);
        mCirclePaint.setStrokeWidth(mCircleLineWidth);

        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setColor(Color.RED);
        mLinePaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mLinePaint.setStrokeWidth(mHourLineWidth);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(mDrawFilter);
        super.onDraw(canvas);
        // 繪製錶盤
        drawCircle(canvas);
        // 繪製刻度
        drawLines(canvas);
    }

    /* 繪製刻度 */
    private void drawLines(Canvas canvas) {
        for (int degree = 0; degree <= 360; degree++) {
            if (degree % 30 == 0) {
                //時針
                mLineBottom = mLineTop + mHourLineHeight;
                mLinePaint.setStrokeWidth(mHourLineWidth);
            } else {
                mLineBottom = mLineTop + mMinuteLineHeight;
                mLinePaint.setStrokeWidth(mMinuteLineWidth);
            }

            if (degree % 6 == 0) {
                canvas.save();
                canvas.rotate(degree, mCenterX, mCenterY);
                canvas.drawLine(mLineLeft, mLineTop, mLineLeft, mLineBottom, mLinePaint);
                canvas.restore();
            }
        }
    }

    /* 繪製錶盤 */
    private void drawCircle(Canvas canvas) {
        canvas.drawCircle(mCenterX, mCenterY, mCenterRadius, mCirclePaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterX = w / 2;
        mCenterY = h / 2;
        mCenterRadius = Math.min(mCenterX, mCenterY) - mCircleLineWidth / 2;

        mLineLeft = mCenterX - mMinuteLineWidth / 2;
        mLineTop = mCenterY - mCenterRadius;
    }
}
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
畫一個正方形螺旋圖


思路非常的簡單: 
1. 繪製一個和屏幕等寬的正方形; 
2. 將畫布以正方形中心爲基準點進行縮放; 
3. 在縮放的過程中繪製原正方形;

注:每次繪製都得使用canvas.save() 和 canvas.restore()進行畫布的鎖定和回滾,以免除對後面繪製的影響。

先初始化畫筆,注意此時畫筆需要設置成空心:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(8);
    int l = 10;
    int t = 10;
    int r = 410;
    int b = 410;
    int space = 30;
    Rect squareRect = new Rect(l, t, r, b);
    int squareCount = (r - l) / space;
    float px = l + (r - l) / 2;
    float py = t + (b - t) / 2;
    for (int i = 0; i < squareCount; i++) {
        // 保存畫布
        canvas.save();
        float fraction = (float) i / squareCount;
        // 將畫布以正方形中心進行縮放
        canvas.scale(fraction, fraction, px, py);
        canvas.drawRect(squareRect, paint);
        // 畫布回滾
        canvas.restore();
    }
}
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
一起來看下繪製的效果: 


引用:
自定義控件之繪圖篇(四):canvas變換與操作 
自定義View之繪圖篇(六):Canvas那些你應該知道的變換 
Canvas之translate、scale、rotate、skew方法講解! 
Android 2D Graphics學習(二)、Canvas篇1、Canvas基本使用 
android canvas layer (圖層)詳解與進階

綜合習題
實現圖片圓角帶邊框的效果


@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Bitmap rawBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.cat);

    Bitmap bitmap = getRoundCornerBitmap(rawBitmap, 50);
    canvas.drawBitmap(bitmap, 0, 0, new Paint());
}

/**
 * @param bitmap 原圖
 * @param pixels 圓角大小
 * @return
 */
public Bitmap getRoundCornerBitmap(Bitmap bitmap, float pixels) {
//獲取bitmap的寬高
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();

    Bitmap cornerBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Paint paint = new Paint();
    Canvas canvas = new Canvas(cornerBitmap);
    paint.setAntiAlias(true);

    canvas.drawRoundRect(new RectF(0, 0, width, height), pixels, pixels, paint);
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(bitmap, null, new RectF(0, 0, width, height), paint);

//繪製邊框
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(6);
    paint.setColor(Color.GREEN);
    canvas.drawRoundRect(new RectF(0, 0, width, height), pixels, pixels, paint);

    return cornerBitmap;
}
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
1、首先通過Bitmap cornerBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);生成cornerBitmap 實例,注意了Bitmap 只能通過靜態方法來獲取它的實例,並不能直接 new出來。

2、繪製圓角矩形canvas.drawRoundRect(new RectF(0, 0, width, height), pixels, pixels, paint);。

3、爲Paint設置PorterDuffXfermode。參數 PorterDuff.Mode.SRC_IN 取交集。

4、繪製原圖。canvas.drawBitmap(bitmap, null, new RectF(0, 0, width, height), paint);

5、繪製邊框圓角。 canvas.drawRoundRect(new RectF(0, 0, width, height), pixels, pixels, paint);

引用:
自定義View之繪圖篇(六):Canvas那些你應該知道的變換

相關文章:

《Android自定義控件三部曲文章索引》: http://blog.csdn.net/harvic880925/article/details/50995268
--------------------- 
作者:moira33 
來源:CSDN 
原文:https://blog.csdn.net/moira33/article/details/79111343 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章