注意:這其實是一篇CustomPaint的使用教程S汲ⅰ候学!
源碼地址: github.com/yumi0629/Fl…
在Flutter中, CustomPaint
就像是 Android 中的Paint一樣撒穷,可以用它繪制出各種各樣的自定義圖形竭鞍。確實板惑,Paint的使用比較復雜,我覺得直接講API的話也太無聊了偎快,要記住Paint的用法冯乘,還是自己動手畫一個比較實在。
那為什么是畫一個CircleProgressBar呢晒夹?其實這個控件本來是為了交作業(yè)的裆馒,之前在講Hero的時候留了一個小練習,里面有一個頁面丐怯,有一個很炫酷的圓形ProgressBar選擇器喷好,當時為了偷懶我就沒寫(不要打我),所以現(xiàn)在來補交來读跷。在寫這個CircleProgressBar的時候發(fā)現(xiàn)梗搅, CustomPaint
中基本的API都使用到了,畫圓效览、畫弧線无切、畫布旋轉、Paint的各種屬性的意義等等知識點都有涉及到丐枉。所以說哆键,看完這篇文章,你絕對可以自己動手嘗試畫一些炫酷的UI控件來矛洞!
國際慣例洼哎,先上效果圖:
什么是CustomPaint
CustomPaint
是一個繼承自 SingleChildRenderObjectWidget
的控件,所以注意沼本,不能用setState的方式來刷新它X汀! painter
就是我們的主繪制工具抽兆,它是一個 CustomPainter
识补; foregroundPainter
是用來繪制前景的工具; size
為畫布大小辫红,這個size會傳遞給 Painter
凭涂; isComplex
和 willChange
是告訴Flutter你的 CustomPaint
是否復雜到需要使用cache相關的功能; child
屬性我們一般不填贴妻,即使你是想要在你的 CustomPaint
上添加一些其他的布局切油,也不建議放在child屬中性,因為你會發(fā)現(xiàn)你并不會得到你想要的結果名惩。
所有的繪制都是發(fā)生在Painter里面的澎胡,繪制的代碼寫在我們的自定義 CustomPainter
中:
我們需要重寫 paint()
和 shouldRepaint()
這兩個方法,一個是繪制流程娩鹉,一個是在刷新布局的時候告訴Flutter是否需要重繪攻谁。注意下 paint
方法中的size參數(shù),就是我們在 CustomPaint
中定義的size屬性弯予,它包含了基本的畫布大小信息戚宦。
真正地繪制則是通過 canvas
和 Paint
來實現(xiàn)的,我們將定義好了的Paint畫筆傳遞給 canvas.drawXXX()
方法锈嫩,這個方法會告訴Flutter我們需要繪制一個什么東西受楼,是一個圓呢、還是一條線呢呼寸?
一些常用的 canvas
繪制API:
一些常用的 Paint
屬性:
繪制步驟分析
首先是靜態(tài)進度條的繪制那槽,我們先拆解這個CircleProgressBar為三部分:底部圓環(huán)、進度條和顯示當前進度的小圓點等舔。因為 Canvas的繪制順序是按代碼順序一層一層往上疊加的 骚灸,所以我們的繪制步驟應該是:繪制底部圓環(huán)——>繪制進度條——>繪制小圓點。
然后是手勢拖動的實現(xiàn)慌植,我們選用 GestureDetector
來實現(xiàn)就可以了甚牲,在 onPanUpdate
回調中實時刷新進度條與小圓點的位置,這里面需要注意的地方是可觸摸區(qū)域的計算蝶柿。
靜態(tài)CircleProgressBar繪制
繪制所需要的變量基本都標注在上圖中了丈钙,圓心坐標就是整塊畫布的中心點,我們定義為 (center,center)
交汤,其中 center = size.width * 0.5
雏赦。小圓點的半徑定義為 dotRadius
劫笙。灰色實線部分為底部圓環(huán)星岗,progressBar的寬度為紅色虛線部分所示填大,其大小應該比底部圓環(huán)略大,至于大多少俏橘,你可以自己定義允华。在本次的例子中,我將灰色實線與紅色虛線之間的部定義為 radiusOffset = dotRadius * 0.4
寥掐,這個值盡量不要寫死靴寂,那么 radiusOffset*2
就是progressBar寬度比底部圓環(huán)大的值。 innerRadius
和 outRadius
分別為底部圓環(huán)的內/外半徑召耘,大小如圖上所示(純數(shù)學知識百炬,不解釋)。然后我們可以根據 innerRadius
和 outRadius
計算出progressBar寬度 progressWith = outerRadius - innerRadius + radiusOffset
污它。 drawRadius
是一個大小為畫布寬度的一半減去小圓點半徑的變量收壕,這個變量在繪制progressBar和小圓點的時候很有用,用來確定progressBar和小圓點的位置轨蛤。
Step 1 底部圓環(huán)繪制
底部圓環(huán)的繪制非常簡單蜜宪,實際上就是畫一個圓。為什么說畫圓環(huán)和畫圓會是一樣的呢祥山? Paint
是畫筆圃验,回想一下我們在寫字的時候,寫出來的字是不是有粗有細缝呕?同樣地澳窑, Paint
在畫線的時候也是有寬度的,我們畫一個有寬度的圓供常,不就是畫一個圓環(huán)了嗎摊聋?
canvas.drawCircle(Offset c, double radius, Paint paint)
這個方法就是繪制一個圓,其中c為圓心坐標點栈暇,這個offset偏移值是以畫布原點(左上角)為坐標軸中心點來計算的麻裁,很明顯大小為 offsetCenter = Offset(center, center)
;radius為圓環(huán)半徑源祈,大小其實就是圖上標示的 drawRadius
煎源;paint就是我們的畫筆,這里要注意香缺,繪制圓環(huán)需要設置 style = PaintingStyle.stroke
手销,否則畫筆會默認充滿內部,那么你繪制出來的就是一個圓了图张。
Step 2 底部進度條
繪制進度條實際上就是繪制圓弧锋拖,我們使用 canvas.drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
诈悍。 rect參數(shù)就是圓弧所在的整圓的Rect,我們使用 Rect.fromCircle
來構造這個整圓的Rect: final Rect arcRect = Rect.fromCircle(center: offsetCenter, radius: drawRadius);
兽埃; startAngle
為起始弧度侥钳, sweepAngle
為需要繪制的圓弧長度,這里要注意讲仰,這兩個值都是 弧度制 的慕趴,canvas里面與角度有關的變量都是弧度制的痪蝇,在計算的時候一定要注意鄙陡; useCenter
屬性標示是否需要將圓弧與圓心相連; paint
就是我們的畫筆躏啰。
補充:弧度與角度的弧線轉換:
假設當前進度為 progress
(范圍為0.0~1.0)趁矾,那么當前角度為 angle = 360.0 * progress
,當前弧度為 radians = degToRad(angle)
给僵,上述代碼可以繪制出一個基礎的圓弧毫捣。但是我們會發(fā)現(xiàn),圓弧的兩端是平的帝际,很影響美觀蔓同,這時候就需要用到 paint
的 strokeCap
屬性了。
我們將 paint
設置為 StrokeCap.round
蹲诀,就能得到一個最基本的進度條了斑粱。
接下來我們給進度條添加顏色,按照設計稿脯爪,我們需要添加一個漸變色则北。漸變色可以通過 paint
的 shader
屬性來實現(xiàn):
Flutter提供了三種基礎的用來繪制漸變效果的類:SweepGradient(掃描漸變)、LinearGradient(線性漸變)和RadialGradient(徑向漸變)痕慢。
很明顯尚揣,我們需要用到的是 SweepGradient
:
注意,這里有一個很大的坑掖举,我們可以從上面的SweepGradient事例圖上看到快骗,默認情況下是從90°的地方作為起點的,這跟我們的要求明顯是不符的塔次。SweepGradient有一個startAngle屬性滨巴,那么我們是否可以將其設置為 degToRad(-90°)
就可以解決問題了呢?答案是:不可以俺叭。這里懷疑是Flutter的一個bug恭取,startAngle屬性不生效,我們可以看一下這個issue: SweepGradient startAngle doesn't work as expected.
那么怎么解決呢熄守?我想了很久之后決定采用一個曲線救國的方法蜈垮,那就是: 旋轉畫布
:孽恕!攒发。反正是一個圓弧嘛调塌,那我把畫布逆時針旋轉90°不就行了嘛(這里還要注意,畫布默認旋轉中心為坐標軸原點惠猿,而且貌似不能更改羔砾,至少我沒找到,所以需要旋轉后再平移偶妖,對canvas的位置操作需要倒著寫姜凄,所以實際代碼是先寫translate,再寫rotate):
畫到這里你是不是覺得已經很OK了呢趾访?運行一下态秧,啊嘞,怎么會這樣紙扼鞋?
這是我們給stroke設置了StrokeCap.round導致的申鱼,因為Flutter在給線繪制圓角時,是在線長的外面加了一段圓角云头,導致實際長度會超過我們定義的長度捐友。那怎么辦呢?還是曲線救國溃槐,我們在drawArc的時候匣砖,將起始角度往后偏移一段不就可以了嗎?我們將這段偏移弧度定義為 offset
竿痰,其大小為 offset = asin(progressWidth * 0.5 / drawRadius)
(怎么算出來的脆粥?數(shù)學問題,自己那張草稿紙畫畫就知道啦~)影涉。
所以最終的繪制代碼應該為:
那么到此為止变隔,我們的進度條部分也繪制完成了。
Step 3 繪制小圓點
繪制小圓點就比較簡單了蟹倾,只要計算出小圓點的圓心位置就可以了匣缘,純初中數(shù)學計算,自己拿紙畫畫就知道啦鲜棠。繪制函數(shù)依然是 canvas.drawCircle
肌厨,因為是繪制圓,所以不需要更改PaintingStyle豁陆。
Step 4 細節(jié)修飾:繪制底部圓環(huán)陰影和小圓點外圈
- 繪制圓環(huán)陰影
繪制陰影有兩種方法柑爸,實現(xiàn)出來的效果也不太一樣。
1)使用 canvas.drawShadow()
來繪制 :
drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
盒音,根據API要求表鳍,我們需要先計算出圓環(huán)的Path馅而,Path的相關API只支持向path中添加圓、弧線譬圣、直線瓮恭、點等屬性,我們沒法直接構建一個圓環(huán)對應的對象Path厘熟。換個角度思考一下屯蹦,圓環(huán)的Path其實是外層圓與內層圓組合的結果,所以我們使用 Path.combine()
方法來獲得圓環(huán)的路徑绳姨,通過設置組合模式為 PathOperation.difference
可以獲取內外兩個圓的公共部分的Path登澜,也就是圓環(huán)的Path:
2)使用paint的 MaskFilter.blur()
來繪制 :
這個方法其實是用來繪制毛玻璃效果的,用來繪制陰影就缆,聽起來也有些曲線救國的意味帖渠,但是官方注釋中有一句話:
所以這個真的也是可以用來繪制陰影的谒亦,而且Flutter在繪制一些Button控件的時候也是使用來blur的效果來實現(xiàn)的竭宰。 MaskFilter.blur()
其實就是將你繪制的東西變模糊,所以我們可以繪制一個圓環(huán)份招,然后將其進行高斯模糊切揭,造成一種加了“陰影”的假象。
兩者繪制結果的區(qū)別很明顯锁摔, canvas.drawShadow()
是將整個圓環(huán)作為一個整體廓旬,為其添加陰影;而 MaskFilter.blur()
其實就是繪制兩個模糊的圓環(huán)谐腰,作為一種陰影的替代品孕豹。使用哪種方式繪制,還是取決于你需要什么樣的效果十气。
- 小圓點外圈繪制
這個沒什么難度的励背,就是在小圓點外面再繪制一個圓環(huán)而已:
到此為止,一個靜態(tài)的CircleProgressBar就繪制完成了:
添加手勢控制
手勢控制我們通過最簡單的方式來實現(xiàn)砸西,那就是在CircleProgressBar外面包裹一層 GestureDetector
叶眉,然后在 onPanUpdate
回調中刷新進度:
進度的記錄我們依然是使用 AnimationController
,因為我們可以使用 controller.animateTo()
方法芹枷,很方便得將進度條從當前位置平滑地移動到目標位置:
接下來就是判斷用戶的觸摸點是否在有效范圍內衅疙,因為用戶只有在觸摸圓環(huán)的時候才應該觸發(fā)手勢,判斷方法也很簡單鸳慈,那就是看系統(tǒng)反饋給我們的pointer位置收否位于圓環(huán)上饱溢。但是實際操作會有一個問題,那就是系統(tǒng)反饋的觸摸點位置是一個全局的坐標點走芋,坐標軸原點在屏幕的左上角绩郎,然后圓環(huán)在屏幕中的全局坐標我們無法知曉絮识。好在Flutter為我們提供了一個全局坐標與局部坐標的轉換方法:
拿到局部坐標后,通過計算觸摸點與圓心的距離嗽上,是否在內次舌、外半徑范圍內,就可以判斷是否為有效觸摸了(一般情況下觸摸范圍會比圓環(huán)更大一線兽愤,方便用戶操作彼念,所以我將validInnerRadius的值,設置地比widget.radius - widget.dotRadius更小一點):
接下來就是計算觸摸點所在的角度了浅萧,要注意根據邊來計算角度時逐沙,位于不同的象限,要做不同的處理:
將觸摸點所在的角度轉化為進度洼畅,改變 progressController.value
的值吩案,通過 setState()
的方式,通知界面刷新帝簇,一個跟隨著用戶手勢而更改進度的CircleProgressBar就完成了徘郭。
這是因為我們在繪制進度條的時候進行了偏移導致的,如果你想通過調整進度條的方式來修改丧肴,會比較麻煩残揉,不妨換個角度,當角度很小的時候(radians < offset)芋浮,進度條其實是被小圓點擋住了抱环,看不到的,那么直接不繪制就可以了纸巷。