在 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 中没有直接提供画虚线的方法,但是我们可以借助 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;
}
}
效果:

三、画圆
圆形其实本质是一个特殊的椭圆,我们可以通过 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);
}
上面的代码的效果如下:

画一个圆形还是很容易的,接下来尝试画一个复杂的,如下:

对上面的绘制图形简单分析一下,所以的圆形大小相同,相切于同一个点 (0,0),然后所有圆形的交点可以组成一个圆形,并且相邻的两个点之间的弧度相同。

我们先尝试分析一下这些交点的关系。
首先假设一共有 n 个圆形,那么将有 n 给交点,然后假设其中的一个点(也是一个圆的圆心)的坐标是 (x,y) 。

由于一个圆的弧度是 2π,那么圆的弧度和个数置级的关系如下:

接下来就是我们高中学到的三角函数了。通过上面的分析,我们可以得到如下的值:

进而计算得出 x 和 y 的值 :

因此到这里,我们有了每个圆形圆心的 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);
}
}
详细的代码参考如下:
效果:

四、多边形绘制
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;
}
}
效果:

五、螺旋曲线
曲线可以理解为点的移动,画一个螺旋曲线其实还是有点难度的。
为了达到曲线的效果,我们可以先把中心的移动到 (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。 完整代码可以在这里找到 :
效果:

六、超级进阶
最后展示一个行星旋转的动画效果,完整代码地址:
效果:

最后
欢迎关注「Flutter 编程开发」微信公众号 。
