一切视觉的动效都只是感性的欺骗,如我手中的线,跳动的人偶。她征服着你,我控制着她。--捷特
本文所有代码: 【github:https://github.com/toly1994328/flutter_play_bezier】
- 【Flutter高级玩法】 贝塞尔曲线的表象认知
- 【flutter高级玩法】贝塞尔实战1 - 波浪
- 【Flutter高级玩法】 贝塞尔曲线的本质认知
前言
事项预告: 2020-04-04 晚8:30
【编程技术交流圣地-Flutter群】: 图文直播某个组件源码,共同交流学习。咱们有缘再见。
上一篇中通过一些可操作的案例感性地了解贝塞尔曲线是什么东西。
本篇将介绍贝塞尔曲线的一个简单应用,也是我曾经入门Android绘制的第一个东西
这里想强调一下:贝塞尔曲线甚至说是绘制的本身和平台并没有太大的关联性,可以很方便的移植。重要的不是api本身,而是你能用这些api做出什么。
圆形 | 椭圆 | 圆角矩形 |
---|---|---|
一、静态绘制
1. 绘制单体波
最重要的是知道自己想画什么。先看一下曲线怎么画。上一篇说过,
二贝最重要的是两个点控制点
和终点
。如下图,即可得到一个波峰。
为波的宽高各取一个变量,
waveWidth
,waveHeight
,呢么很容易得到这三个点的坐标
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth/2, -waveHeight*2,
waveWidth, 0);
复制代码
这样就绘制了一个
波
,通过waveWidth
,waveHeight
控制长度和宽度。
2. 二贝的相对绘制
先对绘制
relativeQuadraticBezierTo
,是以当前点为参考点进行绘制。
也就再画线是刚才的终点相当于0,0。 复制一份就是一个波。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
复制代码
我们想要的是类似正弦的波,稍微改一笔即可。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
复制代码
再拷贝一份,就又是一个波。值就是相对绘制的好处。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
复制代码
3. 实现波动的原理
接下来是很关键的一步,为了好看,我画了一个辅助的紫色box,并左移两个波。
canvas.save();
canvas.translate(-2*waveWidth, 0);
_mainPath.moveTo(-2*waveWidth, 0);
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.close();
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill);
canvas.restore();
复制代码
然后画出底部区域,我将下面的波高改为了20.
canvas.save();
canvas.translate(-2*waveWidth, 0);
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth*2 * 2.0, 0);
_mainPath.close();
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill);
canvas.restore();
复制代码
这样静态的绘制就已经over了。接下来的事情就非常简单了,让波不断的移动即可。
二. 实现动画
1. 定义动画器
AnimationController可以让数字在0~1间不断变化。在变化时对界面进行刷新
画布中接受一个factor
的移动因子,在点击时执行AnimationController#repeat
来不断运行
class TolyWave extends StatefulWidget {
@override
_TolyWaveState createState() => _TolyWaveState();
}
class _TolyWaveState extends State<TolyWave> with SingleTickerProviderStateMixin{
AnimationController _controller;
@override
void initState() {
//横屏
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
//全屏显示
SystemChrome.setEnabledSystemUIOverlays([]);
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500))
..addListener((){
setState(() {
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail) => _controller.repeat(),
child: CustomPaint(
painter: BezierPainter(factor: _controller.value),
),
);
}
}
复制代码
然后在画布中移动
2*waveWidth*factor
即可得到一个不断运动的波。
canvas.save();
canvas.translate(-2*waveWidth+2*waveWidth*factor, 0);
// 英雄所见...
canvas.restore();
复制代码
2. 画布裁剪
可能现在你还没有看出什么,那我现在将紫色矩形框裁一下
@override
void paint(Canvas canvas, Size size) {
canvas.clipRect((Rect.fromCenter(
center: Offset( waveWidth, 0),width: waveWidth*2,height: 200.0)));
canvas.save();
// 英雄所见...
复制代码
快速 | 慢速 | 宽度 |
---|---|---|
这样一来,基本的逻辑算是整清了
3. 动画曲线
既然用了动画,怎么能少的了曲线。
fastOutSlowIn | easeInQuad | linear |
---|---|---|
class _TolyWaveState extends State<TolyWave> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _anim;
@override
void initState() {
//英雄所见...
_anim = CurveTween(curve: Curves.linear).animate(_controller);
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail) => _controller.repeat(reverse: false),
child: CustomPaint(
painter: BezierPainter(factor: _anim.value),
),
);
}
}
复制代码
4. 二重波
原理也很简单,在原来的基础上,再画一个移动速度翻倍的波,将原来的透明度变浅即可 由于速度变成两倍,移动距离边长,所以波形需要三份。
fastOutSlowIn | easeInQuad | linear |
---|---|---|
@override
void paint(Canvas canvas, Size size) {
center = center.translate(-size.width / 2, 0);
canvas.drawColor(Colors.white, BlendMode.color);
canvas.translate(size.width / 2, size.height / 2);
canvas.clipPath(Path()..addRect(Rect.fromCenter(center: Offset( waveWidth, 0),width: waveWidth*2,height: 200.0)));
// _drawGrid(canvas, size); //绘制格线
// _drawAxis(canvas, size); //绘制轴线
canvas.save();
canvas.save();
canvas.translate(-4*waveWidth+2*waveWidth*factor, 0);
drawWave(canvas);
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill..color=Colors.red.withAlpha(88));
canvas.restore();
canvas.translate(-4*waveWidth+2*waveWidth*factor*2, 0);
drawWave(canvas);
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill..color=Colors.red);
canvas.restore();
}
void drawWave(Canvas canvas) {
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth*3 * 2.0, 0);
_mainPath.close();
}
复制代码
下面就来揭密为什么动画只是视觉的骗术。
你所见的永动,只是局部范围的重复。
把剪裁的区域去掉,也就是下面这丑陋的东西。
5. 圆形剪裁
除了规规整整的矩形,也可以裁成椭圆
圆形 | 椭圆 | 圆角矩形 |
---|---|---|
---->[圆]----
canvas.clipPath(Path()
..addOval(Rect.fromCenter(
center: Offset( waveWidth, 0),width: waveWidth*2,height: waveWidth*2)));
---->[椭圆]----
canvas.clipPath(Path()
..addOval(Rect.fromCenter(
center: Offset( waveWidth, 0),
width: waveWidth*2,height: 200.0)));
---->[圆角矩形]----
canvas.clipPath(Path()
..addRRect(RRect.fromRectXY(Rect.fromCenter(
center: Offset( waveWidth, 0),
width: waveWidth*2,height: 200.0), 30 , 30)));
复制代码
到此为止铺垫结束,大家可以下载用
toly_wave.dart
文件自己玩玩
三、FlutterWaveLoading
组件
核心的原理和思想都说完了,就不废话了,下面直接贴源码,想研究的自己研究一下。不想研究的可以直接拿去用。
List.generate(9, (v) => 0.1 * v+0.1)
.map((e) => FlutterWaveLoading(
width: 75, //宽
height: 75,//高
isOval: true, // 是否椭圆裁切
progress: e, // 进度
waveHeight: 3, //波浪高
color: Colors.blue, //颜色
))
.toList()
复制代码
/// create by 张风捷特烈 on 2020-04-04
/// contact me by email 1981462002@qq.com
/// 说明: 贝塞尔曲线测试画布
///
class FlutterWaveLoading extends StatefulWidget {
final double width;
final double height;
final double waveHeight;
final Color color;
final double strokeWidth;
final double progress;
final double factor;
final int secondAlpha;
final double borderRadius;
final bool isOval;
FlutterWaveLoading(
{
this.width = 100,
this.height = 100/0.618,
this.factor = 1,
this.waveHeight = 5,
this.progress = 0.5,
this.color = Colors.green,
this.strokeWidth = 3,
this.secondAlpha = 88,
this.isOval = false,
this.borderRadius = 20});
@override
_FlutterWaveLoadingState createState() => _FlutterWaveLoadingState();
}
class _FlutterWaveLoadingState extends State<FlutterWaveLoading>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _anim;
@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1200))
..addListener(() {
setState(() {});
})
..repeat();
_anim = CurveTween(curve: Curves.linear).animate(_controller);
super.initState();
}
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(
width: widget.width,
height: widget.height,
child: CustomPaint(
painter: BezierPainter(
factor: _anim.value,
waveHeight: widget.waveHeight,
progress: widget.progress,
color: widget.color,
strokeWidth: widget.strokeWidth,
secondAlpha: widget.secondAlpha,
isOval: widget.isOval,
borderRadius: widget.borderRadius),
),
),
);
}
}
class BezierPainter extends CustomPainter {
Paint _mainPaint;
Path _mainPath;
double waveWidth = 80;
double wrapHeight;
final double waveHeight;
final Color color;
final double strokeWidth;
final double progress;
final double factor;
final int secondAlpha;
final double borderRadius;
final bool isOval;
BezierPainter(
{this.factor = 1,
this.waveHeight = 8,
this.progress = 0.5,
this.color = Colors.green,
this.strokeWidth = 3,
this.secondAlpha = 88,
this.isOval = false,
this.borderRadius = 20}) {
_mainPaint = Paint()
..color = Colors.yellow
..style = PaintingStyle.stroke
..strokeWidth = 2;
_mainPath = Path();
}
@override
void paint(Canvas canvas, Size size) {
print(size);
waveWidth = size.width / 2;
wrapHeight = size.height;
Path path = Path();
if (!isOval) {
path.addRRect(
RRect.fromRectXY(Offset(0, 0) & size, borderRadius, borderRadius));
canvas.clipPath(path);
canvas.drawPath(
path,
_mainPaint
..strokeWidth = strokeWidth
..color = color);
}
if (isOval) {
path.addOval(Offset(0, 0) & size);
canvas.clipPath(path);
canvas.drawPath(
path,
_mainPaint
..strokeWidth = strokeWidth
..color = color);
}
canvas.translate(0, wrapHeight);
canvas.save();
canvas.translate(0, waveHeight);
canvas.save();
canvas.translate(-4 * waveWidth + 2 * waveWidth * factor, 0);
drawWave(canvas);
canvas.drawPath(
_mainPath,
_mainPaint
..style = PaintingStyle.fill
..color = color.withAlpha(88));
canvas.restore();
canvas.translate(-4 * waveWidth + 2 * waveWidth * factor * 2, 0);
drawWave(canvas);
canvas.drawPath(
_mainPath,
_mainPaint
..style = PaintingStyle.fill
..color = color);
canvas.restore();
}
void drawWave(Canvas canvas) {
_mainPath.moveTo(0, 0);
_mainPath.relativeLineTo(0, -wrapHeight * progress);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth * 3 * 2.0, 0);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
复制代码
尾声
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,期待与你的交流与切磋。
@张风捷特烈 2019.04.04 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com --微信:zdl1994328
~ END ~