本文主要講述了 Flutter 如何實現自定義 Widget 以及自定義餅形圖實戰,若有不當之處敬請指正。git
閱讀本文大約須要6分鐘。github
Flutter 官方目前已經提供不少的小部件,能夠直接使用,有 Material 風格的小部件,也有 iOS 風格的小部件,還有一些佈局相關的小部件。在正常開發中能知足絕大多的頁面場景,可是仍有部分小部件是官方沒有提供的。雖然官方沒有提供完整的小部件,可是官方提供了讓咱們自定義小部件的功能。canvas
在 Flutter 中自定義 Widget 經常使用的有二種方式:經過組合其餘 Widget 、自繪。函數
組合其餘 Widget佈局
這種方式是經過拼裝其餘基礎的 Widget 來組合成一個新的 Widget ,好比使用 Icon 和 Text 放在 Row 來組合成一個帶圖標功能的 Text。學習
在平時的 Flutter 中常常會使用這種方法來實現不一樣的佈局。ui
自繪this
若是遇到沒法經過組合完成的頁面UI,或者一些獨特的UI,好比圓形進度條,統計圖表等。這個時候最好的辦法就是經過自定義 Widget 來繪畫出咱們所須要的樣子,在 Flutter 中提供了 CustomPainter 和 Canvas 來供咱們繪製。spa
對於複雜或者不規則的 UI ,咱們可能沒法使用組合的方式完成。好比:須要一個三角形,五邊形,一個折線圖,一個餅形圖,數字進度條等。有時候咱們能夠直接讓 UI 設計師直接提供圖片去展現,可是有些數據是動態的或者 UI 是須要和用戶交互的,這個時候使用圖片可能就達不到咱們所須要的效果了,就須要咱們本身去實現繪製 UI 了。設計
幾乎全部的 UI 系統都會提供一個自繪 UI 的接口,這個接口一般會提供一個 2D 的畫布 Canvas,在 Canvas 內部封裝了一些基礎的繪製 API,咱們只須要調用相關的繪製 API 就能夠繪製各類自定的圖形了。
在 Flitter 中,它爲咱們提供了一個 CustomPainter Widget,咱們能夠結合畫筆 CustomPainter 來實現自定義 Widget。
繼承 CustomPainter 須要實現這個類的兩個關鍵方法:paint
和 shouldRepaint
。在 paint 方法決定繪製什麼,使用傳遞過來的 canvas 和 size 完成繪製,shouldRepaint 決定否須要重繪的,返回 false 表明這個 Widget 繪製完成後不須要從新繪製。
想要完成繪製僅靠 canvas 是沒法完成繪製的,還須要一個畫筆 paint 。
Paint _paint = Paint()
..color = Colors.red
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 12.0;
複製代碼
color: 設置畫筆顏色;
isAntiAlias:是否開啓抗鋸齒;
style:設置填充模式;
strokeWidth:設置畫筆粗細
Paint 的設置有不少,可是正常開發中不會使用那麼多的屬性,具體的能夠參考一下官方文檔;
繪製點
drawPoints(PointMode pointMode, List points, Paint paint)
繪製點只須要傳入PointMode枚舉和 point 集合就能夠了。
pointMode枚舉有三個:points(點),lines(線,隔點鏈接),polygon(線,相鄰鏈接)
繪製圓
canvas.drawCircle(offset, radius, paint)
繪製圓須要傳入圓心 offset ,半徑 radius,設置paint的填充模式能夠繪製填充和不填充的圓。
繪製橢圓
drawOval(Rect rect, Paint paint)
繪製橢圓須要傳入一個矩形 Rect 來肯定大小和位置。
繪製圓弧
drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
繪製圓弧須要傳入的參數比較多一點,首先須要一個矩形 Rect 來肯定大小和位置,接着傳入開始弧度 startAngle,多少弧度 sweepAngle,是否使用中心點繪製 useCenter。
這裏須要注意的是,Android繪製 startAngle 和 sweepAngle 使用的是角度,這裏使用的弧度,角度和弧度的換算爲:
弧度 = 角度 * PI/180;
角度 = 弧度 * 180/PI;
繪製圓角矩形
drawRRect(RRect rrect, Paint paint)
繪製圓角矩形比較簡單,只須要傳入 RRect,RRect 可直接使用 fromRectAndRadius,傳入矩形大小位置 Rect 和圓角大小的 Radius。
經常使用的繪製方法就這些,canvas提供了不少的繪製,能夠去官方文檔查看。
從圖中看大體能夠分爲三個步驟:
if (size.width > size.height) {
radius = size.height / 3;
} else {
radius = size.width / 3;
}
line1 = radius / 3;
line2 = radius / 2;
canvas.translate(size.width / 2, size.height / 2);
Rect rect = Rect.fromLTRB(-radius, -radius, radius, radius);
複製代碼
首先根據size的大小肯定咱們所要繪製的圓形的半徑,這裏的半徑設置爲寬高中較小的一邊的三分之一,爲何不是一半,是由於後面須要繪製線和文字,全部須要預留出來。
接着肯定繪製圓的圓心,這裏直接使用 Canvas 的 translate 方法把畫布移動到圓心,接着設置圓的大小和位置爲: Rect.fromLTRB(-radius, -radius, radius, radius)
。
肯定了圓形的圓心大小後,咱們就須要繪製組成圓形的每個扇形,這裏咱們繪製扇形須要知道扇形的大小,全部咱們須要先定義一個數據類:
abstract class BasePieEntity{
String getTitle();
double getData();
double angle;
Color getColor();
}
複製代碼
定義了一個抽象類,只須要實現 getTitle、getData 和 getColor 這三個方法,具體的數據類能夠根據業務需求定義,基礎該基礎類便可。
在接收數據的時候,須要統計出每個數據須要多大的角度:
var total = 0.0;
this.entities.forEach((e) {
total += e.getData();
});
this.entities.forEach((e) {
e.angle = e.getData() / total * 360;
});
複製代碼
計算出每條數據的角度,接下來咱們只須要循環這個數據,根據數據中的角度繪製每個扇形區域便可:
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
_paint.color = entity.getColor();
canvas.drawArc(rect, (currentAngle * pi / 180), (entity.angle * pi / 180),
true, _paint);
currentAngle += entity.angle;
}
複製代碼
首先繪製線分爲兩個部分,一部分是斜的,一部分是橫線,首先咱們繪製斜線:
繪製斜線首先找到繪製線的兩個座標點,一個座標點在扇形的中間,且點在扇形的邊緣,另外一個轉折點是圓心到起始點的延長線上。
首先經過角度肯定第一個點,角度爲起始角度+繪製角度的二分之一,經過三角函數計算出繪製線的起始點:
// 1,計算開始座標和轉折點座標
var startX = r * (cos((currentAngle + (angle / 2)) * (pi / 180)));
var startY = r * (sin((currentAngle + (angle / 2)) * (pi / 180)));
複製代碼
同理根據延長線的大小加上半徑使用三角函數便可得出轉折點的座標:
var stopX = (r + line1) * (cos((currentAngle + (angle / 2)) * (pi / 180)));
var stopY = (r + line1) * (sin((currentAngle + (angle / 2)) * (pi / 180)));
複製代碼
計算完起始點和轉折點須要計算終點的座標,終點的座標分爲兩種狀況,一種是在圓心的左邊,那橫線就是向左繪製,另外一種就是在右邊,橫線須要向右邊繪製,根據判斷左右得出終點的座標:
// 二、計算座標在左邊仍是在右邊,並計算橫線結束座標
var endX;
if (stopX - startX > 0) {
endX = stopX + line2;
} else {
endX = stopX - line2;
}
複製代碼
獲得了起始點,轉折點,和結束點的座標,接下來須要根據相應的座標點繪製斜線和橫線便可:
// 三、繪製斜線和橫線
canvas.drawLine(Offset(startX, startY), Offset(stopX, stopY), _paint);
canvas.drawLine(Offset(stopX, stopY), Offset(endX, stopY), _paint);
複製代碼
繪製完線,接下來須要繪製橫線上方和下方的文字,上方繪製扇形所佔的百分比,下方繪製標題。
在 Flutter 中繪製文字不是使用 Canvas 繪製,而是使用畫筆 TextPainter 繪製。
在 TextPainter 中能夠設置文字畫筆的風格和文字的屬性:
// 文字畫筆 風格定義
TextPainter _newVerticalAxisTextPainter(String text, Color color) {
return _textPainter
..text = TextSpan(
text: text,
style: new TextStyle(
color: color,
fontSize: 12.0,
),
);
}
複製代碼
首先咱們繪製也須要計算文字開始的座標:
// 四、繪製文字
// 繪製下方名稱
// 上下間距偏移量
var offset = 4;
// 一、測量文字
var tp = _newVerticalAxisTextPainter(name, color);
tp.layout();
var w = tp.width;
// 二、計算文字座標
var textStartX;
if (stopX - startX > 0) {
if (w > line2) {
textStartX = (stopX + offset);
} else {
textStartX = (stopX + (line2 - w) / 2);
}
} else {
if (w > line2) {
textStartX = (stopX - offset - w);
} else {
textStartX = (stopX - (line2 - w) / 2 - w);
}
}
複製代碼
同理,計算出上方百分比文字的座標:
// 繪製上方百分比,步驟同上
var per = (angle / 360.0 * 100).toStringAsFixed(2) + "%";
var tpPre = _newVerticalAxisTextPainter(per, color);
tpPre.layout();
w = tpPre.width;
var h = tpPre.height;
if (stopX - startX > 0) {
if (w > line2) {
textStartX = (stopX + offset);
} else {
textStartX = (stopX + (line2 - w) / 2);
}
} else {
if (w > line2) {
textStartX = (stopX - offset - w);
} else {
textStartX = (stopX - (line2 - w) / 2 - w);
}
}
複製代碼
計算得出起始座標,接下來繪製下方文字:
tp.paint(canvas, Offset(textStartX, stopY + offset));
複製代碼
上方百分比文字:
tpPre.paint(canvas, Offset(textStartX, stopY - offset - h));
複製代碼
至此,繪製一個餅形圖就完成了。
完整代碼奉上GitHub地址:flutter_demo ,歡迎star和fork。
到此,本文就結束了,若有不當之處敬請指正,一塊兒學習探討,謝謝🙏。