這多是第二好的自定義 View 教程之繪製

面試系列 不繼續了嗎?

知道個人人都知道,以前我寫了這個 面試系列宣言,現在好像一直都沒有連載,而是隔三差五地來一篇,其實也是由於筆者也能力有限,構思一篇文章須要足夠的時間去印證其準確性,而以前的部分就由於印證不夠形成了勘誤。java

值得注意的是,本系列不會中止的。面試的不少知識點在於平時的積累,但自定義 View 這個東西,就得緊緊掌握了。自定義 View 將分爲幾期,本期咱們只講繪製。git

爲何咱們要學自定義 View?

大多數時候,咱們均可以採用官方自帶或者 GitHub 上的三方開源庫實現各類各樣炫酷的效果。但,需求倒是五花八門的,你永遠沒法改變設計師們的想象力和創造力。而咱們要作的,就是把他們的想象力和創造力變成現實。github

這期怎麼變成第二好了?

對,我沒有寫錯,本期自定義 View 教程不再是最好的了,由於這期基本是 HenCoder 的濃縮總結版。面試

HenCoder,給高級 Android 工程師的進階手冊 ,筆者也是一直在像追劇同樣的追。好像這裏確實有了給我凱哥打廣告的嫌疑,但把好東西,分享給你們,纔是最最重要的。canvas

筆者也是七進七出自定義 View,確實是看了很多教程和書籍,都沒有一個很好的自定義 View 能力。而做爲 Android 開發中必不可少的能(裝)力(逼)手段,也是一個很好的可讓咱們在面試以及開發中脫穎而出。微信

廢話不能太多,我要開始啦!ide

自定義 View 能夠簡單的分爲三步,繪製、佈局、觸摸反饋。本期,咱們首先講繪製。佈局

自定義 View 繪製的重中之重

自定義的繪製就是重寫繪製方法,其中最經常使用的就是 onDraw()。(固然有其它的,後面會說起,這裏先賣個關子。)而繪製的關鍵就是 Canvas 的使用:post

  • Canvas 的繪製類方法:drawXXX() (關鍵參數:Paint)學習

  • Canvas 的輔助類方法:範圍裁切和幾何變換。

一切的開始:onDraw()

自定義繪製的上手很是容易:提早建立好 Paint 對象,重寫 onDraw(),把繪製代碼寫在 onDraw() 裏面,就是自定義繪製最基本的實現。大概就像這樣:

Paint paint = new Paint();

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

    // 繪製一個圓
    canvas.drawCircle(300, 300, 200, paint);
}

就這麼簡單。因此關於 onDraw() 其實沒什麼好說的,一個很普通的方法重寫,惟一須要注意的是別漏寫了 super.onDraw()。你可能會點擊進去查看到 super.onDraw() 實際上是一個空實現,那可能只是由於你繼承的是 View 吧,你繼承 View 的其它子類試試?

Canvas.drawXXX() 系列方法的使用

Canvas 下面的 drawXXX() 系列的方法真沒啥好講的,你想畫什麼圖形直接畫就行了。而參數其實也給的很是的明瞭。你必定要所有了解學習的話,直接能夠去看官方文檔或者凱哥的 自定義View 1-1

  • 填充顏色:Canvas.drawColor(@ColorInt int color)

  • 畫圓:drawCircle(float centerX, float centerY, float radius, Paint paint)

  • 畫矩形:drawRect(float left, float top, float right, float bottom, Paint paint)

  • 畫點:drawPoint(float x, float y, Paint paint)

  • 批量畫點:drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint)

  • 畫橢圓:drawOval(float left, float top, float right, float bottom, Paint paint)

  • 畫線:drawLine(float startX, float startY, float stopX, float stopY, Paint paint)

  • 畫弧線或者扇形:drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

  • 畫自定義圖形:drawPath(Path path, Paint paint)

  • 畫 Bitmap:drawBitmap(Bitmap bitmap, float left, float top, Paint paint)

  • 畫文字:drawText(String text, float x, float y, Paint paint)

其中能夠看到有很多的座標值參數,你只須要明白的一點是,在 Android 的繪製中,座標系是這樣的。
image
值得注意的是:

  • 在畫弧線或者扇形中的角度 angle,x 軸正方向爲 0°,順時針方向爲正角度,逆時針爲負角度。

  • 畫弧線或者扇形中的 sweepAngle 參數,表明的是繪製的角度,不要被其它方法誤導成了覺得是繪製結束時候的角度,官方爲什麼在這裏作了個變換,其實我也不知道。

  • drawPath() 方法可能相對其它較難,但倒是自定義 View 實際應用中最多的。很是須要了解其三類方法。這裏直接摘抄凱哥的 自定義 View 1-1

  • drawBitmap() 方法中有個參數是 Bitmap,友情提示:Bitmap 能夠經過 BitmapFactory.decodeXXX() 得到。

Path 能夠描述直線、二次曲線、三次曲線、圓、橢圓、弧形、矩形、圓角矩形。把這些圖形結合起來,就能夠描述出不少複雜的圖形。Path 能夠歸結爲兩類方法:

  • 直接描述路徑,也能夠分爲兩組:

    • 添加子圖形:addXXX(), 此類方法在特定狀況下幾個 Canvas.drawPath() 等同於 Canvas.drawXXX()

    • 畫直線或曲線:xxxTo(): 這一組和第一組 addXxx() 方法的區別在於,第一組是添加的完整封閉圖形(除了 addPath() ),而這一組添加的只是一條線。

  • 輔助設置或計算,由於應用場景不多,凱哥也只講了其中一個方法: Path.setFillType(Path.FillType ft) 設置填充方式

上面有比較多的提到 Paint 這個參數,實際上它是真的很好用,直接在下面講解。

Paint 的使用

Paint 真的很重要,在自定義繪製中充當關鍵角色:畫筆,因此咱們天然能夠爲「畫筆」作不少操做,好比設置顏色、繪製模式、粗細等。

  • Paint.setStyle(Style style) 設置繪製模式

  • Paint.setColor(int color) 設置顏色

  • Paint.setStrokeWidth(float width) 設置線條寬度

  • Paint.setTextSize(float textSize) 設置文字大小

  • Paint.setAntiAlias(boolean aa) 設置抗鋸齒開關

嗯,對,抗鋸齒開關還能夠直接在 Paint 初始化的時候直接做爲構造參數:Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG)

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

  • 顏色

  • 效果

  • drawText() 相關

  • 初始化

凱哥專門拿了一期對 Paint 作了重點講解,依然在實際場景應該用處不大,因此須要的直接點擊 這裏 跳轉。

若是你想先知道凱哥都講了什麼,我這裏也單獨給你總結一下:

首先是給 Paint 設置着色器。

  • Paint.setShader(Shader shader):設置着色器,實際上咱們通常傳遞的參數不會直接傳遞 Shader,而會選擇直接傳遞它的子類,具體效果下面給出。

    • 線性漸變:LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,TileMode tile)

image

  • 輻射漸變:RadialGradient(float centerX, float centerY, float radius,int centerColor, int edgeColor, @NonNull TileMode tileMode)

image

  • 掃描漸變:SweepGradient(float cx, float cy, int color0, int color1)

image
還有不少,就不一一給圖了。

  • BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)

  • 混合着色:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

其中須要注意的是:

  • Paint.setShader() 優先級高於 Paint.setColor() 系列方法。

  • 最後一個 tile 參數,表明的是斷點範圍以外的着色規則。它是一個枚舉類型,有三種參數。

    • CLAMP : 直譯是「夾子模式」,會在端點以外延續端點處的顏色。

    • MIRROR : 鏡像模式。

    • REPEAT : 重複模式。

其次是設置顏色過濾

設置顏色過濾能夠採用 Paint.setColorFilter(ColorFilter colorFilter) 方法。它的名字已經足夠解釋它的做用:爲繪製設置顏色過濾。顏色過濾的意思,就是爲繪製的內容設置一個統一的過濾策略,而後 Canvas.drawXXX() 方法會對每一個像素都進行過濾後再繪製出來。

這個其實貌似在拍照或者照片整理類應用上用的比較多,其它方面貌似我還不多遇到過,GitHub 上的庫 StyleImageView 詮釋的很棒。

再其它也就沒啥好說的,感興趣直接去看 HenCoder

這裏能夠重點說一下:Paint.setStrokeCap(Paint.Cap cap),設置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認爲 BUTT
image

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

Canvas 的文字繪製

Canvas 的文字繪製方法有三個:

  • drawText()

  • drawTextRun()

  • drawTextOnPath()

咱們大多數狀況用不了那麼多,因此一樣這裏不作詳解,對於始終想追根到底的同窗,一樣給你提供了 凱哥的連接

下面只對部分須要注意的重點總結一下。

drawText()

  • drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

其中的參數很簡單:text 是文字內容,x 和 y 是文字的座標。但須要注意:這個座標並非文字的左上角,而是一個與左下角比較接近的位置。大概在這裏:
image

而若是你像繪製其餘內容同樣,在繪製文字的時候把座標填成 (0, 0),文字並不會顯示在 View 的左上角,而是會幾乎徹底顯示在 View 的上方,到了 View 外部看不到的位置:

canvas.drawText(text, 0, 0, paint);

大概是這樣:
image

另外,Canvas.drawText() 只能繪製單行的文字,而不能換行。就算顯示不完,也會直接繪製到屏幕外面去。

那若是要換行,得 drawText() 不少次嗎?並無,還有一個 StaticLayout 能夠完美達到咱們的效果。對於詳細使用,這裏也很少提了。

drawTextRun()drawTextOnPath(),運用的可能並很少,這裏就不說了。

簡單提一下設置效果輔助類吧,這個可能直接就有用。

Paint 對文字繪製的輔助

  • 設置文字大小:Paint.setTextSize(float textSize)

  • 設置字體:Paint.setTypeface(Typeface typeface),其中的 Typeface 裏面涵蓋了相關字體。另外,還能夠經過 Typeface.createFromAsset(AssetManager mgr, String path) 來設置自定義字體,其中 mgr 能夠給 getResources().getAssets()path 給文件名字,須要把字體文件 .ttf 放在工程的 res/assets 下,「assets」是新建的專用目錄。

  • 設置文字是否加粗: Paint.setFakeBoldText(boolean fakeBoldText)

  • 設置文字是否加刪除線:Paint.setStrikeThruText(boolean strikeThruText)

  • 設置文字是否加下劃線:Paint.setUnderlineText(boolean underlineText)

  • 設置字體傾斜度:Paint.setTextSkewX(float skewX) 「skewX」 向左傾斜爲正。

  • 設置文字橫向放縮:Paint.setTextScaleX(float scaleX)

  • 設置字體間距,默認值爲 0:Paint.setLetterSpacing(float letterSpacing) 這個不是行間距哦。

  • 設置文字對齊方式:Paint.setTextAlign(Paint.Align align),其中「align」有三個值:LEFTCENTERRIGHT,默認值是 LEFT

  • 設置繪製所使用的 Locale:Paint.setTextLocale(Locale locale) / Paint.setTextLocales(LocaleList locales)

實際上,這些方法基本都在咱們 TextView 裏面的。

自定義 View 之範圍裁切

範圍裁切主要採用兩個方法:

  • clipRect()

  • clipPath()

clipRect() 很簡單,只須要傳遞和 RectF 同樣的參數便可。你能夠除了裁剪矩形,還想作其它樣式的裁剪,惋惜這裏只有經過 path 的方法了(我也很奇怪爲啥沒有看到其它方法),再一次印證了 path 的重要性有木有。

值得注意的是:咱們一般會在範圍裁切先後加上 Canvas.save()Canvas.restore() 來及時恢復繪製範圍。大概代碼是這樣。

canvas.save();  
canvas.clipRect(left, top, right, bottom);  
canvas.drawBitmap(bitmap, x, y, paint);  
canvas.restore();

另外一個值得注意的點是:必定是先作範圍裁切操做,再作 Canvas.drawXXX() 操做,順序放反的話你會發現毛效果都沒有。除了裁切,幾何變換也是如此。

幾何變換

幾何變換的使用大概分爲三類:

  • 使用 Canvas 來作常見的二維變換;

  • 使用 Matrix 來作常見和不常見的二維變換;

  • 使用 Camera 來作三維變換

直接採用 Canvas 自帶方法進行二維變換

  • Canvas.translate(float dx, float dy)

平移,其中,dx 和 dy 分別表示橫向和縱向的位移。

  • Canvas.rotate(float degrees, float px, float py)

旋轉,其中 degrees 是旋轉角度,順時針爲正向,pxpy 表明軸心座標。

  • Canvas.scale(float sx, float sy, float px, float py)

放縮,其中 sx,sy 分別是橫向和縱向的放縮倍數,px 、py 爲放縮的軸心,這裏千萬不要受到重載方法 Canvas.scale(float sx,float sy) 的影響。

  • skew(float sx, float sy)

錯切。這裏的 sx 和 sy 分別是 x 方向和 y 方向的錯切係數。值得注意的是,這裏 sx 和 sy 值爲 0 的時候表明本身的方向不錯切。

再次重申,須要先作了二位變換,再執行 「drawXXX」操做,重要的事情必定會說三遍。

二位變換的另外一種方式 —— Matrix

Matrix 作常見變換的基本套路

  • 建立 Matrix 對象;

  • 調用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法來設置幾何變換;

  • 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 來把幾何變換應用到 Canvas。

Matrix matrix = new Matrix();

...

matrix.reset();  
matrix.postTranslate();  
matrix.postRotate();

canvas.save();  
canvas.concat(matrix);  
canvas.drawBitmap(bitmap, x, y, paint);  
canvas.restore();

把 Matrix 應用到 Canvas 有兩個方法: Canvas.setMatrix(matrix)Canvas.concat(matrix)

  • Canvas.setMatrix(matrix):用 Matrix 直接替換 Canvas 當前的變換矩陣,即拋棄 Canvas 當前的變換,改用 Matrix 的變換(注:根據凱哥收到的反饋,不一樣的系統中 setMatrix(matrix) 的行爲可能不一致,因此仍是儘可能用 concat(matrix) 吧);

  • Canvas.concat(matrix):用 Canvas 當前的變換矩陣和 Matrix 相乘,即基於 Canvas 當前的變換,疊加上 Matrix 中的變換。

其中須要注意的是:當多個 Matrix 須要用到的時候,你並不須要初始化多個 Matrix,而能夠直接經過調用 Matrix.reset()Matrix 進行重置。

對於採用 Matrix 來實現不規則變換以及採用 Camera 實現三維變換這裏也就很少說了,實際遇到的時候,你也能夠 點擊這裏 複習一下呀。

精彩的繪製順序

前面講了一大堆繪製方法,以及範圍裁切和變換,咱們這裏再說說繪製順序。

Android 裏面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。好比你在重疊的位置先畫圓再畫方,和先畫方再畫圓所呈現出來的結果確定是不一樣的:
image

到底放在 super.onDraw() 上面仍是下面?

一般若是咱們繼承的是 View 的話,super.onDraw() 只是一個空實現,因此它的位置放在哪兒都沒事,甚至直接不要也沒事,但反正加上也沒啥影響,儘可能仍是加上吧。

因爲 Android 的繪製順序性,當你繼承自已經有繪製的其餘 View(好比 TextView)的時候,放在 super.onDraw() 上面就意味着繪製代碼會被控件的原內容蓋住。

dispatchDraw():繪製子 View 的方法

還記得我上面賣的關子嗎?自定義繪製其實不止 onDraw() 一個方法。onDraw() 只是負責自身主體內容繪製的。而有的時候,你想要的遮蓋關係沒法經過 onDraw() 來實現,而是須要經過別的繪製方法。

凱哥這塊真的寫的是太有意思了,因此我也是直接 copy 了過來。

例如,你繼承了一個 LinearLayout,重寫了它的 onDraw() 方法,在 super.onDraw() 中插入了你本身的繪製代碼,使它可以在內部繪製一些斑點做爲點綴:

public class SpottedLinearLayout extends LinearLayout {  
    ...

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

       ... // 繪製斑點
    }
}

image

看起來確實沒有問題,可是你會發現,當你添加了子 View 以後,你的斑點不見了:
image

形成這種狀況的緣由是 Android 的繪製順序:在繪製過程當中,每個 ViewGroup 會先調用本身的 onDraw() 來繪製完本身的主體以後再去繪製它的子 View。對於上面這個例子來講,就是你的 LinearLayout 會在繪製完斑點後再去繪製它的子 View。那麼在子 View 繪製完成以後,先前繪製的斑點就被子 View 蓋住了。

具體來說,這裏說的「繪製子 View」是經過另外一個繪製方法的調用來發生的,這個繪製方法叫作:dispatchDraw()。也就是說,在繪製過程當中,每一個 View 和 ViewGroup 都會先調用 onDraw() 方法來繪製主體,再調用 dispatchDraw() 方法來繪製子 View。

注:雖然 View 和 ViewGroup 都有 dispatchDraw() 方法,不過因爲 View 是沒有子 View 的,因此通常來講 dispatchDraw() 這個方法只對 ViewGroup(以及它的子類)有意義。

image

回到剛纔的問題:怎樣才能讓 LinearLayout 的繪製內容蓋住子 View 呢?只要讓它的繪製代碼在子 View 的繪製以後再執行就行了。因此直接執行在 super.dispatchDraw() 的下面便可。

簡單總結一下繪製順序

凱哥確實強勢,在文章的最後,直接貼圖,不能再清晰了,因此我也是直接跳過了其中 N 個環節,直接上圖。
image
image
注意:

  • 在 ViewGroup 的子類中重寫除 dispatchDraw() 之外的繪製方法時,可能須要調用 setWillNotDraw(false)

  • 在重寫的方法有多個選擇時,優先選擇 onDraw()

寫在最後

本期的自定義 View 之繪製就到這裏結束了,強烈推薦 點擊連接 跟着凱哥操,不得挨飛刀。


作不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公衆號搜索「nanchen」關注個人微信公衆號,目前多運營 Android ,盡本身所能爲你提高。若是你喜歡,爲我點贊分享吧~
nanchen

相關文章
相關標籤/搜索