知道個人人都知道,以前我寫了這個 面試系列宣言,現在好像一直都沒有連載,而是隔三差五地來一篇,其實也是由於筆者也能力有限,構思一篇文章須要足夠的時間去印證其準確性,而以前的部分就由於印證不夠形成了勘誤。java
值得注意的是,本系列不會中止的。面試的不少知識點在於平時的積累,但自定義 View 這個東西,就得緊緊掌握了。自定義 View 將分爲幾期,本期咱們只講繪製。git
大多數時候,咱們均可以採用官方自帶或者 GitHub 上的三方開源庫實現各類各樣炫酷的效果。但,需求倒是五花八門的,你永遠沒法改變設計師們的想象力和創造力。而咱們要作的,就是把他們的想象力和創造力變成現實。github
對,我沒有寫錯,本期自定義 View 教程不再是最好的了,由於這期基本是 HenCoder 的濃縮總結版。面試
HenCoder,給高級 Android 工程師的進階手冊 ,筆者也是一直在像追劇同樣的追。好像這裏確實有了給我凱哥打廣告的嫌疑,但把好東西,分享給你們,纔是最最重要的。canvas
筆者也是七進七出自定義 View,確實是看了很多教程和書籍,都沒有一個很好的自定義 View 能力。而做爲 Android 開發中必不可少的能(裝)力(逼)手段,也是一個很好的可讓咱們在面試以及開發中脫穎而出。微信
廢話不能太多,我要開始啦!ide
自定義 View 能夠簡單的分爲三步,繪製、佈局、觸摸反饋。本期,咱們首先講繪製。佈局
自定義的繪製就是重寫繪製方法,其中最經常使用的就是 onDraw()
。(固然有其它的,後面會說起,這裏先賣個關子。)而繪製的關鍵就是 Canvas
的使用:post
Canvas 的繪製類方法:drawXXX() (關鍵參數:Paint)學習
Canvas 的輔助類方法:範圍裁切和幾何變換。
自定義繪製的上手很是容易:提早建立好 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() 系列的方法真沒啥好講的,你想畫什麼圖形直接畫就行了。而參數其實也給的很是的明瞭。你必定要所有了解學習的話,直接能夠去看官方文檔或者凱哥的 自定義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 的繪製中,座標系是這樣的。
值得注意的是:
在畫弧線或者扇形中的角度 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.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.setShader(Shader shader):設置着色器,實際上咱們通常傳遞的參數不會直接傳遞
Shader
,而會選擇直接傳遞它的子類,具體效果下面給出。
線性漸變:LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,TileMode tile)
輻射漸變:RadialGradient(float centerX, float centerY, float radius,int centerColor, int edgeColor, @NonNull TileMode tileMode)
掃描漸變:SweepGradient(float cx, float cy, int color0, int color1)
還有不少,就不一一給圖了。
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 詮釋的很棒。
這裏能夠重點說一下:Paint.setStrokeCap(Paint.Cap cap)
,設置線頭的形狀。線頭形狀有三種:BUTT
平頭、ROUND
圓頭、SQUARE
方頭。默認爲 BUTT
。
虛線是額外加的,虛線左邊是線的實際長度,虛線右邊是線頭。有了虛線做爲輔助,能夠清楚地看出 BUTT 和 SQUARE 的區別。
Canvas 的文字繪製方法有三個:
drawText()
drawTextRun()
drawTextOnPath()
咱們大多數狀況用不了那麼多,因此一樣這裏不作詳解,對於始終想追根到底的同窗,一樣給你提供了 凱哥的連接。
下面只對部分須要注意的重點總結一下。
drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
其中的參數很簡單:text 是文字內容,x 和 y 是文字的座標。但須要注意:這個座標並非文字的左上角,而是一個與左下角比較接近的位置。大概在這裏:
而若是你像繪製其餘內容同樣,在繪製文字的時候把座標填成 (0, 0),文字並不會顯示在 View 的左上角,而是會幾乎徹底顯示在 View 的上方,到了 View 外部看不到的位置:
canvas.drawText(text, 0, 0, paint);
大概是這樣:
另外,Canvas.drawText()
只能繪製單行的文字,而不能換行。就算顯示不完,也會直接繪製到屏幕外面去。
那若是要換行,得 drawText()
不少次嗎?並無,還有一個 StaticLayout
能夠完美達到咱們的效果。對於詳細使用,這裏也很少提了。
對 drawTextRun()
和 drawTextOnPath()
,運用的可能並很少,這裏就不說了。
簡單提一下設置效果輔助類吧,這個可能直接就有用。
設置文字大小: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」有三個值:LEFT
、CENTER
和 RIGHT
,默認值是 LEFT
。
設置繪製所使用的 Locale:Paint.setTextLocale(Locale locale)
/ Paint.setTextLocales(LocaleList locales)
實際上,這些方法基本都在咱們 TextView 裏面的。
範圍裁切主要採用兩個方法:
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.translate(float dx, float dy)
平移,其中,dx 和 dy 分別表示橫向和縱向的位移。
Canvas.rotate(float degrees, float px, float py)
旋轉,其中 degrees
是旋轉角度,順時針爲正向,px
和 py
表明軸心座標。
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 的 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 裏面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。好比你在重疊的位置先畫圓再畫方,和先畫方再畫圓所呈現出來的結果確定是不一樣的:
一般若是咱們繼承的是 View 的話,super.onDraw() 只是一個空實現,因此它的位置放在哪兒都沒事,甚至直接不要也沒事,但反正加上也沒啥影響,儘可能仍是加上吧。
因爲 Android 的繪製順序性,當你繼承自已經有繪製的其餘 View(好比 TextView)的時候,放在 super.onDraw()
上面就意味着繪製代碼會被控件的原內容蓋住。
還記得我上面賣的關子嗎?自定義繪製其實不止 onDraw()
一個方法。onDraw()
只是負責自身主體內容繪製的。而有的時候,你想要的遮蓋關係沒法經過 onDraw()
來實現,而是須要經過別的繪製方法。
凱哥這塊真的寫的是太有意思了,因此我也是直接 copy 了過來。
例如,你繼承了一個 LinearLayout,重寫了它的 onDraw() 方法,在 super.onDraw() 中插入了你本身的繪製代碼,使它可以在內部繪製一些斑點做爲點綴:
public class SpottedLinearLayout extends LinearLayout { ... protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... // 繪製斑點 } }
看起來確實沒有問題,可是你會發現,當你添加了子 View 以後,你的斑點不見了:
形成這種狀況的緣由是 Android 的繪製順序:在繪製過程當中,每個 ViewGroup 會先調用本身的 onDraw()
來繪製完本身的主體以後再去繪製它的子 View。對於上面這個例子來講,就是你的 LinearLayout 會在繪製完斑點後再去繪製它的子 View。那麼在子 View 繪製完成以後,先前繪製的斑點就被子 View 蓋住了。
具體來說,這裏說的「繪製子 View」是經過另外一個繪製方法的調用來發生的,這個繪製方法叫作:dispatchDraw()
。也就是說,在繪製過程當中,每一個 View 和 ViewGroup 都會先調用 onDraw()
方法來繪製主體,再調用 dispatchDraw()
方法來繪製子 View。
注:雖然 View 和 ViewGroup 都有
dispatchDraw()
方法,不過因爲 View 是沒有子 View 的,因此通常來講dispatchDraw()
這個方法只對 ViewGroup(以及它的子類)有意義。
回到剛纔的問題:怎樣才能讓 LinearLayout 的繪製內容蓋住子 View 呢?只要讓它的繪製代碼在子 View 的繪製以後再執行就行了。因此直接執行在 super.dispatchDraw()
的下面便可。
凱哥確實強勢,在文章的最後,直接貼圖,不能再清晰了,因此我也是直接跳過了其中 N 個環節,直接上圖。
注意:
在 ViewGroup 的子類中重寫除
dispatchDraw()
之外的繪製方法時,可能須要調用setWillNotDraw(false)
;在重寫的方法有多個選擇時,優先選擇
onDraw()
。
本期的自定義 View 之繪製就到這裏結束了,強烈推薦 點擊連接 跟着凱哥操,不得挨飛刀。
作不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公衆號搜索「nanchen」關注個人微信公衆號,目前多運營 Android ,盡本身所能爲你提高。若是你喜歡,爲我點贊分享吧~