Flutter贝塞尔曲线之水波纹与球形进度(二)

1,234 阅读2分钟

续上篇,再用贝塞尔曲线绘制一个循环水波纹,一个水波纹进度球,先看效果,以下效果的实现用的都是二阶贝塞尔曲线。

效果图

bezierShow4.gif
bezierShow3.gif

我们先实现简单的循环水波纹绘制,我们可以经常见到当手持长绳或上下或左右挥舞绳子,都会产生波形路径,此时如果我们有黑幕遮住 绳子两端只留下绳子中间的波形部分,就可得到一段无限循环的波形图,前提是一直在抖动绳子。。。

循环水波纹

实现分析

先看一下下图,下图也表述了上述场景。

bezier2.jpeg
综上所述,只要屏幕内的波形够多,只要控制住offset的偏移量,无限循环,就能得到我们所想要的波形。

不封闭的循环波

bezierShow4_2.gif

图3 _waveCount 不够的封闭循环波

bezierShow4_1.gif

关键代码

@override
void paint(Canvas canvas, Size size) {
  // TODO: implement paint
  _screenHeight = size.height; //屏幕高,这里不是一直成立,像当有Center 父控件的时候就不成立
  _screenWidth = size.width; //屏幕宽
  _waveCount = (_screenWidth / waveLength)
      .round()+2; //根据波长算出波的个数,这里的波浪个数多一点,这样可以在屏幕内看到更完整的波浪
  _centerY = _screenHeight / 2; //中心高度的值
  _path.moveTo(
      -waveLength + this.myOffsetX, _centerY); //把画笔的起点移到屏幕外的一个波长处,Y轴在屏幕中间
  //this.myOffsetX水平方向的偏移量
  for (int i = 0; i < _waveCount; i++) {
    canvas.save();
    canvas.restore();
    //绘制波谷,控制点在(-3M/4,_centerY),结束点在(-waveLength / 2,_centerY)
    //此处可以先把(waveLength * i) + this.myOffsetX 这段代码移除,所画的就是一个波谷,屏幕外的波谷
    _path.quadraticBezierTo(
        -waveLength / 4 * 3 + (waveLength * i) + this.myOffsetX,
        _centerY + _waveHeight,
        -waveLength / 2 + (waveLength * i) + this.myOffsetX,
        _centerY);
    //绘制波峰,控制点在(-M/4,_centerY),结束点在(0,_centerY)
    //此处可以先把(waveLength * i) + this.myOffsetX 这段代码移除,所画的就是一个波峰,屏幕外的波峰
    _path.quadraticBezierTo(
        -waveLength / 4 + (waveLength * i) + this.myOffsetX,
        _centerY - _waveHeight,
        0 + waveLength * i + this.myOffsetX,
        _centerY);
    canvas.drawPath(_path, _whitePaint); //绘制
  }

  ///封闭绘制区域,构成“深水面”
  _path.lineTo(_screenWidth, _screenHeight); //将画笔从当前位置画到屏幕最右下角
  _path.lineTo(0, _screenHeight); //画到屏幕左下角
  _path.close();
  canvas.drawPath(_path, _pathPaint);
}

水波纹进度球

在上面的基础上,在加了一个竖直方向变化的偏移量_progressY;还有就是之前是方形的,现在变成圆形,方形变圆形只要 把整个画布裁剪成圆的即可。

没有裁剪画布成圆的图

![]

bezierShow3_1.gif

关键代码

@override
void paint(Canvas canvas, Size size) {
  // TODO: implement paint
  //centerOffset圆心
  _path.addArc(Rect.fromCircle(center: centerOffset, radius: r), 0, 360);
  canvas.clipPath(_path); //把画布裁剪成一个圆形,这样怎么画都是圆。
  canvas.drawCircle(centerOffset, r, _pointPaint); //画圆
  _path.reset(); //重置路径
  //this.progress的范围是0-100。
  _progressY = centerOffset.dy + (r - r / 50 * this.progress); //算出Y点的坐标
  //将画笔移动至屏幕外
  _path.moveTo(-waveLength + moveX, _progressY);
  //这里的波峰波谷稍微多点,所以waveCount*2
  for (int i = 0; i < waveCount * 2; i++) {
    canvas.save();
    canvas.restore();
    //绘制波谷,同上
    _path.quadraticBezierTo(
        -waveLength / 4 * 3 + (waveLength * i) + moveX,
        _progressY + waveHeight,
        -waveLength / 2 + (waveLength * i) + moveX,
        _progressY);
    //绘制波峰,同上
    _path.quadraticBezierTo(-waveLength / 4 + (waveLength * i) + moveX,
        _progressY - waveHeight, 0 + waveLength * i + moveX, _progressY);
  }
  print("_moveX=${moveX.toString()}");
  //封闭圆
  _path.moveTo(centerOffset.dx + r, _progressY);
  _path.lineTo(centerOffset.dx + r, centerOffset.dy + r);
  _path.lineTo(centerOffset.dx - r, centerOffset.dy + r);
  _path.lineTo(centerOffset.dx - r, _progressY);
  _path.close();
  canvas.drawPath(_path, _pathPaint);
}

完整代码

Screen类的代码在前面时钟绘制一篇有,要在程序启动的时候调用init初始化。

///贝塞尔曲线示例二
class CustomBezierWidget1 extends StatefulWidget {
  @override
  _CustomBezierWidget1State createState() => _CustomBezierWidget1State();
}

class _CustomBezierWidget1State extends State<CustomBezierWidget1>
    with SingleTickerProviderStateMixin {
  AnimationController animationController;

  Animation<double> animation;

  final double _waveLength = 300; //波浪长

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    animationController = new AnimationController(
        vsync: this, duration: Duration(milliseconds: 600));
    animation = Tween<double>(begin: 0, end: 300).animate(animationController)
      ..addListener(() {
        setState(() {});
      });
    animationController.repeat();
  }

  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    animationController.stop();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: BezierPainter1(animation.value, _waveLength),
    );
  }
}

class BezierPainter1 extends CustomPainter {
  final double myOffsetX; //平移量
  final int _waveHeight = 30; //波浪高
  final double waveLength; //一个波浪的长度

  Paint _pointPaint; //点画笔
  Paint _pathPaint; //线画笔
  Paint _whitePaint; //空白画笔

  double _screenHeight; //屏幕高
  double _screenWidth; //屏幕宽
  double _centerY; //屏幕中间Y坐标

  int _waveCount; //波浪个数

  Path _path = Path(); //路径

  BezierPainter1(this.myOffsetX, this.waveLength) {
    _pointPaint = Paint()
      ..color = Colors.teal
      ..strokeWidth = 4
      ..isAntiAlias = true
      ..style = PaintingStyle.fill;
    _pathPaint = Paint()
      ..color = Colors.deepOrange
      ..style = PaintingStyle.fill
      ..isAntiAlias = true
      ..strokeWidth = 1;

    _whitePaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill
      ..isAntiAlias = true
      ..strokeWidth = 1;
  }

  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    _screenHeight = size.height; //屏幕高,这里不是一直成立,像当有Center 父控件的时候就不成立
    _screenWidth = size.width; //屏幕宽
    _waveCount = (_screenWidth / waveLength)
        .round()+2; //根据波长算出波的个数,这里的波浪个数多一点,这样可以在屏幕内看到更完整的波浪
    _centerY = _screenHeight / 2; //中心高度的值
    _path.moveTo(
        -waveLength + this.myOffsetX, _centerY); //把画笔的起点移到屏幕外的一个波长处,Y轴在屏幕中间
    //this.myOffsetX水平方向的偏移量
    for (int i = 0; i < _waveCount; i++) {
      canvas.save();
      canvas.restore();
      //绘制波谷,控制点在(-3M/4,_centerY),结束点在(-waveLength / 2,_centerY)
      //此处可以先把(waveLength * i) + this.myOffsetX 这段代码移除,所画的就是一个波谷,屏幕外的波谷
      _path.quadraticBezierTo(
          -waveLength / 4 * 3 + (waveLength * i) + this.myOffsetX,
          _centerY + _waveHeight,
          -waveLength / 2 + (waveLength * i) + this.myOffsetX,
          _centerY);
      //绘制波峰,控制点在(-M/4,_centerY),结束点在(0,_centerY)
      //此处可以先把(waveLength * i) + this.myOffsetX 这段代码移除,所画的就是一个波峰,屏幕外的波峰
      _path.quadraticBezierTo(
          -waveLength / 4 + (waveLength * i) + this.myOffsetX,
          _centerY - _waveHeight,
          0 + waveLength * i + this.myOffsetX,
          _centerY);
      canvas.drawPath(_path, _whitePaint); //绘制
    }

    ///封闭绘制区域,构成“深水面”
    _path.lineTo(_screenWidth, _screenHeight); //将画笔从当前位置画到屏幕最右下角
    _path.lineTo(0, _screenHeight); //画到屏幕左下角
    _path.close();
    canvas.drawPath(_path, _pathPaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}

/// 贝塞尔曲线示例三
class WidgetCircleProgressWidget extends StatefulWidget {
  @override
  _WidgetCircleProgressWidgetState createState() =>
      _WidgetCircleProgressWidgetState();
}

class _WidgetCircleProgressWidgetState
    extends State<WidgetCircleProgressWidget> {
  Timer timer;

  double progress = 0;

  bool flag = false;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
      setState(() {
        if (flag) {
          progress = progress - 1;
        } else {
          progress = progress + 1;
        }

        if (progress == 0) {
          flag = false;
        }

        if (progress == 100) flag = true;
      });
    });
  }
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    timer.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return CustomBezierWidget2(progress);
  }
}

class CustomBezierWidget2 extends StatefulWidget {
  final double progress;

  CustomBezierWidget2(this.progress);

  @override
  _CustomBezierWidget2State createState() => _CustomBezierWidget2State();
}

class _CustomBezierWidget2State extends State<CustomBezierWidget2>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  Animation<double> _animationTranslate;

  double _moveX; //移动的X,此处变化一个波长
  double _r; //半径
  double waveLength; //波长

  double _waveCount = 2;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _r = Screen.screenWidthDp / 3;
    waveLength = 2 * _r / _waveCount;
    _animationController =
        new AnimationController(vsync: this, duration: Duration(seconds: 1));
    _animationTranslate =
        Tween<double>(begin: 0, end: waveLength).animate(_animationController)
          ..addListener(() {
            setState(() {});
          });
    _animationController.repeat();
  }

  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    _animationController.stop();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: BezierPainter2(
          progress: this.widget.progress,
          waveHeight: 15,
          moveX: _animationTranslate.value,
          r: _r,
          waveLength: waveLength),
    );
  }
}

class BezierPainter2 extends CustomPainter {
  final double progress; //进度
  final double waveHeight; //波浪高
  final double moveX; //移动的X,此处变化一个波长
  final double r; //半径
  final double waveLength; //一个波浪的长度
  final waveCount = 2; //波浪个数

  double _progressY; //移动中Y的坐标

  Paint _pointPaint; //点画笔
  Paint _pathPaint; //线画笔
  Paint _whitePaint; //空白画笔

  Path _path = Path(); //路径

  Offset centerOffset; //圆心

  BezierPainter2(
      {this.progress, this.waveHeight, this.moveX, this.r, this.waveLength}) {
    _pointPaint = Paint()
      ..color = Colors.teal
      ..strokeWidth = 4
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke;
    _pathPaint = Paint()
      ..color = Colors.deepOrange
      ..style = PaintingStyle.fill
      ..isAntiAlias = true
      ..strokeWidth = 1;
    _whitePaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true
      ..strokeWidth = 1;
    centerOffset = Offset(Screen.screenWidthDp / 2, Screen.screenHeightDp / 2);
  }

  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    //centerOffset圆心
    _path.addArc(Rect.fromCircle(center: centerOffset, radius: r), 0, 360);
    canvas.clipPath(_path); //把画布裁剪成一个圆形,这样怎么画都是圆。
    canvas.drawCircle(centerOffset, r, _pointPaint); //画圆
    _path.reset(); //重置路径
    //this.progress的范围是0-100。
    _progressY = centerOffset.dy + (r - r / 50 * this.progress); //算出Y点的坐标
    //将画笔移动至屏幕外
    _path.moveTo(-waveLength + moveX, _progressY);
    //这里的波峰波谷稍微多点,所以waveCount*2
    for (int i = 0; i < waveCount * 2; i++) {
      canvas.save();
      canvas.restore();
      //绘制波谷,同上
      _path.quadraticBezierTo(
          -waveLength / 4 * 3 + (waveLength * i) + moveX,
          _progressY + waveHeight,
          -waveLength / 2 + (waveLength * i) + moveX,
          _progressY);
      //绘制波峰,同上
      _path.quadraticBezierTo(-waveLength / 4 + (waveLength * i) + moveX,
          _progressY - waveHeight, 0 + waveLength * i + moveX, _progressY);
    }
    print("_moveX=${moveX.toString()}");
    //封闭圆
    _path.moveTo(centerOffset.dx + r, _progressY);
    _path.lineTo(centerOffset.dx + r, centerOffset.dy + r);
    _path.lineTo(centerOffset.dx - r, centerOffset.dy + r);
    _path.lineTo(centerOffset.dx - r, _progressY);
    _path.close();
    canvas.drawPath(_path, _pathPaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}