本文全部代码: 【github:https://github.com/toly1994328/flutter_play_bezier】git
先看看本文要干吗:
github
-- | -- |
---|---|
![]() |
![]() |
![]() |
![]() |
在玩贝塞尔以前先作点准备活动热热身。打个网格对学习贝塞尔曲线是颇有帮助的。以下是以中心为原点的坐标系,
x向右
,y向下
编程
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home:Paper());
}
}
复制代码
为了绘制的纯粹和雅观,这里把状态量去掉,而且手机横向。canvas
/// create by 张风捷特烈 on 2020-03-27
/// contact me by email 1981462002@qq.com
/// 说明: 纸
class Paper extends StatefulWidget {
@override
_PaperState createState() => _PaperState();
}
class _PaperState extends State<Paper> {
@override
void initState() {
//横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
//全屏显示
SystemChrome.setEnabledSystemUIOverlays([]);
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: BezierPainter(),
);
}
}
复制代码
注意: 这里永久的将画布原点移到画布的中心点,以后因此的绘制都将以中心为(0,0)点。 bash
/// create by 张风捷特烈 on 2020-03-27
/// contact me by email 1981462002@qq.com
/// 说明: 贝塞尔曲线测试画布
class BezierPainter extends CustomPainter {
Paint _gridPaint;
Path _gridPath;
BezierPainter() {
_gridPaint = Paint()..style=PaintingStyle.stroke;
_gridPath = Path();
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawColor(Colors.white, BlendMode.color);
canvas.translate(size.width/2, size.height/2);
_drawGrid(canvas,size);//绘制格线
_drawAxis(canvas, size);//绘制轴线
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
void _drawGrid(Canvas canvas, Size size) {
_gridPaint
..color = Colors.grey
..strokeWidth = 0.5;
_gridPath = _buildGridPath(_gridPath, size);
canvas.drawPath(_buildGridPath(_gridPath, size), _gridPaint);
canvas.save();
canvas.scale(1, -1); //沿x轴镜像
canvas.drawPath(_gridPath, _gridPaint);
canvas.restore();
canvas.save();
canvas.scale(-1, 1); //沿y轴镜像
canvas.drawPath(_gridPath, _gridPaint);
canvas.restore();
canvas.save();
canvas.scale(-1, -1); //沿原点镜像
canvas.drawPath(_gridPath, _gridPaint);
canvas.restore();
}
void _drawAxis(Canvas canvas, Size size) {
canvas.drawPoints(PointMode.lines, [
Offset(-size.width/2, 0) , Offset(size.width/2, 0),
Offset( 0,-size.height/2) , Offset( 0,size.height/2),
Offset( 0,size.height/2) , Offset( 0-7.0,size.height/2-10),
Offset( 0,size.height/2) , Offset( 0+7.0,size.height/2-10),
Offset(size.width/2, 0) , Offset(size.width/2-10, 7),
Offset(size.width/2, 0) , Offset(size.width/2-10, -7),
], _gridPaint..color=Colors.blue..strokeWidth=1.5);
}
Path _buildGridPath(Path path, Size size,{step = 20.0}) {
for (int i = 0; i < size.height / 2 / step; i++) {
path.moveTo(0, step * i);
path.relativeLineTo(size.width / 2, 0);
}
for (int i = 0; i < size.width / 2 / step; i++) {
path.moveTo( step * i,0);
path.relativeLineTo(0,size.height / 2, );
}
return path;
}
}
复制代码
先不看哪些花里胡哨的贝塞尔曲线的动画。让咱们从实践中一点点去摸索。如此美丽的初见,为什么要这么复杂?当你渐渐去认识她,了解她,熟悉她,便会明白:
哦,原来如此如此,这般这般...
微信
贝塞尔
三个字,也不用以为压力太大,满打满算也就两个函数而已。---->[二次贝塞尔曲线]----
void quadraticBezierTo(double x1, double y1, double x2, double y2)
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2)
---->[三次贝塞尔曲线]----
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
复制代码
二次贝塞尔曲线须要传入四个
double
类型的值。markdown
首先新准备个画笔和路径,在构造函数里初始化。准备两个测试点
p1,p2
,
而后轻轻的用quadraticBezierTo
描一笔,就出来一个曲线。less
class BezierPainter extends CustomPainter {
// 英雄所见...
Paint _mainPaint;
Path _mainPath;
BezierPainter() {
// 英雄所见...
_mainPaint = Paint()..color=Colors.orange..style=PaintingStyle.stroke..strokeWidth=2;
_mainPath = Path();
}
Offset p0 =Offset(0, 0);
Offset p1 =Offset(100, 100);
Offset p2 =Offset( 120, -60);
@override
void paint(Canvas canvas, Size size) {
// 英雄所见...
_mainPath.moveTo(p0.dx, p0.dy);
_mainPath.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
canvas.drawPath(_mainPath, _mainPaint);
}
复制代码
为了更好的理解贝塞尔曲线,如今咱们须要绘制辅助帮咱们理解。如今想将与贝塞尔曲线有关系的三个点画出来。一样,我不想弄脏画笔,因此新拿一个
_helpPaint
。在_drawHelp
方法里进行绘制辅助线。ide
class BezierPainter extends CustomPainter {
// 英雄所见...
Paint _helpPaint;
BezierPainter() {
// 英雄所见...
_helpPaint = Paint()
..color=Colors.purple
..style=PaintingStyle.stroke
..strokeCap=StrokeCap.round;
}
void _drawHelp(Canvas canvas) {
canvas.drawPoints(PointMode.points,[p0, p1, p1,p2], _helpPaint..strokeWidth=8);
}
复制代码
void _drawHelp(Canvas canvas) {
canvas.drawPoints(PointMode.lines,[p0, p1, p1,p2], _helpPaint..strokeWidth=1);
canvas.drawPoints(PointMode.points,[p0, p1, p1,p2], _helpPaint..strokeWidth=8);
}
复制代码
这不就是三个点嘛,要能拖拖看就行了。没问题,应你所求函数
如今有两个要点: 【1】 如何获取触点 【2】如何经过一个触点控制三个点位
简单讲解
因为点位须要变化,BezierPainter只承担绘制的责任
,这里在组件中定义点位信息_pos
和选中索引_selectIndex
,经过构造函数传入BezierPainter。为了方便你们玩耍,我单独写个文件play_bezier2.dart
里面有个PlayBezier2Page组件。
---->[_PaperState]----
class PlayBezier2Page extends StatefulWidget {
@override
_PlayBezier2PageState createState() => _PlayBezier2PageState();
}
class _PlayBezier2PageState extends State<PlayBezier2Page> {
List<Offset> _pos = <Offset>[];
int _selectPos;
@override
void initState() {
//横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
//全屏显示
SystemChrome.setEnabledSystemUIOverlays([]);
_initPoints();//初始化点
super.initState();
}
复制代码
获取触点信息
GestureDetector组件
能够获取触点信息,而后传给画布便可。@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail){
// Todo
},
onPanEnd: (detail){
// Todo
},
onPanUpdate: (detail) {
// Todo
},
child: CustomPaint(
painter: BezierPainter(pos: _pos,selectPos:selectPos),
),
);
}
复制代码
一个触点控制三个点位
这就有点技术含量了。须要进行
点域的判断
来肯定当前点击的是哪一个点。
好比在半径为6的区域内算做命中
,就须要在点击时判断是否命中某个点。具体逻辑为:
///判断出是否在某点的半径为r圆范围内
bool judgeCircleArea(Offset src, Offset dst, double r) =>
(src - dst).distance <= r;
复制代码
void judgeSelect(Offset src, {double x = 0, double y = 0}) {
var p = src.translate(-x, -y);
for (int i = 0; i < _pos.length; i++) {
if (judgeCircleArea(p, _pos[i], 15)) {
selectPos = i;
}
}
}
void judgeZone(Offset src, {double x = 0, double y = 0}) {
for (int i = 0; i < _pos.length; i++) {
if (judgeCircleArea(src, _pos[i], 15)) {
selectPos = i;
_pos[i] = src;
}
}
}
复制代码
前三个点须要用户点击,而后画出一段二贝曲线,以后再点击不会添加点,而是判断是否触点在指望的圆域内。这样数据的处理就完成了。根基【捷特第二定理】
一切的界面交互和动态视觉效果都是连续时间点状态量的变化和刷新的结合。
如今全部的状态量和刷新都已经实现,剩下的就是将这些量显示在界面上。
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail) {
if (_pos.length < 3) {
_pos.add(detail.localPosition);
}
setState(() => judgeSelect(detail.localPosition));
},
onPanEnd: (detail) {
setState(() => selectPos = null);
},
onPanUpdate: (detail) {
setState(() => judgeZone(detail.localPosition));
},
child: CustomPaint(
painter: BezierPainter(pos: _pos, selectPos: selectPos),
),
);
}
复制代码
绘制
网格和辅助的和上面逻辑基本一致,详见源码,这里就不贴了。当点数小于三个时,仅绘制触点,不然绘制曲线和辅助线。
有一点须要注意: 咱们的点位是相对于屏幕左上角的,须要平移到画布中心
class BezierPainter extends CustomPainter {
Paint _mainPaint;
Path _mainPath;
int selectPos;
List<Offset> pos;
BezierPainter({this.pos, this.selectPos}) {
_mainPaint = Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 2;
_mainPath = Path();
}
@override
void paint(Canvas canvas, Size size) {
pos = pos.map((e)=>e.translate(-size.width / 2, -size.height / 2)).toList();
canvas.drawColor(Colors.white, BlendMode.color);
canvas.translate(size.width / 2, size.height / 2);
_drawGrid(canvas, size); //绘制格线
_drawAxis(canvas, size); //绘制轴线
if(pos.length<3){
canvas.drawPoints(PointMode.points, pos, _helpPaint..strokeWidth = 8);
}else{
_mainPath.moveTo(pos[0].dx, pos[0].dy);
_mainPath.quadraticBezierTo(pos[1].dx, pos[1].dy, pos[2].dx, pos[2].dy);
canvas.drawPath(_mainPath, _mainPaint);
_drawHelp(canvas);
_drawSelectPos(canvas);
}
}
// 英雄所见...
void _drawSelectPos(Canvas canvas) {
if (selectPos == null) return;
canvas.drawCircle(
pos[selectPos],
10,
_helpPaint
..color = Colors.green
..strokeWidth = 2);
}
}
复制代码
经过前面的介绍,一段二次的贝塞尔曲线有三个点决定,
起点
、控制点
、终点
关于起点,默认是(0,0),你也在绘制以前moveTo设置起点,当绘制连续的贝塞尔曲线,下一段曲线的起点就是上一段的终点。因此二次贝塞尔曲线相当重要的是两个点:也就是入参中的控制点和终点
。
前面的二次贝塞尔实现了,那如今来看三次的
cubicTo
。须要六个参数,也就是三个点。
咱们可使用以前的代码,很快捷的生成以下效果。源代码在play_bezier3.dart
前面点集在
_pos
中维护,如今须要四个点,so easy
点击时将限制数改成4个
---->[_PlayBezier3PageState]----
onPanDown: (detail) {
if (_pos.length < 4) {
_pos.add(detail.localPosition);
}
setState(() => judgeSelect(detail.localPosition));
},
复制代码
绘制将限制数改成4个
if(pos.length<4){
canvas.drawPoints(PointMode.points, pos, _helpPaint..strokeWidth = 8);
}else{
_mainPath.moveTo(pos[0].dx, pos[0].dy);
_mainPath.cubicTo(pos[1].dx, pos[1].dy, pos[2].dx, pos[2].dy, pos[3].dx, pos[3].dy);
canvas.drawPath(_mainPath, _mainPaint);
_drawHelp(canvas);
_drawSelectPos(canvas);
}
复制代码
That is all
,这就是分工明确的好处,变化时只变需变化待变化的,总体的流程和思路是恒定的。
三贝很厉害,能够说无所不能。只有你想不到,没有她作不到
Ps中的钢笔路径就是多段的三贝曲线。因此仍是颇有玩头的。
--
绘制拟圆
下面的图看着像个圆,但实际上是四段三贝拟合而成的。目前咱们的代码中最在乎的就是点位数据。因此关键就是寻找点。本小节源码在:
circle_bezier.dart
中
第一段-左下
这里直接给出点,至于
0.551915024494
是什么,后面有机会会带你一块儿推导。有兴趣的话,你也能够本身查一查资料。和以前同样,核心的绘制就是那么一句。
---->[CircleBezierPage]----
class CircleBezierPage extends StatefulWidget {
@override
_CircleBezierPageState createState() => _CircleBezierPageState();
}
class _CircleBezierPageState extends State<CircleBezierPage> {
List<Offset> _pos = <Offset>[];
int selectPos;
//单位圆(即半径为1)控制线长
final rate = 0.551915024494;
double _radius=150;
@override
void initState() {
//横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
//全屏显示
SystemChrome.setEnabledSystemUIOverlays([]);
_initPoints();
super.initState();
}
void _initPoints() {
_pos = List<Offset>();
//第一段线
_pos.add(Offset(0,rate)*_radius);
_pos.add(Offset(1 - rate, 1)*_radius);
_pos.add(Offset(1, 1)*_radius);
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: BezierPainter(pos: _pos, selectPos: selectPos),
),
);
}
---->[BezierPainter#paint]----
_mainPath.moveTo(0, 0);
for (int i = 0; i < pos.length / 3; i++) {
_mainPath.cubicTo(
pos[3*i+0].dx, pos[3*i+0].dy,
pos[3*i+1].dx, pos[3*i+1].dy,
pos[3*i+2].dx, pos[3*i+2].dy);
}
复制代码
其余三段
初始点时,将这12点放入列表。而后将赋值的点线绘制出来。
---->[CircleBezierPage#_initPoints]----
void _initPoints() {
_pos = List<Offset>();
//第一段线
_pos.add(Offset(0,rate)*_radius);
_pos.add(Offset(1 - rate, 1)*_radius);
_pos.add(Offset(1, 1)*_radius);
//第二段线
_pos.add(Offset(1 + rate, 1)*_radius);
_pos.add(Offset(2, rate)*_radius);
_pos.add(Offset(2, 0)*_radius);
//第三段线
_pos.add(Offset(2, -rate)*_radius);
_pos.add(Offset(1 + rate, -1)*_radius);
_pos.add(Offset(1, -1)*_radius);
//第四段线
_pos.add(Offset(1 - rate, -1)*_radius);
_pos.add(Offset(0, -rate)*_radius);
_pos.add(Offset(0, 0));
}
---->[BezierPainter#_drawHelp]----
void _drawHelp(Canvas canvas) {
_helpPaint..strokeWidth = 1;
canvas.drawLine(pos[0], pos[11],_helpPaint);
canvas.drawLine(pos[1], pos[2],_helpPaint);
canvas.drawLine(pos[2], pos[3],_helpPaint);
canvas.drawLine(pos[4], pos[5],_helpPaint);
canvas.drawLine(pos[5], pos[6],_helpPaint);
canvas.drawLine(pos[7], pos[8],_helpPaint);
canvas.drawLine(pos[8], pos[9],_helpPaint);
canvas.drawLine(pos[10], pos[11],_helpPaint);
canvas.drawLine(pos[11], pos[0],_helpPaint);
canvas.drawPoints(PointMode.points, pos, _helpPaint..strokeWidth = 8);
}
复制代码
看这控制柄,满满的拖动欲望,来实现一下吧
有了以前的铺垫,下面的代码应该很容易接受吧。
@override
Widget build(BuildContext context) {
var x = MediaQuery.of(context).size.width/2;
var y = MediaQuery.of(context).size.height/2;
return GestureDetector(
onPanDown: (detail) {
setState(() => judgeSelect(detail.localPosition,x: x,y: y));
},
onPanEnd: (detail) {
setState(() => selectPos = null);
},
onPanUpdate: (detail) {
setState(() => judgeZone(detail.localPosition,x: x,y: y));
},
child: CustomPaint(
painter: BezierPainter(pos: _pos, selectPos: selectPos),
),
);
}
///判断出是否在某点的半径为r圆范围内
bool judgeCircleArea(Offset src, Offset dst, double r) =>
(src - dst).distance <= r;
void judgeSelect(Offset src, {double x = 0, double y = 0}) {
print(src);
var p = src.translate(-x, -y);
print(p);
for (int i = 0; i < _pos.length; i++) {
if (judgeCircleArea(p, _pos[i], 15)) {
selectPos = i;
}
}
}
void judgeZone(Offset src, {double x = 0, double y = 0}) {
var p = src.translate(-x, -y);
for (int i = 0; i < _pos.length; i++) {
if (judgeCircleArea(p, _pos[i], 15)) {
selectPos = i;
_pos[i] = p;
}
}
}
复制代码
也许你以为贝塞尔曲线也就那样。那么你忽略了一个很重要的东西。
贝塞尔曲线
是一条路径。路径是个什么东西,以前写了一篇关于路径使用的冰山一角
【Flutter高级玩法-shape】Path在手,天下我有
如今再准备一条路径,看看路径间的如何操做
class BezierPainter extends CustomPainter {
Path _clipPath;
//英雄所见...
BezierPainter({this.pos, this.selectPos}) {
_clipPath=Path();
//英雄所见...
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
_clipPath.addOval(Rect.fromCenter(center: Offset(0, 0),width: 100,height: 100));
canvas.drawPath(_clipPath, _mainPaint);
//英雄所见...
}
复制代码
PathOperation.difference
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
var drawPath = Path.combine(PathOperation.difference, _mainPath, _clipPath);
canvas.drawPath(drawPath, _mainPaint);
复制代码
PathOperation.union
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
var drawPath = Path.combine(PathOperation.union, _mainPath, _clipPath);
canvas.drawPath(drawPath, _mainPaint);
复制代码
PathOperation.reverseDifference
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
var drawPath = Path.combine(PathOperation.reverseDifference, _mainPath, _clipPath);
canvas.drawPath(drawPath, _mainPaint);
复制代码
PathOperation.intersect
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
var drawPath = Path.combine(PathOperation.intersect, _mainPath, _clipPath);
canvas.drawPath(drawPath, _mainPaint);
复制代码
PathOperation.xor
固然路径并不是是线条,也能够进行填色。
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
var drawPath = Path.combine(PathOperation.xor, _mainPath, _clipPath);
canvas.drawPath(drawPath, _mainPaint..style=PaintingStyle.fill);
复制代码
OK,本篇到这里就告一段落,下一篇会找几个实际的用途,来看看贝塞尔曲线的妙用。 敬请期待。
最后,祝我生日快乐。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,期待与你的交流与切磋。
@张风捷特烈 2019.03.28 未允禁转
个人公众号:编程之王
联系我--邮箱:1981462002@qq.com --微信:zdl1994328
~ END ~