用 Flutter 的 Canvas 畫點有趣的圖形

簡介

上一篇我們介紹了使用 Flutter 的 Canvas 繪制基本圖形的示例,簡單的示例沒什么好玩的,今天這一篇我們來點有趣的,我們會完成如下圖形的繪制:

  1. 發現數學重復之美:使用等邊三角形組合成彩虹傘面。
  2. 繪制彩虹。
  3. 繪制評分用的五角星。

通過這一篇,我們可以知道自定義形狀繪制的基本原理,然后可以在這個基礎上繪制你自己想要繪制的圖形。

等邊三角形構建重復之美

首先我們來繪制等邊三角形,其實上一篇我們也有繪制等邊三角形,只是那是將三個頂點手動計算出來的,這一篇我們封裝一個繪制等邊三角形的通用方法。老規矩,先定義方法的輸入參數,如下所示:

  • canvasCanvas 畫布
  • color:繪制顏色
  • startVertex:三角形的第一個頂點位置,這里我們其他邊都是相對這個點旋轉的
  • length:邊長
  • startAngle:第一條邊相對水平方向旋轉的夾角,這樣我們可以改變夾角來更改三角形的繪制位置。
  • clockwise:順時針繪制,如果是順時針,則繪制的偏移夾角往順時針方向開始,否則逆時針。
  • filled:是否填充圖形。
void drawEquilateralTriangle(
    Canvas canvas, {
    required Color color,
    required Offset startVertex,
    required double length,
    double startAngle = 0,
    clockwise = true,
    filled = true,
  })

等邊三角形基于一個頂點,一條邊和起始角度后就可以計算其他兩個頂點的位置,具體推到通過三角函數就可以了。


三角形

具體計算三角形的三個頂點的方法如下,這里逆時針方向和順時針方向的計算方式有點不同,需要區分一下。

static List<Offset> getEquilateralTriangleVertexes(
      Offset startVertex, double length,
      {double startAngle = 0, bool clockwise = true}) {
  double point2X, point2Y, point3X, point3Y;
  point2X = startVertex.dx + length * cos(startAngle);
  point2Y = startVertex.dy - length * sin(startAngle);
  if (clockwise) {
    point3X = startVertex.dx + length * cos(pi / 3 + startAngle);
    point3Y = startVertex.dy - length * sin(pi / 3 + startAngle);
  } else {
    point3X = startVertex.dx + length * cos(pi / 3 - startAngle);
    point3Y = startVertex.dy + length * sin(pi / 3 - startAngle);
  }

  return [startVertex, Offset(point2X, point2Y), Offset(point3X, point3Y)];
}

有了頂點我們就可以使用 Path 將頂點連起來就完成等邊三角形的繪制了,繪制三角形的實現方法如下:

void drawEquilateralTriangle(
    Canvas canvas, {
    required Color color,
    required Offset startVertex,
    required double length,
    double startAngle = 0,
    clockwise = true,
    filled = true,
  }) {
    assert(length > 0);
    Path trianglePath = Path();
    List<Offset> vertexes = ShapesUtil.getEquilateralTriangleVertexes(
      startVertex,
      length,
      clockwise: clockwise,
      startAngle: startAngle,
    );
    trianglePath.moveTo(vertexes[0].dx, vertexes[0].dy);
    for (int i = 1; i < vertexes.length; i++) {
      trianglePath.lineTo(vertexes[i].dx, vertexes[i].dy);
    }
    trianglePath.close();
    Paint paint = Paint();
    paint.color = color;
    if (!filled) {
      paint.style = PaintingStyle.stroke;
    }
    canvas.drawPath(trianglePath, paint);
  }
}

單獨一個三角形沒啥意思,我們通過畫6個等邊三角形,每個三角形旋轉60度,空心繪制看看怎么樣?


6個等邊三角形

一個 完美的六邊形出來了,再試試12個怎么樣。


12個等邊三角形

形狀越多,會越接近圓形,你會充分發現對稱之美。下面是我們用24個三角形,填充不同顏色后的效果。有點像一把彩虹傘的傘面了,感覺是不是很美?

彩虹傘?

上面圖形的實現代碼如下,其中顏色是通過一個顏色數組完成的。

int number = 24;
for (int i = 0; i < number; ++i) {
  drawEquilateralTriangle(
    canvas,
    color: colors[i],
    startVertex: Offset(center.width, center.height),
    length: 120,
    startAngle: i * 2 * pi / number,
    clockwise: true,
    filled: true,
  );
}

繪制彩虹

有了上面的彩虹傘一樣的啟發,我們決定來繪制彩虹。彩虹其實比較簡單,繪制7條不同顏色的弧線即可。這里講一下弧線的繪制約束。如下圖所示,實際上弧線是通過矩形的內接橢圓限制的(這里用正方形,內接為圓形示例)。外面的矩形限制了橢圓位置和尺寸,而通過 startAngle (起始角度)和 sweepAngle (弧線覆蓋的角度范圍)就能夠確定弧線的起點和終點,從而得到一段弧線。注意的是,數學里我們是逆時針角度為正,但是在 Flutter 默認是順時針為正,因此如果你要從逆時針方向開始角度就要設置為負數。

弧線繪制

下面是弧線繪制的示例代碼:

Path path1 = Path();
Rect rect1 = Rect.fromLTWH(startPoint.dx + (width - innerWidth) / 2,
    startPoint.dy + (width - innerWidth) / 2, innerWidth, innerWidth);
path1.arcTo(rect1, -pi / 6, -2 * pi / 3, true);
paint.color = colors[i];
canvas.drawPath(path1, paint);

有了這個基礎,我們通過循環 ,繪制7條弧線,保證每條弧線挨著就行。而弧線的線條粗細可以用畫筆的寬度來搞定,代碼如下。我們這里每條弧線的中心、起始角度和覆蓋角度是一樣的,通過改變不同弧線的正方形邊長實現彩虹弧線的位置不同,然后畫筆粗細保持為每條彩虹的高度的一半就可以保證每條彩虹是挨著的了。

void drawRainbow(
    Canvas canvas, {
    required Offset startPoint,
    required double width,
  }) {
  assert(width > 0);
  var paint = Paint();
  double rowHeight = 12;
  paint.strokeWidth = rowHeight / 2;
  List<Color> colors = [
    Color(0xFFE05100),
    Color(0xFFF0A060),
    Color(0xFFE0E000),
    Color(0xFF10F020),
    Color(0xFF2080F5),
    Color(0xFF104FF0),
    Color(0xFFA040E5),
  ];
  paint.style = PaintingStyle.stroke;
  for (var i = 0; i < 7; i++) {
    double innerWidth = width - i * rowHeight;
    Path path1 = Path();
    Rect rect1 = Rect.fromLTWH(startPoint.dx + (width - innerWidth) / 2,
        startPoint.dy + (width - innerWidth) / 2, innerWidth, innerWidth);
    path1.arcTo(rect1, -pi / 6, -2 * pi / 3, true);
    paint.color = colors[i];
    canvas.drawPath(path1, paint);
  }
}

最終效果如下圖所示。


彩虹

繪制五角星

五角星相對來說會復雜一些,主要是要知道通過中心點確定10個頂點的坐標,這里就需要利用二維坐標的旋轉公式了,具體可以查閱相關資料,結論是一個點(x2, y2)圍繞另一個點(x1, y1)旋轉某個角度(α)后得到的新坐標(x, y)計算方式如下:

坐標計算

有了這個基礎,我們就可以基于五角星的中心點,第一個頂點,邊長(間隔一個點連線的線段長度)來通過旋轉計算其他頂點了。其中外面5頂點一組計算,內部5個頂點一組計算。最終獲取5個頂點的代碼如下:

static List<Offset> getStarVertexes(Offset center, double length) {
  assert(length > 0);
  // 外接圓半徑計算(五角星銳角為36度)
  double radius = length / 2 / cos(18 / 180 * pi);
  // 內部頂點的半徑
  double innerRadius =
      radius / (cos(36 / 180 * pi) + sin(36 / 180 * pi) / sin(18 / 180 * pi));
  List<Offset> vertexes = [];
  Offset outerStartVertex = Offset(center.dx, center.dy - radius);
  Offset innerStartVertex = Offset(
    center.dx - innerRadius * sin(36 / 180 * pi),
    center.dy - innerRadius * cos(36 / 180 * pi),
  );
  vertexes.add(outerStartVertex);
  vertexes.add(innerStartVertex);
  // 計算方式為以第一個頂點圍繞五角星中心點坐標旋轉得到
  const double rotateAngle = 72 / 180 * pi;
  for (int i = 1; i < 5; ++i) {
    vertexes.add(Offset(
      center.dx +
          (outerStartVertex.dx - center.dx) * cos(-i * rotateAngle) -
          (outerStartVertex.dy - center.dy) * sin(-i * rotateAngle),
      center.dy +
          (outerStartVertex.dy - center.dy) * cos(-i * rotateAngle) +
          (outerStartVertex.dx - center.dx) * sin(-i * rotateAngle),
    ));
    vertexes.add(Offset(
      center.dx +
          (innerStartVertex.dx - center.dx) * cos(-i * rotateAngle) -
          (innerStartVertex.dy - center.dy) * sin(-i * rotateAngle),
      center.dy +
          (innerStartVertex.dy - center.dy) * cos(-i * rotateAngle) +
          (innerStartVertex.dx - center.dx) * sin(-i * rotateAngle),
    ));
  }

  return vertexes;
}

有了頂點,繪制方式就和三角形一樣了,將頂點連起來就好了。下面是我們繪制了一個常見的五星評分的圖形。

五星評分

總結

本篇介紹了基于 Flutter 的 CustomPaint 繪制定制化圖形的示例,可以看到,其實只要 UI 小姐姐給出的圖形能夠用數學表達式表示出來,都可以用 CustomPaintCanvas 來實現。本篇代碼已上傳至:繪圖相關代碼

當然,如果 UI 小姐姐給了一個無法用數學表達式解決的形狀,那么你要往深處想想,小姐姐是不是看上你了!如果不是看上你,那么你可以大膽地回一句:“這個做不了,給我切個圖吧!”

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容