用 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阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容