主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: juejin

highlight:

本文作为对掘金小册 《Flutter 绘制指南 - 妙笔生花》 的一个知识补充点,后面会更新到小册中。在此也希望记录和分享一下 Flutter 中如何通过贝塞尔曲线使折线形成曲线。源码在这

1. 问题描述

现在有一批如下的点,很容易通过 canvas.drawPoints 绘制出如下的折线。

image-20201210091839474

dart ---->[ 点集 ]---- List<Offset> points1 = [ Offset(0, 20), Offset(40, 40) , Offset(80, -20), Offset(120, -40), Offset(160, -80), Offset(200, -20), Offset(240, -40), ];

但很多时候,我们希望用一个曲线 来展示数据,而非生硬的折线。

image-20201210092206460

所以本文就来探讨一下 如何使用贝塞尔曲线对点集进行拟合

image-20201210092727772


2. 绘制点与折线

程序入口文件 main.dart , 此处横屏全屏显示。

```dart ---->[p14bezier/s05bezier_line/main.dart]---- import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'paper.dart';

void main() { // 确定初始化 WidgetsFlutterBinding.ensureInitialized(); //横屏 SystemChrome.setPreferredOrientations( [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); //全屏显示 SystemChrome.setEnabledSystemUIOverlays([]);

runApp(Paper()); } ```

显示组件 Paper ,使用 PaperPainter 画板。

dart ---->[p14_bezier/s05_bezier_line/paper.dart]---- class Paper extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.white, child: CustomPaint( painter: PaperPainter() ), ); } }

通过简单的几步,如下的折线图便跃然纸上。其中 Coordinate 是我写的一个坐标系绘制辅助类,来方便查看点的位置,从而帮助理解。详见源码,不想用的话也不影响,删掉即可。

image-20201210091839474

```dart ---->[p14bezier/s05bezier_line/paper.dart]---- class PaperPainter extends CustomPainter { final Coordinate coordinate = Coordinate(); List points1 = [ Offset(0, 20), Offset(40, 40) , Offset(80, -20), Offset(120, -40), Offset(160, -80), Offset(200, -20), Offset(240, -40), ];

Paint _helpPaint = Paint(); Paint _mainPaint = Paint(); Path _linePath = Path();

@override void paint(Canvas canvas, Size size) { coordinate.paint(canvas, size); // 画布原点 移到 屏幕中心 canvas.translate(size.width / 2, size.height / 2); // 绘制辅助点线 _drawHelp(canvas); }

void _drawHelp(Canvas canvas) { _helpPaint..style = PaintingStyle.stroke; // 绘制点 points1.forEach((element) { canvas.drawCircle(element, 2, _helpPaint..strokeWidth=1..color=Colors.orange); }); // 绘制折线 canvas.drawPoints(PointMode.polygon, points1, _helpPaint..strokeWidth=0.5..color=Colors.red); }

@override bool shouldRepaint(CustomPainter oldDelegate) => false; } ```


3. 贝塞尔曲线拟合

在下面方法中,传入一个 List<Offset> 类型的点集 points 。其中首尾两段线使用二阶贝塞尔曲线,中间的使用三阶贝塞尔曲线。起止点和控制点通过 current 当前点和 next 下一点来控制。

image-20201210092727772

dart void addBezierPathWithPoints(Path path, List<Offset> points) { for (int i = 0; i < points.length - 1; i++) { Offset current = points[i]; Offset next = points[i+1]; if (i == 0) { path.moveTo(current.dx, current.dy); // 控制点 double ctrlX = current.dx + (next.dx - current.dx) / 2; double ctrlY = next.dy; path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy); } else if (i < points.length - 2) { // 控制点 1 double ctrl1X = current.dx + (next.dx - current.dx) / 2; double ctrl1Y = current.dy; // 控制点 2 double ctrl2X = ctrl1X; double ctrl2Y = next.dy; path.cubicTo(ctrl1X,ctrl1Y,ctrl2X,ctrl2Y,next.dx,next.dy); }else{ path.moveTo(current.dx, current.dy); // 控制点 double ctrlX = current.dx + (next.dx - current.dx) / 2; double ctrlY = current.dy; path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy); } } }

首先来看第一段曲线 (0, 20) 是起点 current(40, 40) 是下一点 next,对于二阶贝塞尔曲线来说,只要确定控制点就完事了。这里 控制点 x 取两点的中点横坐标,y 取 next 的纵坐标,即下面的 (10,40) 点。

image-20201210095541558

dart if (i == 0) { path.moveTo(current.dx, current.dy); // 控制点 double ctrlX = current.dx + (next.dx - current.dx) / 2; double ctrlY = next.dy; path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy); }

再看最后一段曲线 ,和第一段类似,三点的位置如下,注意这里使用的是相对于倒数第二个点的添加 relativeQuadraticBezierTo,来保证曲线的连贯性

image-20201210100219171

dart // 控制点 double ctrlX = (next.dx - current.dx) / 2; double ctrlY = 0; path.relativeQuadraticBezierTo(ctrlX, ctrlY, next.dx-current.dx, next.dy-current.dy);

第二段曲线使用 三阶贝塞尔,控制点如下所示。

image-20201210100510624

dart // 控制点 1 double ctrl1X = current.dx + (next.dx - current.dx) / 2; double ctrl1Y = current.dy; // 控制点 2 double ctrl2X = ctrl1X; double ctrl2Y = next.dy; path.cubicTo(ctrl1X,ctrl1Y,ctrl2X,ctrl2Y,next.dx,next.dy);

同样后面的几条线段都是类似,控制点如下,这样就生成了连续的曲线。这里通过 addBezierPathWithPoints 方法就可以实现将一个点集编程一个曲线路径添加到指定 Path 中。

image-20201210100715497

这样使用多个点集也就会形成多个曲线。

image-20201210101239232


4. 在统计图中使用

这样在后面 16 章实现的折线统计图就可以使用曲线来替换折线,代码见 p16_chart.s03_line_plus

曲线

本篇到此结束,不止是 Flutter 中的贝塞尔曲线,其他平台、框架中的贝塞尔曲线也是类似的,所以这个知识点虽然比较很小,但很重要。很好地理解它,能提升你对贝塞尔曲线的认识,一把利器握在手里,你是要驾驭它,而不是畏惧它。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐