阅读 2006

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

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

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


前言

  • 事项预告: 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 ~

关注下面的标签,发现更多相似文章
评论