本文是自定義View的第二篇,主要學習:Paint
第一篇地址Android自定義View(一) -- 初識
本文計劃根據HenCoder系列文章進行學習,所以代碼風格及博文素材可能會摘自其中。
第一篇中,學習使用Paint 的常用方法設置各種屬性,配合drawxxx()方法繪制各種簡單View,本篇主要對Paint進行詳細了解,做出更細致、絢麗的效果。
進入正題,Paint的API主要分為以下4種:
- 顏色
- 效果
- 初始化
- drawText()相關
下面開始逐個學習:
1. 顏色:
Canvas繪制時,對顏色有三層處理:
1.1 基本顏色:
像素的基本顏色,根據繪制內容的不同而有不同的控制方式:
Canvas 的顏色填充類方法 drawColor/RGB/ARGB() 的顏色,是直接寫在方法的參數里,通過參數來設置的(上期講過了);
drawBitmap() 的顏色,是直接由 Bitmap 對象來提供的(上期也講過了);
除此之外,是圖形和文字的繪制,它們的顏色就需要使用 paint 參數來額外設置了(下面要講的)
1.1.1 直接設置顏色:
1.1.1.1 setColor(int color)
setColor() 對應的 get 方法是 getColor()
1.1.1.2 setARGB(int a, int r, int g, int b)
和 setColor(color) 一樣,它的參數用的是更直接的三原色與透明度的值。實際運用中,setColor() 和 setARGB() 哪個方便和順手用哪個吧
1.1.2 setShader(Shader shader) 設置 Shader
Shader 這個英文單詞很多人沒有見過,它的中文叫做「著色器」,也是用于設置繪制顏色的。「著色器」不是 Android 獨有的,它是圖形領域里一個通用的概念,它和直接設置顏色的區別是,著色器設置的是一個顏色方案,或者說是一套著色規則。當設置了 Shader 之后,Paint 在繪制圖形和文字時就不使用 setColor/ARGB() 設置的顏色了,而是使用 Shader 的方案中的顏色。
注意:在設置了 Shader 的情況下, Paint.setColor/ARGB() 所設置的顏色就不再起作用。
在 Android 的繪制里使用 Shader ,并不直接用 Shader 這個類,而是用它的幾個子類。具體來講有 LinearGradient RadialGradient SweepGradient BitmapShader ComposeShader 這么幾個:
1.1.2.1 LinearGradient 線性漸變:
構造方法:
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile) 。
參數:
x0 y0 x1 y1:漸變的兩個端點的位置
color0 color1 是端點的顏色
tile:端點范圍之外的著色規則,類型是 TileMode。TileMode 一共有 3 個值可選: CLAMP, MIRROR 和 REPEAT。CLAMP (夾子模式???算了這個詞我不會翻)會在端點之外延續端點處的顏色;MIRROR 是鏡像模式;REPEAT 是重復模式。具體的看一下例子就明白。
1.1.2.2 RadialGradient 輻射漸變
輻射漸變很好理解,就是從中心向周圍輻射狀的漸變
構造方法:
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)。
參數:
centerX centerY:輻射中心的坐標
radius:輻射半徑
centerColor:輻射中心的顏色
edgeColor:輻射邊緣的顏色
tileMode:輻射范圍之外的著色模式。
1.1.2.3 SweepGradient 掃描漸變:
構造方法:
SweepGradient(float cx, float cy, int color0, int color1)
參數:
cx cy :掃描的中心
color0:掃描的起始顏色
color1:掃描的終止顏色
1.1.2.4 BitmapShader
用 Bitmap 來著色其實是用 Bitmap 的像素來作為圖形或文字的填充。大概像這樣:
構造方法:
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
參數:
bitmap:用來做模板的 Bitmap 對象
tileX:橫向的 TileMode
tileY:縱向的 TileMode。
1.1.2.5 ComposeShader 混合著色器
即兩個Shader混合使用
這是沒有合適logo圖片,以下部分用原博文中素材
注意:上面這段代碼中我使用了兩個 BitmapShader 來作為 ComposeShader() 的參數,而 ComposeShader() 在硬件加速下是不支持兩個相同類型的 Shader 的,所以這里也需要關閉硬件加速才能看到效果。
構造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
參數:
shaderA, shaderB:兩個相繼使用的 Shader
mode: 兩個 Shader 的疊加模式,即 shaderA 和 shaderB 應該怎樣共同繪制。它的類型是 PorterDuff.Mode
PorterDuff.Mode
PorterDuff.Mode 是用來指定兩個圖像共同繪制時的顏色策略的。它是一個 enum,不同的 Mode 可以指定不同的策略。「顏色策略」的意思,就是說把源圖像繪制到目標圖像處時應該怎樣確定二者結合后的顏色,而對于 ComposeShader(shaderA, shaderB, mode) 這個具體的方法,就是指應該怎樣把 shaderB 繪制在 shaderA 上來得到一個結合后的 Shader。沒有聽說過 PorterDuff.Mode 的人,看到這里很可能依然會一頭霧水:「什么怎么結合?就……兩個圖像一疊加,結合唄?還能怎么結合?」你還別說,還真的是有很多種策略來結合。
最符合直覺的結合策略,就是我在上面這個例子中使用的 Mode: SRC_OVER。它的算法非常直觀:就像上面圖中的那樣,把源圖像直接鋪在目標圖像上。不過,除了這種,其實還有一些其他的結合方式。例如如果我把上面例子中的參數 mode 改為 PorterDuff.Mode.DST_OUT,就會變成挖空效果:
image.png
而如果再把 mode 改為 PorterDuff.Mode.DST_IN,就會變成蒙版摳圖效果:
image.png
具體來說,
PorterDuff.Mode
一共有 17 個,可以分為兩類:
- Alpha 合成 (Alpha Compositing)
- 混合 (Blending)
第一類,Alpha 合成,其實就是 「PorterDuff」 這個詞所指代的算法。 「PorterDuff」 并不是一個具有實際意義的詞組,而是兩個人的名字(準確講是姓)。這兩個人當年共同發表了一篇論文,描述了 12 種將兩個圖像共同繪制的操作(即算法)。而這篇論文所論述的操作,都是關于 Alpha 通道(也就是我們通俗理解的「透明度」)的計算的,后來人們就把這類計算稱為Alpha 合成 ( Alpha Compositing ) 。
看下效果吧。效果直接盜 Google 的官方文檔了。
源圖像和目標圖像:
image.png
Alpha 合成:
image.png
image.png
第二類,混合,也就是 Photoshop 等制圖軟件里都有的那些混合模式(multiply darken lighten 之類的)。這一類操作的是顏色本身而不是 Alpha 通道,并不屬于 Alpha 合成,所以和 Porter 與 Duff 這兩個人也沒什么關系,不過為了使用的方便,它們同樣也被 Google 加進了 PorterDuff.Mode 里。
效果依然盜 官方文檔。
image.png
結論
從效果圖可以看出,Alpha 合成類的效果都比較直觀,基本上可以使用簡單的口頭表達來描述它們的算法(起碼對于不透明的源圖像和目標圖像來說是可以的),例如 SRC_OVER 表示「二者都繪制,但要源圖像放在目標圖像的上面」,DST_IN 表示「只繪制目標圖像,并且只繪制它和源圖像重合的區域」。
而混合類的效果就相對抽象一些,只從效果圖不太能看得出它們的著色算法,更看不出來它們有什么用。不過沒關系,你如果拿著這些名詞去問你司的設計師,他們八成都能給你說出來個 123。
所以對于這些 Mode,正確的做法是:對于 Alpha 合成類的操作,掌握他們,并在實際開發中靈活運用;而對于混合類的,你只要把它們的名字記住就好了,這樣當某一天設計師告訴你「我要做這種混合效果」的時候,你可以馬上知道自己能不能做,怎么做。
另外:PorterDuff.Mode 建議你動手用一下試試,對加深理解有幫助。
好了,這些就是幾個 Shader 的具體介紹。
除了使用 setColor/ARGB() 和 setShader() 來設置基本顏色, Paint 還可以來設置 ColorFilter,來對顏色進行第二層處理。
1.2 setColorFilter(ColorFilter colorFilter)
ColorFilter 這個類,它的名字已經足夠解釋它的作用:為繪制設置顏色過濾。顏色過濾的意思,就是為繪制的內容設置一個統一的過濾策略,然后 Canvas.drawXXX() 方法會對每個像素都進行過濾后再繪制出來
在 Paint 里設置 ColorFilter ,使用的是 Paint.setColorFilter(ColorFilter filter) 方法。 ColorFilter 并不直接使用,而是使用它的子類。它共有三個子類:LightingColorFilter PorterDuffColorFilter 和 ColorMatrixColorFilter
1.2.1 LightingColorFilter
這個 LightingColorFilter 是用來模擬簡單的光照效果的。
LightingColorFilter 的構造方法是 LightingColorFilter(int mul, int add) ,參數里的 mul 和 add 都是和顏色值格式相同的 int 值,其中 mul 用來和目標像素相乘,add 用來和目標像素相加:
一個「保持原樣」的「基本 LightingColorFilter 」,mul 為 0xffffff,add 為 0x000000(也就是0),那么對于一個像素,它的計算過程就是:
基于這個「基本 LightingColorFilter 」,你就可以修改一下做出其他的 filter。比如,如果你想去掉原像素中的紅色,可以把它的 mul 改為 0x00ffff (紅色部分為 0 ) ,那么它的計算過程就是:
具體效果是這樣的:
如果想讓去掉綠色,就可以把它的 mul 改為 0xff00ff (綠色部分為 0),效果就是這樣:
具體效果,還是需要根據設計師給的圖來做吧
1.2.2 PorterDuffColorFilter
這個 PorterDuffColorFilter 的作用是使用一個指定的顏色和一種指定的 PorterDuff.Mode 來與繪制對象進行合成。它的構造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的 color 參數是指定的顏色, mode 參數是指定的 Mode。同樣也是 PorterDuff.Mode ,不過和 ComposeShader 不同的是,PorterDuffColorFilter 作為一個 ColorFilter,只能指定一種顏色作為源,而不是一個 Bitmap。
PorterDuff.Mode 前面已經講過了,而 PorterDuffColorFilter 本身的使用是非常簡單的,所以不再展開講。
1.2.3 ColorMatrixColorFilter
ColorMatrixColorFilter 使用一個 ColorMatrix 來對顏色進行處理。 ColorMatrix 這個類,內部是一個 4x5 的矩陣:
[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]
通過計算, ColorMatrix 可以把要繪制的像素進行轉換。對于顏色 [R, G, B, A] ,轉換算法是這樣的:
R’ = a*R + b*G + c*B + d*A + e;
G’ = f*R + g*G + h*B + i*A + j;
B’ = k*R + l*G + m*B + n*A + o;
A’ = p*R + q*G + r*B + s*A + t;
ColorMatrix
有一些自帶的方法可以做簡單的轉換,例如可以使用 setSaturation(float sat)
來設置飽和度;另外你也可以自己去設置它的每一個元素來對轉換效果做精細調整。具體怎樣設置會有怎樣的效果,我就不講了(其實是我也不太會??)。如果你有需求,可以試一下程大治同學做的這個庫:StyleImageView
以上,就是 Paint 對顏色的第二層處理:通過 setColorFilter(colorFilter) 來加工顏色。
除了基本顏色的設置( setColor/ARGB(), setShader() )以及基于原始顏色的過濾( setColorFilter() )之外,Paint 最后一層處理顏色的方法是 setXfermode(Xfermode xfermode) ,它處理的是「當顏色遇上 View」的問題。
1.3 setXfermode(Xfermode xfermode)
"Xfermode" 其實就是 "Transfer mode",用 "X" 來代替 "Trans" 是一些美國人喜歡用的簡寫方式。嚴謹地講, Xfermode 指的是你要繪制的內容和 Canvas 的目標位置的內容應該怎樣結合計算出最終的顏色。但通俗地說,其實就是要你以繪制的內容作為源圖像,以 View 中已有的內容作為目標圖像,選取一個 PorterDuff.Mode 作為繪制內容的顏色處理方案。就像這樣:
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
...
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 畫方
paint.setXfermode(xfermode); // 設置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 畫圓
paint.setXfermode(null); // 用完及時清除 Xfermode
又是 PorterDuff.Mode 。 PorterDuff.Mode 在 Paint 一共有三處 API ,它們的工作原理都一樣,只是用途不同:
另外,從上面的示例代碼可以看出,創建 Xfermode 的時候其實是創建的它的子類 PorterDuffXfermode。而事實上,Xfermode 也只有這一個子類。所以在設置 Xfermode 的時候不用多想,直接用 PorterDuffXfermode 吧。
其實在更早的 Android 版本中,Xfermode 還有別的子類,但別的子類現在已經 deprecated 了,如今只剩下了 PorterDuffXfermode。所以目前它的使用看起來好像有點啰嗦,但其實是由于歷史遺留問題。
Xfermode 注意事項
Xfermode 使用很簡單,不過有兩點需要注意:
1. 使用離屏緩沖(Off-screen Buffer)
實質上,上面這段例子代碼,如果直接執行的話是不會繪制出圖中效果的,程序的繪制也不會像上面的動畫那樣執行,而是會像這樣:
按照邏輯我們會認為,在第二步畫圓的時候,跟它共同計算的是第一步繪制的方形。但實際上,卻是整個 View 的顯示區域都在畫圓的時候參與計算,并且 View 自身的底色并不是默認的透明色,而且是遵循一種迷之邏輯,導致不僅繪制的是整個圓的范圍,而且在范圍之外都變成了黑色。就像這樣:
這……那可如何是好?
要想使用 setXfermode() 正常繪制,必須使用離屏緩存 (Off-screen Buffer) 把內容繪制在額外的層上,再把繪制好的內容貼回 View 中。也就是這樣:
通過使用離屏緩沖,把要繪制的內容單獨繪制在緩沖層, Xfermode 的使用就不會出現奇怪的結果了。使用離屏緩沖有兩種方式:
- Canvas.saveLayer()
saveLayer() 可以做短時的離屏緩沖。使用方法很簡單,在繪制代碼的前后各加一行代碼,在繪制之前保存,繪制之后恢復:
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 畫方
paint.setXfermode(xfermode); // 設置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 畫圓
paint.setXfermode(null); // 用完及時清除 Xfermode
canvas.restoreToCount(saved);
- View.setLayerType()
View.setLayerType() 是直接把整個 View 都繪制在離屏緩沖中。 setLayerType(LAYER_TYPE_HARDWARE) 是使用 GPU 來緩沖, setLayerType(LAYER_TYPE_SOFTWARE) 是直接直接用一個 Bitmap 來緩沖。
關于 Canvas.saveLayer() 和 View.setLayerType() ,這里就不細講它們的意義和原理了,后面也許我會專門用一期來講它們。
如果沒有特殊需求,可以選用第一種方法 Canvas.saveLayer()
來設置離屏緩沖,以此來獲得更高的性能。更多關于離屏緩沖的信息,可以看官方文檔中對于硬件加速的介紹。
2. 控制好透明區域
使用 Xfermode 來繪制的內容,除了注意使用離屏緩沖,還應該注意控制它的透明區域不要太小,要讓它足夠覆蓋到要和它結合繪制的內容,否則得到的結果很可能不是你想要的。我用圖片來具體說明一下:
好,到此為止,前面講的就是 Paint 的第一類 API——關于顏色的三層設置:直接設置顏色的 API 用來給圖形和文字設置顏色; setColorFilter() 用來基于顏色進行過濾處理; setXfermode() 用來處理源圖像和 View 已有內容的關系。
再貼一次本章開始處的圖作為回顧:
2 效果
效果類的 API ,指的就是抗鋸齒、填充/輪廓、線條寬度等等這些。
2.1 setAntiAlias (boolean aa) 設置抗鋸齒
抗鋸齒在上一節已經講過了,話不多說,直接上圖:
抗鋸齒默認是關閉的,如果需要抗鋸齒,需要顯式地打開。另外,除了 setAntiAlias(aa) 方法,打開抗鋸齒還有一個更方便的方式:構造方法。創建 Paint 對象的時候,構造方法的參數里加一個Paint.ANTI_ALIAS_FLAG,就可以在初始化的時候就開啟抗鋸齒。
paint1 = new Paint(Paint.ANTI_ALIAS_FLAG)
2.2 setStyle(Paint.Style style)
setStyle(style) 也在上一節講過了,用來設置圖形是線條風格還是填充風格的(也可以二者并用):
paint.setStyle(Paint.Style.FILL); // FILL 模式,填充
canvas.drawCircle(300, 300, 200, paint);
paint.setStyle(Paint.Style.STROKE); // STROKE 模式,畫線
canvas.drawCircle(300, 300, 200, paint);
FILL 模式是默認模式,所以如果之前沒有設置過其他的 Style,可以不用 setStyle(Paint.Style.FILL) 這句。
2.3 線條形狀
設置線條形狀的一共有 4 個方法:setStrokeWidth(float width), setStrokeCap(Paint.Cap cap), setStrokeJoin(Paint.Join join), setStrokeMiter(float miter) 。
2.3.1 setStrokeWidth(float width)
設置線條寬度。單位為像素,默認值是 0。
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1);
canvas.drawCircle(150, 125, 100, paint);
paint.setStrokeWidth(5);
canvas.drawCircle(400, 125, 100, paint);
paint.setStrokeWidth(40);
canvas.drawCircle(650, 125, 100, paint);
2.3.2 setStrokeCap(Paint.Cap cap)
設置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認為 BUTT。
放出「平頭」「圓頭」「方頭」這種翻譯我始終有點糾結:既覺得自己翻譯得簡潔清晰盡顯機智,同時又擔心用詞會不會有點太過通俗,讓人覺得我不夠高貴冷艷?
當線條的寬度是 1 像素時,這三種線頭的表現是完全一致的,全是 1 個像素的點;而當線條變粗的時候,它們就會表現出不同的樣子:
虛線是額外加的,虛線左邊是線的實際長度,虛線右邊是線頭。有了虛線作為輔助,可以清楚地看出 BUTT 和 SQUARE 的區別。
2.3.3 setStrokeJoin(Paint.Join join)
設置拐角的形狀。有三個值可以選擇:默認為 MITER。
- MITER 尖角
- BEVEL 平角
- ROUND 圓角
paint.setStrokeJoin(Paint.Join.BEVEL);
float[] lines = {100,300,500,500,500,500,800,100,800,100,600,1000};
canvas.drawLines(lines,paint);
輔助理解:
MITER 在現實中其實就是這玩意:
image.png
而 BEVEL 是這玩意:
image.png
2.3.4 setStrokeMiter(float miter)
這個方法是對于 setStrokeJoin() 的一個補充,它用于設置 MITER 型拐角的延長線的最大值。所謂「延長線的最大值」,是這么一回事:
當線條拐角為 MITER 時,拐角處的外緣需要使用延長線來補償:
而這種補償方案會有一個問題:如果拐角的角度太小,就有可能由于出現連接點過長的情況。比如這樣:
所以為了避免意料之外的過長的尖角出現, MITER
型連接點有一個額外的規則:當尖角過長時,自動改用 BEVEL
的方式來渲染連接點。例如上圖的這個尖角,在默認情況下是不會出現的,而是會由于延長線過長而被轉為 BEVEL
型連接點:
至于多尖的角屬于過于尖,尖到需要轉為使用 BEVEL
來繪制,則是由一個屬性控制的,而這個屬性就是 setStrokeMiter(miter)
方法中的 miter
參數。miter
參數是對于轉角長度的限制,具體來講,是指尖角的外緣端點和內部拐角的距離與線條寬度的比。也就是下面這兩個長度的比:
用幾何知識很容易得出這個比值的計算公式:如果拐角的大小為 θ ,那么這個比值就等于 1 / sin ( θ / 2 ) 。
這個 miter limit 的默認值是 4,對應的是一個大約 29° 的銳角:
默認情況下,大于這個角的尖角會被保留,而小于這個夾角的就會被「削成平頭」
所以,這個方法雖然名叫
setStrokeMiter(miter)
,但它其實設置的是「 線條在Join
類型為MITER
時對于MITER
的長度限制」。它的這個名字雖然短,但卻存在一定的迷惑性,如果叫setStrokeJoinMiterLimit(limit)
就更準確了。 Google 的工程師沒有這么給它命名,大概也是不想傷害大家的手指吧,畢竟程序員何苦為難程序員。image
以上就是 4 個關于線條形狀的方法: setStrokeWidth(width)
setStrokeCap(cap)``setStrokeJoint(join)
和 setStrokeMiter(miter)
。
2.4 色彩優化
Paint 的色彩優化有兩個方法: setDither(boolean dither) 和 setFilterBitmap(boolean filter) 。它們的作用都是讓畫面顏色變得更加「順眼」,但原理和使用場景是不同的。
2.4.1 setDither(boolean dither)
設置圖像的抖動。
抖動(注意,它就叫抖動,不是防抖動,也不是去抖動,有些人在翻譯的時候自作主張地加了一個「防」字或者「去」字,這是不對的),是指把圖像從較高色彩深度(即可用的顏色數)向較低色彩深度的區域繪制時,在圖像中有意地插入噪點,通過有規律地擾亂圖像來讓圖像對于肉眼更加真實的做法。
比如向 1 位色彩深度的區域中繪制灰色,由于 1 位深度只包含黑和白兩種顏色,在默認情況下,即不加抖動的時候,只能選擇向上或向下選擇最接近灰色的白色或黑色來繪制,那么顯示出來也只能是一片白或者一片黑。而加了抖動后,就可以繪制出讓肉眼識別為灰色的效果了:
瞧,像上面這樣,用黑白相間的方式來繪制,就可以騙過肉眼,讓肉眼辨別為灰色了。
嗯?你說你看不出灰色,只看出黑白相間?沒關系,那是因為像素顆粒太大,我把像素顆粒縮小,看到完整效果你就會發現變灰了:
不過,抖動可不只可以用在純色的繪制。在實際的應用場景中,抖動更多的作用是在圖像降低色彩深度繪制時,避免出現大片的色帶與色塊。效果盜一下維基百科的圖:
看著很牛逼對吧?確實很牛逼,而且在 Android 里使用起來也很簡單,一行代碼就搞定:
paint.setDither(true);
只要加這么一行代碼,之后的繪制就是加抖動的了。
不過對于現在(2017年)而言, setDither(dither) 已經沒有當年那么實用了,因為現在的 Android 版本的繪制,默認的色彩深度已經是 32 位的 ARGB_8888 ,效果已經足夠清晰了。只有當你向自建的 Bitmap 中繪制,并且選擇 16 位色的 ARGB_4444 或者 RGB_565 的時候,開啟它才會有比較明顯的效果。
2.4.2 setFilterBitmap(boolean filter)
設置是否使用雙線性過濾來繪制 Bitmap
。
圖像在放大繪制的時候,默認使用的是最近鄰插值過濾,這種算法簡單,但會出現馬賽克現象;而如果開啟了雙線性過濾,就可以讓結果圖像顯得更加平滑。效果依然盜維基百科的圖:
牛逼吧?而且它的使用同樣也很簡單:
paint.setFilterBitmap(true);
加上這一行,在放大繪制 Bitmap 的時候就會使用雙線性過濾了。
以上就是 Paint 的兩個色彩優化的方法: setDither(dither) ,設置抖動來優化色彩深度降低時的繪制效果; setFilterBitmap(filterBitmap) ,設置雙線性過濾來優化 Bitmap 放大繪制的效果。
2.5 setPathEffect(PathEffect effect)
使用 PathEffect 來給圖形的輪廓設置效果。對 Canvas 所有的圖形繪制有效,也就是 drawLine() drawCircle() drawPath() 這些方法。
下面就具體說一下 Android 中的 6 種 PathEffect。PathEffect 分為兩類,單一效果的 CornerPathEffect DiscretePathEffect DashPathEffect PathDashPathEffect ,和組合效果的 SumPathEffect ComposePathEffect。
2.5.1 CornerPathEffect
把所有拐角變成圓角。
PathEffect pathEffect = new CornerPathEffect(40);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
它的構造方法 CornerPathEffect(float radius) 的參數 radius 是圓角的半徑。
2.5.2 DiscretePathEffect
把線條進行隨機的偏離,讓輪廓變得亂七八糟。亂七八糟的方式和程度由參數決定。
PathEffect pathEffect = new DiscretePathEffect(20,5);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
DiscretePathEffect 具體的做法是,把繪制改為使用定長的線段來拼接,并且在拼接的時候對路徑進行隨機偏離。它的構造方法 DiscretePathEffect(float segmentLength, float deviation) 的兩個參數中, segmentLength 是用來拼接的每個線段的長度, deviation 是偏離量。這兩個值設置得不一樣,顯示效果也會不一樣,具體的你自己多試幾次就明白了,這里不再貼更多的圖。
2.5.3 DashPathEffect
使用虛線來繪制線條。
PathEffect pathEffect = new DashPathEffect(new float[]{20,10,5,10},0);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
它的構造方法 DashPathEffect(float[] intervals, float phase) 中, 第一個參數 intervals 是一個數組,它指定了虛線的格式:數組中元素必須為偶數(最少是 2 個),按照「畫線長度、空白長度、畫線長度、空白長度」……的順序排列,例如上面代碼中的 20, 5, 10, 5 就表示虛線是按照「畫 20 像素、空 5 像素、畫 10 像素、空 5 像素」的模式來繪制;第二個參數 phase 是虛線的偏移量。
2.5.4 PathDashPathEffect
這個方法比 DashPathEffect 多一個前綴 Path ,所以顧名思義,它是使用一個 Path 來繪制「虛線」。具體看圖吧:
path1 = new Path();
path1.lineTo(30,30);
path1.lineTo(50,10);
path1.close();
PathEffect pathEffect = new PathDashPathEffect(path1,40,0, PathDashPathEffect.Style.TRANSLATE);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
它的構造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 參數是用來繪制的 Path ; advance 是兩個相鄰的 shape 段之間的間隔,不過注意,這個間隔是兩個 shape 段的起點的間隔,而不是前一個的終點和后一個的起點的距離; phase 和 DashPathEffect 中一樣,是虛線的偏移;最后一個參數 style,是用來指定拐彎改變的時候 shape 的轉換方式。style 的類型為 PathDashPathEffect.Style ,是一個 enum ,具體有三個值:
TRANSLATE:位移
ROTATE:旋轉
MORPH:變體
2.5.5 SumPathEffect
這是一個組合效果類的 PathEffect 。它的行為特別簡單,就是分別按照兩種 PathEffect 分別對目標進行繪制。
PathEffect pathEffect1 = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);
PathEffect pathEffect2 = new DiscretePathEffect(20,5);
PathEffect pathEffect = new SumPathEffect(pathEffect1,pathEffect2);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
參數PathEffect first, PathEffect second,將兩個PathEffect組合使用
2.5.6 ComposePathEffect
這也是一個組合效果類的 PathEffect 。不過它是先對目標 Path 使用一個 PathEffect,然后再對這個改變后的 Path 使用另一個 PathEffect。
PathEffect pathEffect1 = new DashPathEffect(new float[]{40, 10, 20, 10}, 0);
PathEffect pathEffect2 = new DiscretePathEffect(20,5);
PathEffect pathEffect = new ComposePathEffect(pathEffect1,pathEffect2);
paint.setPathEffect(pathEffect);
canvas.drawPath(path,paint);
它的構造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的兩個 PathEffect 參數, innerpe 是先應用的, outerpe 是后應用的。所以上面的代碼就是「先偏離,再變虛線」。而如果把兩個參數調換,就成了「先變虛線,再偏離」。至于具體的視覺效果……我就不貼圖了,你自己試試看吧!
上面這些就是 Paint 中的 6 種 PathEffect。它們有的是有獨立效果的,有的是用來組合不同的 PathEffect 的,功能各不一樣。
注意: PathEffect 在有些情況下不支持硬件加速,需要關閉硬件加速才能正常使用:
Canvas.drawLine() 和 Canvas.drawLines() 方法畫直線時,setPathEffect() 是不支持硬件加速的;
PathDashPathEffect 對硬件加速的支持也有問題,所以當使用 PathDashPathEffect 的時候,最好也把硬件加速關了。
剩下的兩個效果類方法:setShadowLayer() 和 setMaskFilter() ,它們和前面的效果類方法有點不一樣:它們設置的是「附加效果」,也就是基于在繪制內容的額外效果。
2.6 setShadowLayer(float radius, float dx, float dy, int shadowColor)
在之后的繪制內容下面加一層陰影。
paint.setShadowLayer(10,0,0,Color.RED);
canvas.drawText("自定義View",500,1200,paint);
canvas.drawText("自定義View",500,1200,paint);
效果就是上面這樣。方法的參數里, radius 是陰影的模糊范圍; dx dy 是陰影的偏移量; shadowColor 是陰影的顏色。
如果要清除陰影層,使用 clearShadowLayer() 。
注意:
- 在硬件加速開啟的情況下, setShadowLayer() 只支持文字的繪制,文字之外的繪制必須關閉硬件加速才能正常繪制陰影。
- 如果 shadowColor 是半透明的,陰影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,陰影的透明度就使用 paint 的透明度。
2.7 setMaskFilter(MaskFilter maskfilter)
為之后的繪制設置 MaskFilter。上一個方法 setShadowLayer() 是設置的在繪制層下方的附加效果;而這個 MaskFilter 和它相反,設置的是在繪制層上方的附加效果。
到現在已經有兩個 setXxxFilter(filter) 了。前面有一個 setColorFilter(filter) ,是對每個像素的顏色進行過濾;而這里的 setMaskFilter(filter) 則是基于整個畫面來進行過濾。
MaskFilter 有兩種: BlurMaskFilter 和 EmbossMaskFilter。
2.7.1 BlurMaskFilter
模糊效果的 MaskFilter。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);
paint.setMaskFilter(new BlurMaskFilter(300, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(bitmap,100,200,paint);
它的構造方法 BlurMaskFilter(float radius, BlurMaskFilter.Blur style) 中, radius 參數是模糊的范圍, style 是模糊的類型。一共有四種:
NORMAL: 內外都模糊繪制
SOLID: 內部正常繪制,外部模糊
INNER: 內部模糊,外部不繪制
OUTER: 內部不繪制,外部模糊(什么鬼?)
2.7.2 EmbossMaskFilter
浮雕效果的 MaskFilter。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);
paint.setMaskFilter(new EmbossMaskFilter(new float[]{0, 1, 1}, 0.2f, 8, 10));
canvas.drawBitmap(bitmap,100,200,paint);
它的構造方法 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) 的參數里, direction 是一個 3 個元素的數組,指定了光源的方向; ambient 是環境光的強度,數值范圍是 0 到 1; specular 是炫光的系數; blurRadius 是應用光線的范圍。
2.8 獲取繪制的 Path
這是效果類的最后一組方法,也是效果類唯一的一組 get 方法。
這組方法做的事是,根據 paint 的設置,計算出繪制 Path 或文字時的實際 Path。
這里你可能會冒出兩個問題:
- 什么叫「實際 Path」? Path 就是 Path,這加上個「實際」是什么意思?
- 文字的 Path ?文字還有 Path?
這兩個問題(咦好像有四個問號)的答案就在后面的內容里。
2.8.1 getFillPath(Path src, Path dst)
首先解答第一個問題:「實際 Path」。所謂實際 Path ,指的就是 drawPath() 的繪制內容的輪廓,要算上線條寬度和設置的 PathEffect。
默認情況下(線條寬度為 0、沒有 PathEffect),原 Path 和實際 Path 是一樣的;而在線條寬度不為 0 (并且模式為 STROKE 模式或 FLL_AND_STROKE ),或者設置了 PathEffect 的時候,實際 Path 就和原 Path 不一樣了:
通過 getFillPath(src, dst) 方法就能獲取這個實際 Path。方法的參數里,src 是原 Path ,而 dst 就是實際 Path 的保存位置。 getFillPath(src, dst) 會計算出實際 Path,然后把結果保存在 dst 里。
2.8.2 getTextPath(String text, int start, int end, float x, float y, Path path) / getTextPath(char[] text, int index, int count, float x, float y, Path path)
這里就回答第二個問題:「文字的 Path」。文字的繪制,雖然是使用 Canvas.drawText() 方法,但其實在下層,文字信息全是被轉化成圖形,對圖形進行繪制的。 getTextPath() 方法,獲取的就是目標文字所對應的 Path 。這個就是所謂「文字的 Path」。
這兩個方法, getFillPath()
和 getTextPath()
,就是獲取繪制的 Path
的方法。之所以把它們歸類到「效果」類方法,是因為它們主要是用于圖形和文字的裝飾效果的位置計算,比如自定義的下劃線效果。
到此為止, Paint 的第二類方法——效果類,就也介紹完了。
3 drawText() 相關
Paint
有些設置是文字繪制相關的,即和 drawText()
相關的。
比如設置文字大小:
比如設置文字間隔:
比如設置各種文字效果:
除此之外,Paint
還有很多與文字繪制相關的設置或計算的方法,非常詳細。不過由于太詳細了,相關方法太多了(Paint
超過一半的方法都是 drawText()
相關的,算不算多?),如果放在這里講它們的話,內容會顯得有點過量。所以這一節我就不講它們了,把它們放在下一節里單獨講。
4 初始化類
這一類方法很簡單,它們是用來初始化 Paint
對象,或者是批量設置 Paint
的多個屬性的方法。
4.1 reset()
重置 Paint
的所有屬性為默認值。相當于重新 new
一個,不過性能當然高一些啦。
4.2 set(Paint src)
把 src
的所有屬性全部復制過來。相當于調用 src
所有的 get
方法,然后調用這個 Paint
的對應的 set
方法來設置它們。
4.3 setFlags(int flags)
批量設置 flags。相當于依次調用它們的 set
方法。例如: ???
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
這行代碼,和下面這兩行是等價的:
paint.setAntiAlias(true);
paint.setDither(true);
setFlags(flags)
對應的 get
方法是 int getFlags()
。
好了,這些就是 Paint
的四類方法:顏色類、效果類、文字繪制相關以及初始化類。其中顏色類、效果類和初始化類都已經在這節里面講過了,剩下的一類——文字繪制類,下一節單獨講。
最后再強調一遍:這期的內容沒必要全部背會,只要看懂、理解,記住有這么個東西就行了。以后在用到的時候,再拐回來翻一翻就行了。