Flutter 自定義 Widget 之餅形圖實戰

曉峰殘月發表於2019-09-19

本文主要講述了 Flutter 如何實現自定義 Widget 以及自定義餅形圖實戰,如有不當之處敬請指正。

閱讀本文大約需要6分鐘。

背景

Flutter 官方目前已經提供很多的小部件,可以直接使用,有 Material 風格的小部件,也有 iOS 風格的小部件,還有一些佈局相關的小部件。在正常開發中能滿足絕大多的頁面場景,但是仍有部分小部件是官方沒有提供的。雖然官方沒有提供完整的小部件,但是官方提供了讓我們自定義小部件的功能。

介紹

在 Flutter 中自定義 Widget 常用的有二種方式:通過組合其他 Widget 、自繪。

  1. 組合其他 Widget

    這種方式是通過拼裝其他基礎的 Widget 來組合成一個新的 Widget ,比如使用 Icon 和 Text 放在 Row 來組合成一個帶圖示功能的 Text。

    在平時的 Flutter 中經常會使用這種方法來實現不同的佈局。

  2. 自繪

    如果遇到無法通過組合完成的頁面UI,或者一些獨特的UI,比如圓形進度條,統計圖表等。這個時候最好的辦法就是通過自定義 Widget 來繪畫出我們所需要的樣子,在 Flutter 中提供了 CustomPainter 和 Canvas 來供我們繪製。

方法

對於複雜或者不規則的 UI ,我們可能無法使用組合的方式完成。比如:需要一個三角形,五邊形,一個折線圖,一個餅形圖,數字進度條等。有時候我們可以直接讓 UI 設計師直接提供圖片去展示,但是有些資料是動態的或者 UI 是需要和使用者互動的,這個時候使用圖片可能就達不到我們所需要的效果了,就需要我們自己去實現繪製 UI 了。

幾乎所有的 UI 系統都會提供一個自繪 UI 的介面,這個介面通常會提供一個 2D 的畫布 Canvas,在 Canvas 內部封裝了一些基礎的繪製 API,我們只需要呼叫相關的繪製 API 就可以繪製各種自定的圖形了。

在 Flitter 中,它為我們提供了一個 CustomPainter Widget,我們可以結合畫筆 CustomPainter 來實現自定義 Widget。

繼承 CustomPainter 需要實現這個類的兩個關鍵方法:paintshouldRepaint 。在 paint 方法決定繪製什麼,使用傳遞過來的 canvas 和 size 完成繪製,shouldRepaint 決定否需要重繪的,返回 false 代表這個 Widget 繪製完成後不需要重新繪製。

繪製

想要完成繪製僅靠 canvas 是無法完成繪製的,還需要一個畫筆 paint 。

構建 paint

  Paint _paint = Paint()
    ..color = Colors.red
    ..isAntiAlias = true
    ..style = PaintingStyle.fill
    ..strokeWidth = 12.0;
複製程式碼
  1. color: 設定畫筆顏色;

  2. isAntiAlias:是否開啟抗鋸齒;

  3. style:設定填充模式;

  4. strokeWidth:設定畫筆粗細

    Paint 的設定有很多,但是正常開發中不會使用那麼多的屬性,具體的可以參考一下官方文件;

Canvas用法

  1. 繪製點

    drawPoints(PointMode pointMode, List points, Paint paint)

    繪製點只需要傳入PointMode列舉和 point 集合就可以了。

    pointMode列舉有三個:points(點),lines(線,隔點連線),polygon(線,相鄰連線)

  2. 繪製圓

    canvas.drawCircle(offset, radius, paint)

    繪製圓需要傳入圓心 offset ,半徑 radius,設定paint的填充模式可以繪製填充和不填充的圓。

  3. 繪製橢圓

    drawOval(Rect rect, Paint paint)

    繪製橢圓需要傳入一個矩形 Rect 來確定大小和位置。

  4. 繪製圓弧

    drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

    繪製圓弧需要傳入的引數比較多一點,首先需要一個矩形 Rect 來確定大小和位置,接著傳入開始弧度 startAngle,多少弧度 sweepAngle,是否使用中心點繪製 useCenter。

    這裡需要注意的是,Android繪製 startAngle 和 sweepAngle 使用的是角度,這裡使用的弧度,角度和弧度的換算為:

    弧度 = 角度 * PI/180;

    角度 = 弧度 * 180/PI;

  5. 繪製圓角矩形

    drawRRect(RRect rrect, Paint paint)

    繪製圓角矩形比較簡單,只需要傳入 RRect,RRect 可直接使用 fromRectAndRadius,傳入矩形大小位置 Rect 和圓角大小的 Radius。

常用的繪製方法就這些,canvas提供了很多的繪製,可以去官方文件檢視。

實戰餅形圖

效果圖

Flutter 自定義 Widget 之餅形圖實戰

從圖中看大致可以分為三個步驟:

  1. 繪製扇形區域
  2. 繪製每個扇形區域的線
  3. 繪製文字

1、確定大小位置

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)

2、繪製扇形

確定了圓形的圓心大小後,我們就需要繪製組成圓形的每一個扇形,這裡我們繪製扇形需要知道扇形的大小,所有我們需要先定義一個資料類:

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;
}
複製程式碼

3、繪製線

首先繪製線分為兩個部分,一部分是斜的,一部分是橫線,首先我們繪製斜線:

繪製斜線首先找到繪製線的兩個座標點,一個座標點在扇形的中間,且點在扇形的邊緣,另一個轉折點是圓心到起始點的延長線上。

首先通過角度確定第一個點,角度為起始角度+繪製角度的二分之一,通過三角函式計算出繪製線的起始點:

// 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)));
複製程式碼

計算完起始點和轉折點需要計算終點的座標,終點的座標分為兩種情況,一種是在圓心的左邊,那橫線就是向左繪製,另一種就是在右邊,橫線需要向右邊繪製,根據判斷左右得出終點的座標:

// 2、計算座標在左邊還是在右邊,並計算橫線結束座標
var endX;
if (stopX - startX > 0) {
  endX = stopX + line2;
} else {
  endX = stopX - line2;
}
複製程式碼

得到了起始點,轉折點,和結束點的座標,接下來需要根據相應的座標點繪製斜線和橫線即可:

// 3、繪製斜線和橫線
canvas.drawLine(Offset(startX, startY), Offset(stopX, stopY), _paint);
canvas.drawLine(Offset(stopX, stopY), Offset(endX, stopY), _paint);
複製程式碼

4、繪製文字

繪製完線,接下來需要繪製橫線上方和下方的文字,上方繪製扇形所佔的百分比,下方繪製標題。

在 Flutter 中繪製文字不是使用 Canvas 繪製,而是使用畫筆 TextPainter 繪製。

在 TextPainter 中可以設定文字畫筆的風格和文字的屬性:

// 文字畫筆 風格定義
TextPainter _newVerticalAxisTextPainter(String text, Color color) {
  return _textPainter
    ..text = TextSpan(
      text: text,
      style: new TextStyle(
        color: color,
        fontSize: 12.0,
      ),
    );
}
複製程式碼

首先我們繪製也需要計算文字開始的座標:

    // 4、繪製文字
    // 繪製下方名稱
    // 上下間距偏移量
    var offset = 4;
    // 1、測量文字
    var tp = _newVerticalAxisTextPainter(name, color);
    tp.layout();

    var w = tp.width;
    // 2、計算文字座標
    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。

到此,本文就結束了,如有不當之處敬請指正,一起學習探討,謝謝?。

slogan

相關文章