HenCoder Android 開發進階: 自定義 View 1-2 Paint 詳解

這期是 HenCoder 自定義繪製的第二期: Paint。若是你沒看過第一期,能夠先去看一下第一期:html

HenCoder Android 開發進階:自定義 View 1-1 繪製基礎java

簡介

上一期我已經簡單說過, CanvasdrawXXX() 方法配合 Paint 的幾個經常使用方法能夠實現最多見的繪製需求;而若是你只會基本的繪製, Paint 的徹底功能的掌握,能讓你更進一步,作出一些更加細緻、炫酷的效果。把 Paint 掌握以後,你幾乎再也不會遇到「iOS 組能夠實現,但你卻實現不了」的繪製效果。android

因爲依然是講繪製的,因此這期就沒有介紹視頻了。繪製的內容一共須要講大概 5~6 期才能講完,也就是說你要看 5~6 期才能成爲自定義繪製的高手。相對於上期的內容,這期的內容更爲專項、深度更深。對於沒有深刻研究過 Paint 的人,這期是一個對 Paint 的詮釋;而對於嘗試過研究 Paint 但仍然對其中一些 API 有疑惑的人,這期也能夠幫你解惑。git

另外,也正因爲這期的內容是更爲專項的,因此建議你在看的時候,沒必要像上期那樣把全部東西都徹底記住,而是隻要把內容理解了就好。這期的內容,只要作到「知道有這麼個東西」,在須要用到的時候能想起來這個功能能不能作、大體用什麼作就好,至於具體的實現,到時候拐回來再翻一次就好了。程序員

好,下面進入正題。github

Paint 的 API 大體能夠分爲 4 類:算法

  • 顏色
  • 效果
  • drawText() 相關
  • 初始化

下面我就對這 4 類分別進行介紹:canvas

1 顏色

Canvas 繪製的內容,有三層對顏色的處理:數組

這圖大概看看就行,不用鑽研明白再往下看,由於等這章講完你就懂了。緩存

1.1 基本顏色

像素的基本顏色,根據繪製內容的不一樣而有不一樣的控制方式: Canvas 的顏色填充類方法 drawColor/RGB/ARGB() 的顏色,是直接寫在方法的參數裏,經過參數來設置的(上期講過了); drawBitmap() 的顏色,是直接由 Bitmap 對象來提供的(上期也講過了);除此以外,是圖形和文字的繪製,它們的顏色就須要使用 paint 參數來額外設置了(下面要講的)。

Canvas 的方法 像素顏色的設置方式
drawColor/RGB/ARGB() 直接做爲參數傳入
drawBitmap() bitmap 參數的像素顏色相同
圖形和文字 (drawCircle() / drawPath() / drawText() ...) paint 參數中設置

Paint 設置顏色的方法有兩種:一種是直接用 Paint.setColor/ARGB() 來設置顏色,另外一種是使用 Shader 來指定着色方案。

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);複製代碼

setColor() 對應的 get 方法是 getColor()

1.1.1.2 setARGB(int a, int r, int g, int b)

其實和 setColor(color) 都是同樣同樣兒的,只是它的參數用的是更直接的三原色與透明度的值。實際運用中,setColor()setARGB() 哪一個方便和順手用哪一個吧。

paint.setARGB(100, 255, 0, 0);
canvas.drawRect(0, 0, 200, 200, paint);
paint.setARGB(100, 0, 0, 0);
canvas.drawLine(0, 0, 200, 200, paint);複製代碼

1.1.2 setShader(Shader shader) 設置 Shader

除了直接設置顏色, Paint 還可使用 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);複製代碼

設置了 Shader 以後,繪製出了漸變顏色的圓。(其餘形狀以及文字均可以這樣設置顏色,我只是沒給出圖。)

注意:在設置了 Shader 的狀況下, Paint.setColor/ARGB() 所設置的顏色就再也不起做用。

構造方法:
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)

參數:
x0 y0 x1 y1:漸變的兩個端點的位置
color0 color1 是端點的顏色
tile:端點範圍以外的着色規則,類型是 TileModeTileMode 一共有 3 個值可選: CLAMP, MIRRORREPEATCLAMP (夾子模式???算了這個詞我不會翻)會在端點以外延續端點處的顏色;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:輻射範圍以外的着色模式。

CLAMP:

MIRROR:

REPEAT:

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

CLAMP:

MIRROR:

REPEAT:

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);複製代碼

注意:上面這段代碼中我使用了兩個 BitmapShader 來做爲 ComposeShader() 的參數,而 ComposeShader() 在硬件加速下是不支持兩個相同類型的 Shader 的,因此這裏也須要關閉硬件加速才能看到效果。

構造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

參數:
shaderA, shaderB:兩個相繼使用的 Shader
mode: 兩個 Shader 的疊加模式,即 shaderAshaderB 應該怎樣共同繪製。它的類型是 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,就會變成挖空效果:

而若是再把 mode 改成 PorterDuff.Mode.DST_IN,就會變成蒙版摳圖效果:

這下明白了吧?

具體來講, PorterDuff.Mode 一共有 17 個,能夠分爲兩類:

  1. Alpha 合成 (Alpha Compositing)
  2. 混合 (Blending)

第一類,Alpha 合成,其實就是 「PorterDuff」 這個詞所指代的算法。 「PorterDuff」 並非一個具備實際意義的詞組,而是兩我的的名字(準確講是姓)。這兩我的當年共同發表了一篇論文,描述了 12 種將兩個圖像共同繪製的操做(即算法)。而這篇論文所論述的操做,都是關於 Alpha 通道(也就是咱們通俗理解的「透明度」)的計算的,後來人們就把這類計算稱爲Alpha 合成 ( Alpha Compositing ) 。

看下效果吧。效果直接盜 Google 的官方文檔了。

源圖像和目標圖像:

Alpha 合成:

第二類,混合,也就是 Photoshop 等製圖軟件裏都有的那些混合模式(multiply darken lighten 之類的)。這一類操做的是顏色自己而不是 Alpha 通道,並不屬於 Alpha 合成,因此和 Porter 與 Duff 這兩我的也沒什麼關係,不過爲了使用的方便,它們一樣也被 Google 加進了 PorterDuff.Mode 裏。

效果依然盜 官方文檔

結論

從效果圖能夠看出,Alpha 合成類的效果都比較直觀,基本上可使用簡單的口頭表達來描述它們的算法(起碼對於不透明的源圖像和目標圖像來講是能夠的),例如 SRC_OVER 表示「兩者都繪製,但要源圖像放在目標圖像的上面」,DST_IN 表示「只繪製目標圖像,而且只繪製它和源圖像重合的區域」。

而混合類的效果就相對抽象一些,只從效果圖不太能看得出它們的着色算法,更看不出來它們有什麼用。不過不要緊,你若是拿着這些名詞去問你司的設計師,他們八成都能給你說出來個 123。

因此對於這些 Mode,正確的作法是:對於 Alpha 合成類的操做,掌握他們,並在實際開發中靈活運用;而對於混合類的,你只要把它們的名字記住就行了,這樣當某一天設計師告訴你「我要作這種混合效果」的時候,你能夠立刻知道本身能不能作,怎麼作。

另外:PorterDuff.Mode 建議你動手用一下試試,對加深理解有幫助。

好了,這些就是幾個 Shader 的具體介紹。

除了使用 setColor/ARGB()setShader() 來設置基本顏色, Paint 還能夠來設置 ColorFilter,來對顏色進行第二層處理。

1.2 setColorFilter(ColorFilter colorFilter)

ColorFilter 這個類,它的名字已經足夠解釋它的做用:爲繪製設置顏色過濾。顏色過濾的意思,就是爲繪製的內容設置一個統一的過濾策略,而後 Canvas.drawXXX() 方法會對每一個像素都進行過濾後再繪製出來。舉幾個現實中比較常見的顏色過濾的例子:

  • 有色光照射:

    w400
    w400

  • 有色玻璃透視:

    w400
    w400

  • 膠捲:

Paint 裏設置 ColorFilter ,使用的是 Paint.setColorFilter(ColorFilter filter) 方法。 ColorFilter 並不直接使用,而是使用它的子類。它共有三個子類:LightingColorFilter PorterDuffColorFilterColorMatrixColorFilter

1.2.1 LightingColorFilter

這個 LightingColorFilter 是用來模擬簡單的光照效果的。

LightingColorFilter 的構造方法是 LightingColorFilter(int mul, int add) ,參數裏的 muladd 都是和顏色值格式相同的 int 值,其中 mul 用來和目標像素相乘,add 用來和目標像素相加:

R' = R * mul.R / 0xff + add.R G' = G * mul.G / 0xff + add.G
B' = B * mul.B / 0xff + add.B複製代碼

一個「保持原樣」的「基本 LightingColorFilter 」,mul0xffffffadd0x000000(也就是0),那麼對於一個像素,它的計算過程就是:

R' = R * 0xff / 0xff + 0x0 = R // R' = R
G' = G * 0xff / 0xff + 0x0 = G // G' = G
B' = B * 0xff / 0xff + 0x0 = B // B' = B複製代碼

基於這個「基本 LightingColorFilter 」,你就能夠修改一下作出其餘的 filter。好比,若是你想去掉原像素中的紅色,能夠把它的 mul 改成 0x00ffff (紅色部分爲 0 ) ,那麼它的計算過程就是:

R' = R * 0x0 / 0xff + 0x0 = 0 // 紅色被移除 G' = G * 0xff / 0xff + 0x0 = G
B' = B * 0xff / 0xff + 0x0 = B複製代碼

具體效果是這樣的:

ColorFilter lightingColorFilter = new LightingColorFilter(0x00ffff, 0x000000);
paint.setColorFilter(lightingColorFilter);複製代碼

表情突然變得陰鬱了

或者,若是你想讓它的綠色更亮一些,就能夠把它的 add 改成 0x003000 (綠色部分爲 0x30 ),那麼它的計算過程就是:

R' = R * 0xff / 0xff + 0x0 = R G' = G * 0xff / 0xff + 0x30 = G + 0x30 // 綠色被增強
B' = B * 0xff / 0xff + 0x0 = B複製代碼

效果是這樣:

ColorFilter lightingColorFilter = new LightingColorFilter(0xffffff, 0x003000);
paint.setColorFilter(lightingColorFilter);複製代碼

這樣的表情才陽光

至於怎麼修改參數來模擬你想要的某種具體光照效果,你就別問我了,仍是跟你司設計師討論吧,這個我不專業……

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複製代碼

xfermode1
xfermode1

又是 PorterDuff.ModePorterDuff.ModePaint 一共有三處 API ,它們的工做原理都同樣,只是用途不一樣:

API 用途
ComposeShader 混合兩個 Shader
PorterDuffColorFilter 增長一個單色的 ColorFilter
Xfermode 設置繪製內容和 View 中已有內容的混合計算方式

另外,從上面的示例代碼能夠看出,建立 Xfermode 的時候實際上是建立的它的子類 PorterDuffXfermode。而事實上,Xfermode 也只有這一個子類。因此在設置 Xfermode 的時候不用多想,直接用 PorterDuffXfermode 吧。

「只有一個子類???什麼設計?」

其實在更早的 Android 版本中,Xfermode 還有別的子類,但別的子類如今已經 deprecated 了,現在只剩下了 PorterDuffXfermode。因此目前它的使用看起來好像有點囉嗦,但實際上是因爲歷史遺留問題。

Xfermode 注意事項

Xfermode 使用很簡單,不過有兩點須要注意:

1. 使用離屏緩衝(Off-screen Buffer)

實質上,上面這段例子代碼,若是直接執行的話是不會繪製出圖中效果的,程序的繪製也不會像上面的動畫那樣執行,而是會像這樣:

爲何會這樣?

按照邏輯咱們會認爲,在第二步畫圓的時候,跟它共同計算的是第一步繪製的方形。但實際上,倒是整個 View 的顯示區域都在畫圓的時候參與計算,而且 View 自身的底色並非默認的透明色,並且是遵循一種迷之邏輯,致使不只繪製的是整個圓的範圍,並且在範圍以外都變成了黑色。就像這樣:

xfermode2
xfermode2

這……那可如何是好?

要想使用 setXfermode() 正常繪製,必須使用離屏緩存 (Off-screen Buffer) 把內容繪製在額外的層上,再把繪製好的內容貼回 View 中。也就是這樣:

xfermode3
xfermode3

經過使用離屏緩衝,把要繪製的內容單獨繪製在緩衝層, 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 來繪製的內容,除了注意使用離屏緩衝,還應該注意控制它的透明區域不要過小,要讓它足夠覆蓋到要和它結合繪製的內容,不然獲得的結果極可能不是你想要的。我用圖片來具體說明一下:

如圖所示,因爲透明區域太小而覆蓋不到的地方,將不會受到 Xfermode 的影響。

好,到此爲止,前面講的就是 Paint 的第一類 API——關於顏色的三層設置:直接設置顏色的 API 用來給圖形和文字設置顏色; setColorFilter() 用來基於顏色進行過濾處理; setXfermode() 用來處理源圖像和 View 已有內容的關係。

再貼一次本章開始處的圖做爲回顧:

2 效果

效果類的 API ,指的就是抗鋸齒、填充/輪廓、線條寬度等等這些。

2.1 setAntiAlias (boolean aa) 設置抗鋸齒

抗鋸齒在上一節已經講過了,話很少說,直接上圖:

抗鋸齒默認是關閉的,若是須要抗鋸齒,須要顯式地打開。另外,除了 setAntiAlias(aa) 方法,打開抗鋸齒還有一個更方便的方式:構造方法。建立 Paint 對象的時候,構造方法的參數里加一個 ANTI_ALIAS_FLAG 的 flag,就能夠在初始化的時候就開啓抗鋸齒。

Paint paint = 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);複製代碼

paint.setStyle(Paint.Style.FILL_AND_STROKE); // FILL_AND_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);複製代碼

線條寬度 0 和 1 的區別

默認狀況下,線條寬度爲 0,但你會發現,這個時候它依然可以畫出線,線條的寬度爲 1 像素。那麼它和線條寬度爲 1 有什麼區別呢?

其實這個和後面要講的一個「幾何變換」有關:你能夠爲 Canvas 設置 Matrix 來實現幾何變換(如放大、縮小、平移、旋轉),在幾何變換以後 Canvas 繪製的內容就會發生相應變化,包括線條也會加粗,例如 2 像素寬度的線條在 Canvas 放大 2 倍後會被以 4 像素寬度來繪製。而當線條寬度被設置爲 0 時,它的寬度就被固定爲 1 像素,就算 Canvas 經過幾何變換被放大,它也依然會被以 1 像素寬度來繪製。Google 在文檔中把線條寬度爲 0 時稱做「hairline mode(髮際線模式)」。

2.3.2 setStrokeCap(Paint.Cap cap)

設置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認爲 BUTT

放出「平頭」「圓頭」「方頭」這種翻譯我始終有點糾結:既以爲本身翻譯得簡潔清晰盡顯機智,同時又擔憂用詞會不會有點太過通俗,讓人以爲我不夠高貴冷豔?

當線條的寬度是 1 像素時,這三種線頭的表現是徹底一致的,全是 1 個像素的點;而當線條變粗的時候,它們就會表現出不一樣的樣子:

虛線是額外加的,虛線左邊是線的實際長度,虛線右邊是線頭。有了虛線做爲輔助,能夠清楚地看出 BUTTSQUARE 的區別。

2.3.3 setStrokeJoin(Paint.Join join)

設置拐角的形狀。有三個值能夠選擇:MITER 尖角、 BEVEL 平角和 ROUND 圓角。默認爲 MITER

輔助理解:

MITER 在現實中其實就是這玩意:

而 BEVEL 是這玩意:

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 的工程師沒有這麼給它命名,大概也是不想傷害你們的手指吧,畢竟程序員何苦爲難程序員。

以上就是 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() 這些方法。大概像這樣:

PathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 10);
paint.setPathEffect(pathEffect);

...

canvas.drawCircle(300, 300, 200, paint);複製代碼

下面就具體說一下 Android 中的 6 種 PathEffectPathEffect 分爲兩類,單一效果的 CornerPathEffect DiscretePathEffect DashPathEffect PathDashPathEffect ,和組合效果的 SumPathEffect ComposePathEffect

2.5.1 CornerPathEffect

把全部拐角變成圓角。

PathEffect pathEffect = new CornerPathEffect(20);
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 來繪製「虛線」。具體看圖吧:

Path dashPath = ...; // 使用一個三角形來作 dash
PathEffect pathEffect = new PathDashPathEffect(dashPath, 40, 0,
        PathDashPathEffectStyle.TRANSLATE);
paint.setPathEffect(pathEffect);

...

canvas.drawPath(path, paint);複製代碼

它的構造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 參數是用來繪製的 Pathadvance 是兩個相鄰的 shape 段之間的間隔,不過注意,這個間隔是兩個 shape 段的起點的間隔,而不是前一個的終點和後一個的起點的距離; phaseDashPathEffect 中同樣,是虛線的偏移;最後一個參數 style,是用來指定拐彎改變的時候 shape 的轉換方式。style 的類型爲 PathDashPathEffect.Style ,是一個 enum ,具體有三個值:

  • TRANSLATE:位移
  • ROTATE:旋轉
  • MORPH:變體

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 dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
PathEffect discreteEffect = new DiscretePathEffect(20, 5); 
pathEffect = new ComposePathEffect(dashEffect, discreteEffect);

...

canvas.drawPath(path, paint);複製代碼

它的構造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的兩個 PathEffect 參數, innerpe 是先應用的, outerpe 是後應用的。因此上面的代碼就是「先偏離,再變虛線」。而若是把兩個參數調換,就成了「先變虛線,再偏離」。至於具體的視覺效果……我就不貼圖了,你本身試試看吧!

上面這些就是 Paint 中的 6 種 PathEffect。它們有的是有獨立效果的,有的是用來組合不一樣的 PathEffect 的,功能各不同。

注意: PathEffect 在有些狀況下不支持硬件加速,須要關閉硬件加速才能正常使用:

  1. Canvas.drawLine()Canvas.drawLines() 方法畫直線時,setPathEffect() 是不支持硬件加速的;
  2. PathDashPathEffect 對硬件加速的支持也有問題,因此當使用 PathDashPathEffect 的時候,最好也把硬件加速關了。

剩下的兩個效果類方法:setShadowLayer()setMaskFilter() ,它們和前面的效果類方法有點不同:它們設置的是「附加效果」,也就是基於在繪製內容的額外效果。

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)

爲以後的繪製設置 MaskFilter。上一個方法 setShadowLayer() 是設置的在繪製層下方的附加效果;而這個 MaskFilter 和它相反,設置的是在繪製層上方的附加效果。

到如今已經有兩個 setXxxFilter(filter) 了。前面有一個 setColorFilter(filter) ,是對每一個像素的顏色進行過濾;而這裏的 setMaskFilter(filter) 則是基於整個畫面來進行過濾。

MaskFilter 有兩種: BlurMaskFilterEmbossMaskFilter

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 是應用光線的範圍。

不過因爲我沒有在項目中使用過 EmbossMaskFilter,對它的每一個參數具體調節方式並不熟,你有興趣的話本身研究一下吧。

2.8 獲取繪製的 Path

這是效果類的最後一組方法,也是效果類惟一的一組 get 方法。

這組方法作的事是,根據 paint 的設置,計算出繪製 Path 或文字時的實際 Path

這裏你可能會冒出兩個問題:

  1. 什麼叫「實際 Path」? Path 就是 Path,這加上個「實際」是什麼意思?
  2. 文字的 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 的四類方法:顏色類效果類文字繪製相關以及初始化類。其中顏色類、效果類和初始化類都已經在這節裏面講過了,剩下的一類——文字繪製類,下一節單獨講。

最後再強調一遍:這期的內容不必所有背會,只要看懂、理解,記住有這麼個東西就好了。之後在用到的時候,再拐回來翻一翻就好了。

練習項目

爲了不轉頭就忘,強烈建議你趁熱打鐵,作一下這個練習項目:HenCoderPracticeDraw2

下期預告

下期是文字繪製專場,我將會花一整期的篇幅來詳述文字的繪製。慣例放出部分配圖做爲預覽:

感謝

感謝參與這期預發佈內測的讀者:

小邁MadisonRong、小於、戀上你的眸、rubicAndroidmiaoyongjunArchyWang、孫志帥、czwathouTim Aimeecode小生

另外,公開招募內測讀者,願意幫助內測的掃下面的碼加羣吧!(已經加過一羣的就別加這個了,給別人留個名額,兩個羣待遇同樣的。)

讚揚

老規矩,但你的錢換不來任何增值服務,因此真的以爲贊再給錢喲。

相關文章
相關標籤/搜索