Flutter-用CustomPaint畫一個自定義的CircleProgressBar

注意:這其實是一篇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控件來矛洞!

國際慣例洼哎,先上效果圖:

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

什么是CustomPaint

CustomPaint 是一個繼承自 SingleChildRenderObjectWidget 的控件,所以注意沼本,不能用setState的方式來刷新它X汀! painter 就是我們的主繪制工具抽兆,它是一個 CustomPainter 识补; foregroundPainter 是用來繪制前景的工具; size 為畫布大小辫红,這個size會傳遞給 Painter 凭涂; isComplexwillChange 是告訴Flutter你的 CustomPaint 是否復雜到需要使用cache相關的功能; child 屬性我們一般不填贴妻,即使你是想要在你的 CustomPaint 上添加一些其他的布局切油,也不建議放在child屬中性,因為你會發(fā)現(xiàn)你并不會得到你想要的結果名惩。

所有的繪制都是發(fā)生在Painter里面的澎胡,繪制的代碼寫在我們的自定義 CustomPainter 中:

我們需要重寫 paint()shouldRepaint() 這兩個方法,一個是繪制流程娩鹉,一個是在刷新布局的時候告訴Flutter是否需要重繪攻谁。注意下 paint 方法中的size參數(shù),就是我們在 CustomPaint 中定義的size屬性弯予,它包含了基本的畫布大小信息戚宦。

真正地繪制則是通過 canvasPaint 來實現(xiàn)的,我們將定義好了的Paint畫筆傳遞給 canvas.drawXXX() 方法锈嫩,這個方法會告訴Flutter我們需要繪制一個什么東西受楼,是一個圓呢、還是一條線呢呼寸?

一些常用的 canvas 繪制API:

一些常用的 Paint 屬性:

繪制步驟分析

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

首先是靜態(tài)進度條的繪制那槽,我們先拆解這個CircleProgressBar為三部分:底部圓環(huán)、進度條和顯示當前進度的小圓點等舔。因為 Canvas的繪制順序是按代碼順序一層一層往上疊加的 骚灸,所以我們的繪制步驟應該是:繪制底部圓環(huán)——>繪制進度條——>繪制小圓點。

然后是手勢拖動的實現(xiàn)慌植,我們選用 GestureDetector 來實現(xiàn)就可以了甚牲,在 onPanUpdate

回調中實時刷新進度條與小圓點的位置,這里面需要注意的地方是可觸摸區(qū)域的計算蝶柿。

靜態(tài)CircleProgressBar繪制

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

繪制所需要的變量基本都標注在上圖中了丈钙,圓心坐標就是整塊畫布的中心點,我們定義為 (center,center) 交汤,其中 center = size.width * 0.5 雏赦。小圓點的半徑定義為 dotRadius 劫笙。灰色實線部分為底部圓環(huán)星岗,progressBar的寬度為紅色虛線部分所示填大,其大小應該比底部圓環(huán)略大,至于大多少俏橘,你可以自己定義允华。在本次的例子中,我將灰色實線與紅色虛線之間的部定義為 radiusOffset = dotRadius * 0.4 寥掐,這個值盡量不要寫死靴寂,那么 radiusOffset*2 就是progressBar寬度比底部圓環(huán)大的值。 innerRadiusoutRadius 分別為底部圓環(huán)的內/外半徑召耘,大小如圖上所示(純數(shù)學知識百炬,不解釋)。然后我們可以根據 innerRadiusoutRadius 計算出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 手销,否則畫筆會默認充滿內部,那么你繪制出來的就是一個圓了图张。

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

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 就是我們的畫筆躏啰。

補充:弧度與角度的弧線轉換:

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

假設當前進度為 progress (范圍為0.0~1.0)趁矾,那么當前角度為 angle = 360.0 * progress ,當前弧度為 radians = degToRad(angle) 给僵,上述代碼可以繪制出一個基礎的圓弧毫捣。但是我們會發(fā)現(xiàn),圓弧的兩端是平的帝际,很影響美觀蔓同,這時候就需要用到 paintstrokeCap 屬性了。

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

我們將 paint 設置為 StrokeCap.round

蹲诀,就能得到一個最基本的進度條了斑粱。

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

接下來我們給進度條添加顏色,按照設計稿脯爪,我們需要添加一個漸變色则北。漸變色可以通過 paintshader

屬性來實現(xiàn):

Flutter提供了三種基礎的用來繪制漸變效果的類:SweepGradient(掃描漸變)、LinearGradient(線性漸變)和RadialGradient(徑向漸變)痕慢。

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

很明顯尚揣,我們需要用到的是 SweepGradient

注意,這里有一個很大的坑掖举,我們可以從上面的SweepGradient事例圖上看到快骗,默認情況下是從90°的地方作為起點的,這跟我們的要求明顯是不符的塔次。SweepGradient有一個startAngle屬性滨巴,那么我們是否可以將其設置為 degToRad(-90°) 就可以解決問題了呢?答案是:不可以俺叭。這里懷疑是Flutter的一個bug恭取,startAngle屬性不生效,我們可以看一下這個issue: SweepGradient startAngle doesn't work as expected.

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

那么怎么解決呢熄守?我想了很久之后決定采用一個曲線救國的方法蜈垮,那就是: 旋轉畫布

:孽恕!攒发。反正是一個圓弧嘛调塌,那我把畫布逆時針旋轉90°不就行了嘛(這里還要注意,畫布默認旋轉中心為坐標軸原點惠猿,而且貌似不能更改羔砾,至少我沒找到,所以需要旋轉后再平移偶妖,對canvas的位置操作需要倒著寫姜凄,所以實際代碼是先寫translate,再寫rotate):

畫到這里你是不是覺得已經很OK了呢趾访?運行一下态秧,啊嘞,怎么會這樣紙扼鞋?

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

這是我們給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)份招,然后將其進行高斯模糊切揭,造成一種加了“陰影”的假象。

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

兩者繪制結果的區(qū)別很明顯锁摔, canvas.drawShadow() 是將整個圓環(huán)作為一個整體廓旬,為其添加陰影;而 MaskFilter.blur() 其實就是繪制兩個模糊的圓環(huán)谐腰,作為一種陰影的替代品孕豹。使用哪種方式繪制,還是取決于你需要什么樣的效果十气。

  • 小圓點外圈繪制

這個沒什么難度的励背,就是在小圓點外面再繪制一個圓環(huán)而已:

到此為止,一個靜態(tài)的CircleProgressBar就繪制完成了:

Flutter:教你用CustomPaint畫一個自定義的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更小一點):

接下來就是計算觸摸點所在的角度了浅萧,要注意根據邊來計算角度時逐沙,位于不同的象限,要做不同的處理:

Flutter:教你用CustomPaint畫一個自定義的CircleProgressBar

將觸摸點所在的角度轉化為進度洼畅,改變 progressController.value 的值吩案,通過 setState() 的方式,通知界面刷新帝簇,一個跟隨著用戶手勢而更改進度的CircleProgressBar就完成了徘郭。

這是因為我們在繪制進度條的時候進行了偏移導致的,如果你想通過調整進度條的方式來修改丧肴,會比較麻煩残揉,不妨換個角度,當角度很小的時候(radians < offset)芋浮,進度條其實是被小圓點擋住了抱环,看不到的,那么直接不繪制就可以了纸巷。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末镇草,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瘤旨,更是在濱河造成了極大的恐慌梯啤,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裆站,死亡現(xiàn)場離奇詭異条辟,居然都是意外死亡,警方通過查閱死者的電腦和手機宏胯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門羽嫡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肩袍,你說我怎么就攤上這事杭棵。” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵魂爪,是天一觀的道長先舷。 經常有香客問我,道長滓侍,這世上最難降的妖魔是什么蒋川? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮撩笆,結果婚禮上捺球,老公的妹妹穿的比我還像新娘。我一直安慰自己夕冲,他們只是感情好氮兵,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歹鱼,像睡著了一般泣栈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弥姻,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天南片,我揣著相機與錄音,去河邊找鬼蚁阳。 笑死铃绒,一個胖子當著我的面吹牛鸽照,可吹牛的內容都是我干的螺捐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矮燎,長吁一口氣:“原來是場噩夢啊……” “哼定血!你這毒婦竟也來了?” 一聲冷哼從身側響起诞外,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤澜沟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后峡谊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茫虽,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年既们,在試婚紗的時候發(fā)現(xiàn)自己被綠了濒析。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡啥纸,死狀恐怖号杏,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情斯棒,我是刑警寧澤盾致,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布主经,位于F島的核電站,受9級特大地震影響庭惜,放射性物質發(fā)生泄漏罩驻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一护赊、第九天 我趴在偏房一處隱蔽的房頂上張望鉴腻。 院中可真熱鬧,春花似錦百揭、人聲如沸爽哎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽课锌。三九已至,卻和暖如春祈秕,著一層夾襖步出監(jiān)牢的瞬間渺贤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工请毛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留志鞍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓方仿,卻偏偏與公主長得像固棚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子仙蚜,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容