Paint 屬性分類
- 顏色
- 效果
- drawText() 相關
- 初始化
1 顏色
Canvas 繪制的內容,有三層對顏色的處理:
1.1 基本顏色
1.1.1 直接設置顏色
1.1.1.1 setColor(int color)
paint.setColor(Color.parseColor("#009688"));
canvas.drawRect(30, 30, 230, 180, paint);
paint.setColor(Color.parseColor("#FF9800"));
canvas.drawLine(300, 30, 450, 180, paint);
paint.setColor(Color.parseColor("#E91E63"));
canvas.drawText("HenCoder", 500, 130, paint);
1.1.1.2 setARGB(int a, int r, int g, int b)
和 setColor(color) 都是一樣一樣兒的,只是它的參數用的是更直接的三原色與透明度的值。實際運用中,setColor() 和 setARGB() 哪個方便和順手用哪個吧。
1.1.2 setShader(Shader shader) 設置 Shader
- 著色器不是 Android 獨有的,它是圖形領域里一個通用的概念,它和直接設置顏色的區別是,著色器設置的是一個顏色方案,或者說是一套著色規則。
- 當設置了 Shader 之后,Paint 在繪制圖形和文字時就不使用 setColor/ARGB() 設置的顏色了,而是使用 Shader 的方案中的顏色。
- 在 Android 的繪制里使用 Shader ,并不直接用 Shader 這個類,而是用它的幾個子類。具體來講有 LinearGradient RadialGradient SweepGradient BitmapShader ComposeShader 這么幾個:
1.1.2.1 LinearGradient 線性漸變
設置兩個點和兩種顏色,以這兩個點作為端點,使用兩種顏色的漸變來繪制顏色。
就像這樣:
Shader shader = new LinearGradient(100, 100, 500, 500, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"), Shader.TileMode.CLAMP);
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);
構造方法:
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 是重復模式。具體的看一下例子就明白。
CLAMP:
MIRROR:
REPEAT:
1.1.2.2 RadialGradient 輻射漸變
輻射漸變很好理解,就是從中心向周圍輻射狀的漸變
Shader shader = new RadialGradient(300, 300, 200, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"), Shader.TileMode.CLAMP);
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);
構造方法:
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)。
參數:
centerX centerY:輻射中心的坐標
radius:輻射半徑
centerColor:輻射中心的顏色
edgeColor:輻射邊緣的顏色
tileMode:輻射范圍之外的著色模式。
TileMode 類型和LinearGradient的TitleMode是一樣的
1.1.2.3 SweepGradient 掃描漸變
掃描漸變
Shader shader = new SweepGradient(300, 300, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"));
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);
構造方法:
SweepGradient(float cx, float cy, int color0, int color1)
參數:
cx cy :掃描的中心
color0:掃描的起始顏色
color1:掃描的終止顏色
1.1.2.4 BitmapShader
用 Bitmap 來著色(終于不是漸變了)。其實也就是用 Bitmap 的像素來作為圖形或文字的填充
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.batman);
Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);
看著跟 Canvas.drawBitmap() 好像啊?事實上也是一樣的效果。如果你想繪制圓形的 Bitmap,就別用 drawBitmap() 了,改用 drawCircle() + BitmapShader 就可以了(其他形狀同理)。
構造方法:
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
參數:
bitmap:用來做模板的 Bitmap 對象
tileX:橫向的 TileMode
tileY:縱向的 TileMode。
1.1.2.5 ComposeShader 混合著色器
兩個 Shader 一起使用
// 第一個 Shader:頭像的 Bitmap
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.batman);
Shader shader1 = new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 第二個 Shader:從上到下的線性漸變(由透明到黑色)
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo);
Shader shader2 = new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// ComposeShader:結合兩個 Shader
Shader shader = new ComposeShader(shader1, shader2, PorterDuff.Mode.SRC_OVER);
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 300, paint);
ComposeShader() 在硬件加速下是不支持兩個相同類型的 Shader 的,所以這里也需要關閉硬件加速才能看到效果。
image.png
構造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
參數:
shaderA, shaderB:兩個相繼使用的 Shader
mode: 兩個 Shader 的疊加模式,即 shaderA 和 shaderB 應該怎樣共同繪制。它的類型是 PorterDuff.Mode
1.2 setColorFilter(ColorFilter colorFilter)
為繪制設置顏色過濾。顏色過濾的意思,就是為繪制的內容設置一個統一的過濾策略,然后 Canvas.drawXXX() 方法會對每個像素都進行過濾后再繪制出來。
舉幾個現實中比較常見的顏色過濾的例子:
-
有色光照射:
image.png -
有色玻璃透視:
image.png -
膠卷:
image.png
在 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 用來和目標像素相加:
R' = R * mul.R / 0xff + add.R
G' = G * mul.G / 0xff + add.G
B' = B * mul.B / 0xff + add.B
效果:
1.2.2 PorterDuffColorFilter
作用是使用一個指定的顏色和一種指定的 PorterDuff.Mode 來與繪制對象進行合成。
它的構造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的 color 參數是指定的顏色, mode 參數是指定的 Mode。同樣也是 PorterDuff.Mode ,不過和 ComposeShader 不同的是,PorterDuffColorFilter 作為一個 ColorFilter,只能指定一種顏色作為源,而不是一個 Bitmap。
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 可以把要繪制的像素進行轉換。
效果:
1.3 setXfermode(Xfermode xfermode)
- "Xfermode" 其實就是 "Transfer mode",用 "X" 來代替 "Trans" 是一些美國人喜歡用的簡寫方式
- 以繪制的內容作為源圖像,以 View 中已有的內容作為目標圖像,選取一個 PorterDuff.Mode 作為繪制內容的顏色處理方案。
效果:
創建 Xfermode 的時候其實是創建的它的子類 PorterDuffXfermode。而事實上,Xfermode 也只有這一個子類。所以在設置 Xfermode 的時候不用多想,直接用 PorterDuffXfermode 吧。
「只有一個子類???什么設計?」
實在更早的 Android 版本中,Xfermode 還有別的子類,但別的子類現在已經 deprecated 了,如今只剩下了 PorterDuffXfermode。所以目前它的使用看起來好像有點啰嗦,但其實是由于歷史遺留問題。
Xfermode 注意事項
1. 使用離屏緩沖(Off-screen Buffer)
- 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);
2. 控制好透明區域
使用 Xfermode 來繪制的內容,除了注意使用離屏緩沖,還應該注意控制它的透明區域不要太小,要讓它足夠覆蓋到要和它結合繪制的內容,否則得到的結果很可能不是你想要的。
2 效果
效果類的 API ,指的就是抗鋸齒、填充/輪廓、線條寬度等等這些。
2.1 setAntiAlias (boolean aa) 設置抗鋸齒
抗鋸齒在上一節已經講過了,話不多說,直接上圖:
另外,除了 setAntiAlias(aa) 方法,打開抗鋸齒還有一個更方便的方式:構造方法。創建 Paint 對象的時候,構造方法的參數里加一個 ANTI_ALIAS_FLAG 的 flag,就可以在初始化的時候就開啟抗鋸齒。
2.2 setStyle(Paint.Style style)
setStyle(style) 也在上一節講過了,用來設置圖形是線條風格還是填充風格的(也可以二者并用):
paint.setStyle(Paint.Style.FILL); // FILL 模式,填充
canvas.drawCircle(300, 300, 200, paint);
STROKE
FILL_AND_STROKE
2.3 線條形狀
設置線條形狀的一共有 4 個方法:setStrokeWidth(float width), setStrokeCap(Paint.Cap cap), setStrokeJoin(Paint.Join join), setStrokeMiter(float miter) 。
2.3.1 setStrokeWidth(float width)
2.3.2 setStrokeCap(Paint.Cap cap)
設置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認為 BUTT。
2.3.3 setStrokeJoin(Paint.Join join)
設置拐角的形狀。有三個值可以選擇:MITER 尖角、 BEVEL 平角和 ROUND 圓角。默認為 MITER。
2.3.4 setStrokeMiter(float miter)
這個方法是對于 setStrokeJoin() 的一個補充,它用于設置 MITER 型拐角的延長線的最大值。所謂「延長線的最大值」,是這么一回事:
當線條拐角為 MITER 時,拐角處的外緣需要使用延長線來補償:
而這種補償方案會有一個問題:如果拐角的角度太小,就有可能由于出現連接點過長的情況。比如這樣:
所以為了避免意料之外的過長的尖角出現, MITER 型連接點有一個額外的規則:當尖角過長時,自動改用 BEVEL 的方式來渲染連接點。例如上圖的這個尖角,在默認情況下是不會出現的,而是會由于延長線過長而被轉為 BEVEL 型連接點:
2.4 色彩優化
Paint 的色彩優化有兩個方法: setDither(boolean dither) 和 setFilterBitmap(boolean filter) 。它們的作用都是讓畫面顏色變得更加「順眼」,但原理和使用場景是不同的。
2.4.1 setDither(boolean dither)
設置圖像的抖動。
所謂抖動(注意,它就叫抖動,不是防抖動,也不是去抖動,有些人在翻譯的時候自作主張地加了一個「防」字或者「去」字,這是不對的),是指把圖像從較高色彩深度(即可用的顏色數)向較低色彩深度的區域繪制時,在圖像中有意地插入噪點,通過有規律地擾亂圖像來讓圖像對于肉眼更加真實的做法。
例子:
比如向 1 位色彩深度的區域中繪制灰色,由于 1 位深度只包含黑和白兩種顏色,在默認情況下,即不加抖動的時候,只能選擇向上或向下選擇最接近灰色的白色或黑色來繪制,那么顯示出來也只能是一片白或者一片黑。而加了抖動后,就可以繪制出讓肉眼識別為灰色的效果了:
瞇著眼看這幅圖
2.4.2 setFilterBitmap(boolean filter)
設置是否使用雙線性過濾來繪制 Bitmap 。
圖像在放大繪制的時候,默認使用的是最近鄰插值過濾,這種算法簡單,但會出現馬賽克現象;而如果開啟了雙線性過濾,就可以讓結果圖像顯得更加平滑。效果依然盜維基百科的圖:
2.5 setPathEffect(PathEffect effect)
使用 PathEffect 來給圖形的輪廓設置效果。對 Canvas 所有的圖形繪制有效,也就是 drawLine() drawCircle() drawPath() 這些方法。大概像這樣:
PathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 10);
paint.setPathEffect(pathEffect);
...
canvas.drawCircle(300, 300, 200, paint);
下面就具體說一下 Android 中的 6 種 PathEffect。PathEffect 分為兩類,單一效果的 CornerPathEffect DiscretePathEffect DashPathEffect PathDashPathEffect ,和組合效果的 SumPathEffect ComposePathEffect。
2.5.1 CornerPathEffect
把所有拐角變成圓角。
PathEffect pathEffect = new CornerPathEffect(20);
它的構造方法 CornerPathEffect(float radius) 的參數 radius 是圓角的半徑。
2.5.2 DiscretePathEffect
把線條進行隨機的偏離,讓輪廓變得亂七八糟。亂七八糟的方式和程度由參數決定。
PathEffect pathEffect = new DiscretePathEffect(20, 5);
DiscretePathEffect 具體的做法是,把繪制改為使用定長的線段來拼接,并且在拼接的時候對路徑進行隨機偏離。它的構造方法 DiscretePathEffect(float segmentLength, float deviation) 的兩個參數中, segmentLength 是用來拼接的每個線段的長度, deviation 是偏離量。這兩個值設置得不一樣,顯示效果也會不一樣,具體的你自己多試幾次就明白了,這里不再貼更多的圖。
2.5.3 DashPathEffect
使用虛線來繪制線條。
它的構造方法 DashPathEffect(float[] intervals, float phase) 中, 第一個參數 intervals 是一個數組,它指定了虛線的格式:數組中元素必須為偶數(最少是 2 個),按照「畫線長度、空白長度、畫線長度、空白長度」……的順序排列,例如上面代碼中的 20, 5, 10, 5 就表示虛線是按照「畫 20 像素、空 5 像素、畫 10 像素、空 5 像素」的模式來繪制;第二個參數 phase 是虛線的偏移量。
2.5.4 PathDashPathEffect
這個方法比 DashPathEffect 多一個前綴 Path ,所以顧名思義,它是使用一個 Path 來繪制「虛線」。具體看圖吧:
它的構造方法 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:變體
image.png
2.5.5 SumPathEffect
這是一個組合效果類的 PathEffect 。它的行為特別簡單,就是分別按照兩種 PathEffect 分別對目標進行繪制。
PathEffect dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
PathEffect discreteEffect = new DiscretePathEffect(20, 5);
pathEffect = new SumPathEffect(dashEffect, discreteEffect);
...
canvas.drawPath(path, paint);
2.5.6 ComposePathEffect
這也是一個組合效果類的 PathEffect 。不過它是先對目標 Path 使用一個 PathEffect,然后再對這個改變后的 Path 使用另一個 PathEffect。
PathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 10);
paint.setPathEffect(pathEffect);
...
canvas.drawCircle(300, 300, 200, paint);
它的構造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的兩個 PathEffect 參數, innerpe 是先應用的, outerpe 是后應用的。所以上面的代碼就是「先偏離,再變虛線」。而如果把兩個參數調換,就成了「先變虛線,再偏離」。至于具體的視覺效果……我就不貼圖了,你自己試試看吧!
總結
上面這些就是 Paint 中的 6 種 PathEffect。它們有的是有獨立效果的,有的是用來組合不同的 PathEffect 的,功能各不一樣。
注意: PathEffect 在有些情況下不支持硬件加速,需要關閉硬件加速才能正常使用:
Canvas.drawLine() 和 Canvas.drawLines() 方法畫直線時,setPathEffect() 是不支持硬件加速的;
PathDashPathEffect 對硬件加速的支持也有問題,所以當使用 PathDashPathEffect 的時候,最好也把硬件加速關了。
2.6 setShadowLayer(float radius, float dx, float dy, int shadowColor)
在之后的繪制內容下面加一層陰影。
paint.setShadowLayer(10, 0, 0, Color.RED);
...
canvas.drawText(text, 80, 300, paint);
效果就是上面這樣。方法的參數里, radius 是陰影的模糊范圍; dx dy 是陰影的偏移量; shadowColor 是陰影的顏色。
如果要清除陰影層,使用 clearShadowLayer() 。
注意:
- 在硬件加速開啟的情況下, setShadowLayer() 只支持文字的繪制,文字之外的繪制必須關閉硬件加速才能正常繪制陰影。
- 如果 shadowColor 是半透明的,陰影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,陰影的透明度就使用 paint 的透明度。
2.7 setMaskFilter(MaskFilter maskfilter)
上一個方法 setShadowLayer() 是設置的在繪制層下方的附加效果;而這個 MaskFilter 和它相反,設置的是在繪制層上方的附加效果。
到現在已經有兩個 setXxxFilter(filter) 了。前面有一個 setColorFilter(filter) ,是對每個像素的顏色進行過濾;而這里的 setMaskFilter(filter) 則是基于整個畫面來進行過濾。
MaskFilter 有兩種: BlurMaskFilter 和 EmbossMaskFilter。
2.7.1 BlurMaskFilter
模糊效果的 MaskFilter。
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
...
canvas.drawBitmap(bitmap, 100, 100, paint);
它的構造方法 BlurMaskFilter(float radius, BlurMaskFilter.Blur style) 中, radius 參數是模糊的范圍, style 是模糊的類型。一共有四種:
- NORMAL: 內外都模糊繪制
- SOLID: 內部正常繪制,外部模糊
- INNER: 內部模糊,外部不繪制
- OUTER: 內部不繪制,外部模糊(什么鬼?)
2.7.2 EmbossMaskFilter
浮雕效果的 MaskFilter。
paint.setMaskFilter(new EmbossMaskFilter(new float[]{0, 1, 1}, 0.2f, 8, 10));
...
canvas.drawBitmap(bitmap, 100, 100, paint);
它的構造方法 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) 的參數里, direction 是一個 3 個元素的數組,指定了光源的方向; ambient 是環境光的強度,數值范圍是 0 到 1; specular 是炫光的系數; blurRadius 是應用光線的范圍。
2.8 獲取繪制的 Path
根據 paint 的設置,計算出繪制 Path 或文字時的實際 Path。
2.8.1 getFillPath(Path src, Path dst)
所謂實際 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)
文字的繪制,雖然是使用 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.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
...
canvas.drawBitmap(bitmap, 100, 100, paint);
參考:
https://hencoder.com/ui-1-2/
注意:本文全部摘自上面的鏈接,本人只是對上面鏈接內容的自我抽取,符合本人閱讀
注意
最后再強調一遍:這期的內容沒必要全部背會,只要看懂、理解,記住有這么個東西就行了。以后在用到的時候,再拐回來翻一翻就行了。