前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【flutter高级玩法】贝塞尔实战1 - 波浪

【flutter高级玩法】贝塞尔实战1 - 波浪

作者头像
张风捷特烈
发布2020-04-30 17:49:29
1K0
发布2020-04-30 17:49:29
举报

一切视觉的动效都只是感性的欺骗,如我手中的线,跳动的人偶。她征服着你,我控制着她。--捷特

本文所有代码: 【github:https://github.com/toly1994328/flutter_play_bezier】

  • 【Flutter高级玩法】 贝塞尔曲线的表象认知
  • 【flutter高级玩法】贝塞尔实战1 - 波浪
  • 【Flutter高级玩法】 贝塞尔曲线的本质认知

前言
  • 事项预告: 2020-04-04 晚8:30
  • 【编程技术交流圣地-Flutter群】: 图文直播某个组件源码,共同交流学习。咱们有缘再见。

上一篇中通过一些可操作的案例感性地了解贝塞尔曲线是什么东西。 本篇将介绍贝塞尔曲线的一个简单应用,也是我曾经入门Android绘制的第一个东西 这里想强调一下:贝塞尔曲线甚至说是绘制的本身和平台并没有太大的关联性,可以很方便的移植。重要的不是api本身,而是你能用这些api做出什么。

圆形
圆形
椭圆
椭圆
圆角矩形
圆角矩形

一、静态绘制
1. 绘制单体

最重要的是知道自己想画什么。先看一下曲线怎么画。上一篇说过, 二贝最重要的是两个点控制点终点。如下图,即可得到一个波峰。

为波的宽高各取一个变量,waveWidth,waveHeight,呢么很容易得到这三个点的坐标

代码语言:javascript
复制
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(
    waveWidth/2, -waveHeight*2, 
    waveWidth, 0);

这样就绘制了一个,通过waveWidth,waveHeight控制长度和宽度。


2. 二贝的相对绘制

先对绘制relativeQuadraticBezierTo,是以当前点为参考点进行绘制。 也就再画线是刚才的终点相当于0,0。 复制一份就是一个波。

代码语言:javascript
复制
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);

我们想要的是类似正弦的波,稍微改一笔即可。

代码语言:javascript
复制
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);

再拷贝一份,就又是一个波。值就是相对绘制的好处。

代码语言:javascript
复制
_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,并左移两个波。

代码语言:javascript
复制
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.

代码语言:javascript
复制
    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来不断运行

代码语言:javascript
复制
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即可得到一个不断运动的波。

代码语言:javascript
复制
canvas.save();
canvas.translate(-2*waveWidth+2*waveWidth*factor, 0);
// 英雄所见...
canvas.restore();

2. 画布裁剪

可能现在你还没有看出什么,那我现在将紫色矩形框裁一下

代码语言:javascript
复制
  @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
fastOutSlowIn
easeInQuad
easeInQuad
linear
linear
代码语言:javascript
复制
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
fastOutSlowIn
easeInQuad
easeInQuad
linear
linear
代码语言:javascript
复制
  @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. 圆形剪裁

除了规规整整的矩形,也可以裁成椭圆

圆形
圆形
椭圆
椭圆
圆角矩形
圆角矩形
代码语言:javascript
复制
---->[圆]----
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组件

核心的原理和思想都说完了,就不废话了,下面直接贴源码,想研究的自己研究一下。不想研究的可以直接拿去用。

代码语言:javascript
复制
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()

代码语言:javascript
复制
/// 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;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020年04月04日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、静态绘制
    • 1. 绘制单体波
      • 2. 二贝的相对绘制
        • 3. 实现波动的原理
        • 二. 实现动画
          • 1. 定义动画器
            • 2. 画布裁剪
              • 3. 动画曲线
                • 4. 二重波
                  • 5. 圆形剪裁
                  • 三、FlutterWaveLoading组件
                  相关产品与服务
                  云直播
                  云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档