Flutter 83: 图解自定义 ACEWave 波浪 Widget (一)-阿里云开发者社区

开发者社区> 阿策~> 正文

Flutter 83: 图解自定义 ACEWave 波浪 Widget (一)

简介: 0 基础学习 Flutter,第八十三步:尝试绘制波浪效果 (一)!
+关注继续查看

      小菜今天尝试一下绘制波浪的效果,虽然 pub 仓库中已经有成熟的插件,但小菜还是准备用之前学习的 CanvasAnimation 尝试自定义一个 ACEWave

1. 绘制曲线

      绘制波浪首先需要绘制曲线,采用 Canvas 绘制贝塞尔曲线;常用的是数学中通常用的 sin(x) / cos(y) 函数即可;

      其中小菜通过 Canvas 绘制时使用了 path.quadraticBezierTo 来绘制从第一个 Point 到另一个 Point 的贝塞尔曲线;

class _ACEWavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red..strokeCap = StrokeCap.round
      ..strokeWidth = 10..style = PaintingStyle.stroke;
    Path path = Path()
      ..moveTo(0, 500)
      ..quadraticBezierTo(size.width / 4, 300, size.width / 2, 500)
      ..quadraticBezierTo(size.width / 4 * 3, 700, size.width, 500);
    canvas.drawPath(path, paint);
  }
  
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

2. 循环动画

      小菜使用最常用的平移动画来让曲线动起来,其中注意的是:

  1. 当第一次动画结束时,通过 controller.repeat() 来实现循环播放;
  2. 动画需要使用 Curves.linear 线性动画,否则在循环播放过程中衔接不顺畅;
  3. 使用动画时均需在生命周期结束时 dispose() 销毁动画;
class _ACEWaveState extends State<ACEWave> with TickerProviderStateMixin {
  AnimationController _waveController;
  Animation<double> _waveAnimation;
  int _duration = 2000;
  CurvedAnimation _curvedAnimation;

  @override
  Widget build(BuildContext context) {
    return Transform.translate(
        offset: Offset(MediaQuery.of(context).size.width * _curvedAnimation.value, 0.0),
        child: Container(width: MediaQuery.of(context).size.width,
            child: CustomPaint(painter: _ACEWavePainter())));
  }

  _initAnimations() {
    _waveController = AnimationController(duration: Duration(milliseconds: _duration), vsync: this);
    _curvedAnimation = CurvedAnimation(parent: _waveController, curve: Curves.linear);
    _waveAnimation = Tween(begin: 0.0, end: 1.0).animate(_waveController);
    _waveAnimation.addListener(() => setState(() {}));
    _waveController.forward();
    _waveAnimation.addStatusListener((status) {
      switch (status) {
        case AnimationStatus.completed:
          _waveController.repeat();
          break;
        case AnimationStatus.dismissed:
          _waveController.forward();
          break;
        default:
          break;
      }
    });
  }

  _disposeAnimations() {
    _waveController.dispose();
  }

  @override
  void initState() {
    super.initState();
    _initAnimations();
  }

  @override
  void dispose() {
    _disposeAnimations();
    super.dispose();
  }
}

3. 增加波浪周期

      在执行循环动画之后,发现动画过程中,会有一半是空白的,此时我们增加波浪的周期即可,多绘制一个屏幕的波浪即可,小菜建议前后多绘制两个屏幕的曲线,在循环过程中更流畅;

Path path = Path()
  ..moveTo(0 - size.width, 500)
  ..quadraticBezierTo(size.width / 4 - size.width, 300, size.width / 2 - size.width, 500)
  ..quadraticBezierTo(size.width / 4 * 3 - size.width, 700, size.width - size.width, 500)
  ..quadraticBezierTo(size.width / 4, 300, size.width / 2, 500)
  ..quadraticBezierTo(size.width / 4 * 3, 700, size.width, 500);

canvas.drawPath(path, paint);

4. 调整波浪起始位置

      小菜尝试的曲线是 sin(x) 方式的,起始位置都是 (0.0, 0.0),然而多条波浪时不会都从起点开始;于是小菜提供了一个初始位置,来错开各波浪展示位置;

Path path = Path()
  ..moveTo(0 - size.width - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 - size.width - startOffset,
      500 - waveHeight, size.width / 2 - size.width - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 * 3 - size.width - startOffset,
      500 + waveHeight, size.width - size.width - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 - startOffset, 500 - waveHeight,
      size.width / 2 - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 * 3 - startOffset, 500 + waveHeight,
      size.width - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 + size.width - startOffset,
      500 - waveHeight, size.width / 2 + size.width - startOffset, 500)
  ..quadraticBezierTo(size.width / 4 * 3 + size.width - startOffset,
      500 + waveHeight, size.width + size.width - startOffset, 500);

5. 调整波浪宽度和峰值

      小菜调整完波浪起始位置之后对于波浪的宽度和峰值也要进行调整,保证每条波浪效果略有不同;

      小菜预先绘制了前中后三个屏幕曲线,在测试过程中,若屏幕并非是曲线周期倍数时,衔接过程中会有空余,如图;

      于是小菜计算波浪完整周期倍数与屏幕宽的差值作为移动点 moveTo 的附加宽度即可;

for (int i = 0; i < _count; i++) {
  path..moveTo(waveWidth * i - size.width - startOffset, 500.0)
    ..quadraticBezierTo(
        _quaterWidth + waveWidth * i - size.width - startOffset,
        500 - waveHeight,
        _quaterWidth * 2 + waveWidth * i - size.width - startOffset,
        500.0)
    ..moveTo(
        _quaterWidth * 2 + waveWidth * i - size.width - startOffset, 500.0)
    ..quadraticBezierTo(
        _quaterWidth * 3 + waveWidth * i - size.width - startOffset,
        500 + waveHeight,
        _quaterWidth * 4 + waveWidth * i - size.width - startOffset,
        500.0)
    ..moveTo(waveWidth * i + startOffset + (plusWidth), 500.0)
    ..quadraticBezierTo(
        _quaterWidth + waveWidth * i + startOffset + plusWidth,
        500 - waveHeight,
        _quaterWidth * 2 + waveWidth * i + startOffset + plusWidth,
        500.0)
    ..moveTo(
        _quaterWidth * 2 + waveWidth * i + startOffset + plusWidth, 500.0)
    ..quadraticBezierTo(
        _quaterWidth * 3 + waveWidth * i + startOffset + plusWidth,
        500 + waveHeight,
        _quaterWidth * 4 + waveWidth * i + startOffset + plusWidth,
        500.0)
    ..moveTo(waveWidth * i - size.width + startOffset, 500.0)
    ..quadraticBezierTo(
        _quaterWidth + waveWidth * i - size.width + startOffset,
        500 - waveHeight,
        _quaterWidth * 2 + waveWidth * i - size.width + startOffset,
        500.0)
    ..moveTo(
        _quaterWidth * 2 + waveWidth * i - size.width + startOffset, 500.0)
    ..quadraticBezierTo(
        _quaterWidth * 3 + waveWidth * i - size.width + startOffset,
        500 + waveHeight,
        _quaterWidth * 4 + waveWidth * i - size.width + startOffset,
        500.0);
}


      至此,一个基本的波浪模型基本完成,但还有很多优化的方面,小菜在下篇中进一步绘制波浪效果;如有错误,请多多指导!

来源: 阿策小和尚

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Flutter 56: 图解自定义 BubbleWidget 气泡插件
0 基础学习 Flutter,第五十六步:尝试一下自定义气泡插件!
2666 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
9496 0
Flutter 126: 图解自定义两侧对齐 ACETabBar 标签导航栏
0 基础学习 Flutter,第一百二十六步:简单定义一个可设置左右两侧对齐方式及两侧固定位 Icon 的 ACETabBar 导航栏!
54 0
PyQt5在QWidget窗体中显示Qwidget的自定义类(补:完美解决)
【概览】 1、显示原生Qwidget       1)不使用布局(绝对定位)       2)使用布局 2、显示Qwidget的自定义类       1)不使用布局(绝对定位)       2)使用布局   【知识点】 1、显示原生Qwidget 1)不使用布局(绝对定位) 这...
1338 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13185 0
Openstack组件部署 — 将一个自定义 Service 添加到 Keystone
目录 目录 Keystone 认证流程 让 Keystone 为一个新的项目 Service 提供验证功能 最后 Keystone 认证流程 User 使用凭证(username/password) 到 keystone 验证并获得一个临时的 Token 和 Generic catalog(全局目录),临时的 Token 会存储在 keystone-client(cache UUID locally) 和 keystone-backend 中。
874 0
Android开发教程 - 使用Data Binding(八)使用自定义Interface
本系列目录 使用Data Binding(一)介绍 使用Data Binding(二)集成与配置 使用Data Binding(三)在Activity中的使用 使用Data Binding(四)在Fragment中的使用 ...
895 0
+关注
阿策~
Android/Kotlin/Flutter 小菜鸟,基础技术博客学习与整理~
189
文章
11
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载