Flutter Path(二) : Path 進階

Flutter程式設計開發 發表於 2020-05-10

原文地址 : Playing with Paths in Flutter

在 Flutter 中,萬物皆是 Widget ,同時 Flutter 中也提供了許多了不起的 Widget 供我們使用,但是這裡面最能令人喜歡的還是 CustomPaint

CustomPaint 這個元件為我們提供了一個畫布,在 Flutter 的繪圖(paint)階段,我們可以把我們想要繪畫內容繪製上去。

想要在 canvas 上繪圖,有多種不同的方式,其中最高效和常用的就是使用 Path,在本篇文章中,將會展示 Path 的繪製以及在 Path 上應用動畫。如果你對 Path 不熟悉的話,可以參考一下這篇文章

一、畫線

在 Flutter 中,通過 Path 畫線是非常容易的一件事。 首先,將繪製的啟動通過 moveTo 方法移動到指定位置,然後通過 lineTo 方法進行繪製。

class LinePainter extends CustomPainter {
  final double progress;

  LinePainter({this.progress});

  Paint _paint = Paint()
    ..color = Colors.black
    ..strokeWidth = 4.0
    ..style = PaintingStyle.stroke
    ..strokeJoin = StrokeJoin.round;

  @override
  void paint(Canvas canvas, Size size) {
    var path = Path();
    path.moveTo(0, size.height / 2);
    path.lineTo(size.width * progress, size.height / 2);
    canvas.drawPath(path, _paint);
  }

  @override
  bool shouldRepaint(LinePainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}
複製程式碼

效果:

Flutter Path(二) : Path 進階

二、畫虛線

畫虛線相對畫直線來說就複雜一點了,Flutter 中沒有直接提供畫虛線的方法,但是我們可以藉助 PathMetric 來實現。

pathMetric 是一個對 Path 進行測量並且能夠提取子 Path 的工具。

首先,我們要畫一條直線,和上面畫直線一樣,然後我們通過 path.computeMetrics() 獲取到 PathMetrics 物件。通過對 PathMetric 遍歷,我們可以提取到子 Path,這個子 Path 的起點有當前 distance 指定,而長度是我們自己定義的 dashWidth 。

dashPath.addPath(
  pathMetric.extractPath(distance, distance + dashWidth),
  Offset.zero,
);
複製程式碼

完整程式碼:

class DashLinePainter extends CustomPainter {
  final double progress;

  DashLinePainter({this.progress});

  Paint _paint = Paint()
    ..color = Colors.black
    ..strokeWidth = 4.0
    ..style = PaintingStyle.stroke
    ..strokeJoin = StrokeJoin.round;

  @override
  void paint(Canvas canvas, Size size) {
    var path = Path()
      ..moveTo(0, size.height / 2)
      ..lineTo(size.width * progress, size.height / 2);

    Path dashPath = Path();

    double dashWidth = 10.0;
    double dashSpace = 5.0;
    double distance = 0.0;

    for (PathMetric pathMetric in path.computeMetrics()) {
      while (distance < pathMetric.length) {
        dashPath.addPath(
          pathMetric.extractPath(distance, distance + dashWidth),
          Offset.zero,
        );
        distance += dashWidth;
        distance += dashSpace;
      }
    }
    canvas.drawPath(dashPath, _paint);
  }

  @override
  bool shouldRepaint(DashLinePainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}
複製程式碼

效果:

Flutter Path(二) : Path 進階

三、畫圓

圓形其實本質是一個特殊的橢圓,我們可以通過 addOval 方法繪製一個橢圓,這個方法需要一個 Rect 型別的引數,如果我們想繪製圓形,可以通過 Rect.fromCircle 來實現。

@override
void paint(Canvas canvas, Size size) {
  var path = Path();
  path.addOval(Rect.fromCircle(
    center: Offset(0, 0),
    radius: 80.0,
  ));
  canvas.drawPath(path, myPaint);
}
複製程式碼

上面的程式碼的效果如下:

Flutter Path(二) : Path 進階

畫一個圓形還是很容易的,接下來嘗試畫一個複雜的,如下:

Flutter Path(二) : Path 進階

對上面的繪製圖形簡單分析一下,所以的圓形大小相同,相切於同一個點 (0,0),然後所有圓形的交點可以組成一個圓形,並且相鄰的兩個點之間的弧度相同。

Flutter Path(二) : Path 進階

我們先嚐試分析一下這些交點的關係。

首先假設一共有 n 個圓形,那麼將有 n 給交點,然後假設其中的一個點(也是一個圓的圓心)的座標是 (x,y) 。

Flutter Path(二) : Path 進階

由於一個圓的弧度是 2π,那麼圓的弧度和個數置級的關係如下:

Flutter Path(二) : Path 進階

接下來就是我們高中學到的三角函式了。通過上面的分析,我們可以得到如下的值:

Flutter Path(二) : Path 進階

進而計算得出 x 和 y 的值 :

Flutter Path(二) : Path 進階

因此到這裡,我們有了每個圓形圓心的 x 與 y 的計算方法,然後圓形的半徑 r 是我們自己指定的,這樣我們就知道了繪製圓所需要的全部資訊,用程式碼表示如下:

  @override
  void paint(Canvas canvas, Size size) {
    var path = createPath();
    canvas.drawPath(path, myPaint);
  }

Path createPath() {
    var path = Path();
    int n = circles.toInt();
    var range = List<int>.generate(n, (i) => i + 1);
    double angle = 2 * math.pi / n;
    for (int i in range) {
      double x = radius * math.cos(i * angle);
      double y = radius * math.sin(i * angle);
      path.addOval(Rect.fromCircle(center: Offset(x, y), radius: radius));
    }
    return path;
  }
複製程式碼

由於圓形的個數、半徑、圓心所在位置我們都是知道的,那麼進一步我們還可以進行動態的圓形繪製。

  • 圓形個數動態改變
  • 圓形動態繪製

動畫需要使用 AnimationController

class _CirclesState extends State<Circles> with   SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 3),
  );
  _controller.value = 1.0;
}
複製程式碼

而圓形的動態繪製需要用到 pathMetrics,這個類是一個輔助類可以用來測量和提取子路徑的。

  @override
  void paint(Canvas canvas, Size size) {
    var path = createPath();
    PathMetrics pathMetrics = path.computeMetrics();
    for (PathMetric pathMetric in pathMetrics) {
      Path extractPath = pathMetric.extractPath(
        0.0,
        pathMetric.length * progress,
      );
      canvas.drawPath(extractPath, myPaint);
    }
  }
複製程式碼

詳細的程式碼參考如下:

github.com/divyanshub0…

效果:

Flutter Path(二) : Path 進階

四、多邊形繪製

path 繪製裡面另一個比較重要的部分就是多邊形的繪製,多邊形的每一條邊都是一條直線。

多邊形裡面每個頂點的座標的計算方式類似與上面說到的圓形圓心的計算。

Flutter Path(二) : Path 進階

知道了頂點的座標,繪製每條邊就很容易了。

class PolygonPainter extends CustomPainter {
  PolygonPainter({
    this.sides,
    this.progress,
    this.showPath,
    this.showDots,
  });

  final double sides;
  final double progress;
  bool showDots, showPath;

  final Paint _paint = Paint()
    ..color = Colors.purple
    ..strokeWidth = 4.0
    ..style = PaintingStyle.stroke
    ..strokeCap = StrokeCap.round;

  @override
  void paint(Canvas canvas, Size size) {
    var path = createPath(sides.toInt(), 100);
    PathMetric pathMetric = path.computeMetrics().first;
    Path extractPath =
    pathMetric.extractPath(0.0, pathMetric.length * progress);
    if (showPath) {
      canvas.drawPath(extractPath, _paint);
    }
    if (showDots) {
      try {
        var metric = extractPath.computeMetrics().first;
        final offset = metric.getTangentForOffset(metric.length).position;
        canvas.drawCircle(offset, 8.0, Paint());
      } catch (e) {}
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }

  Path createPath(int sides, double radius) {
    var path = Path();
    var angle = (math.pi * 2) / sides;
    path.moveTo(radius * math.cos(0.0), radius * math.sin(0.0));
    for (int i = 1; i <= sides; i++) {
      double x = radius * math.cos(angle * i);
      double y = radius * math.sin(angle * i);
      path.lineTo(x, y);
    }
    path.close();
    return path;
  }
}
複製程式碼

效果:

Flutter Path(二) : Path 進階

五、螺旋曲線

曲線可以理解為點的移動,畫一個螺旋曲線其實還是有點難度的。

為了達到曲線的效果,我們可以先把中心的移動到 (x,y) 座標,然後,對於下一個點,我們讓半徑增加 0.75,而弧度增加 2π/50. 對於每個新增的點,由於半徑和角度增加的都很小,因此我們在視覺上看到的就是一條曲線了,而不是直線。

Path createSpiralPath(Size size) {
    double radius = 0, angle = 0;
    Path path = Path();
    for (int n = 0; n < 200; n++) {
      radius += 0.75;
      angle += (math.pi * 2) / 50;
      var x = size.width / 2 + radius * math.cos(angle);
      var y = size.height / 2 + radius * math.sin(angle);
      path.lineTo(x, y);
    }
    return path;
  }
複製程式碼

同樣的動畫效果需要使用 pathMetric。 完整程式碼可以在這裡找到 :

github.com/divyanshub0…

效果:

Flutter Path(二) : Path 進階

六、超級進階

最後展示一個行星旋轉的動畫效果,完整程式碼地址:

github.com/divyanshub0…

效果:

Flutter Path(二) : Path 進階


完整的程式碼示例

最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

Flutter Path(二) : Path 進階