【flutter高階玩法】貝塞爾實戰1 - 波浪

張風捷特烈發表於2020-04-04

一切視覺的動效都只是感性的欺騙,如我手中的線,跳動的人偶。她征服著你,我控制著她。--捷特

【flutter高階玩法】貝塞爾實戰1 - 波浪

本文所有程式碼: 【github:https://github.com/toly1994328/flutter_play_bezier】


前言

  • 事項預告: 2020-04-04 晚8:30
  • 【程式設計技術交流聖地-Flutter群】: 圖文直播某個元件原始碼,共同交流學習。我們們有緣再見。

上一篇中通過一些可操作的案例感性地瞭解貝塞爾曲線是什麼東西。
本篇將介紹貝塞爾曲線的一個簡單應用,也是我曾經入門Android繪製的第一個東西
這裡想強調一下:貝塞爾曲線甚至說是繪製的本身和平臺並沒有太大的關聯性,可以很方便的移植。重要的不是api本身,而是你能用這些api做出什麼。

【flutter高階玩法】貝塞爾實戰1 - 波浪

圓形 橢圓 圓角矩形
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪

一、靜態繪製

1. 繪製單體

最重要的是知道自己想畫什麼。先看一下曲線怎麼畫。上一篇說過,
二貝最重要的是兩個點控制點終點。如下圖,即可得到一個波峰。

【flutter高階玩法】貝塞爾實戰1 - 波浪

為波的寬高各取一個變數,waveWidth,waveHeight,呢麼很容易得到這三個點的座標

【flutter高階玩法】貝塞爾實戰1 - 波浪

_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(
    waveWidth/2, -waveHeight*2, 
    waveWidth, 0);
複製程式碼

這樣就繪製了一個,通過waveWidth,waveHeight控制長度和寬度。

【flutter高階玩法】貝塞爾實戰1 - 波浪


2. 二貝的相對繪製

先對繪製relativeQuadraticBezierTo,是以當前點為參考點進行繪製。
也就再畫線是剛才的終點相當於0,0。 複製一份就是一個波。

【flutter高階玩法】貝塞爾實戰1 - 波浪

_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
複製程式碼

我們想要的是類似正弦的波,稍微改一筆即可。

【flutter高階玩法】貝塞爾實戰1 - 波浪

_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);
複製程式碼

【flutter高階玩法】貝塞爾實戰1 - 波浪


3. 實現波動的原理

接下來是很關鍵的一步,為了好看,我畫了一個輔助的紫色box,並左移兩個波。

【flutter高階玩法】貝塞爾實戰1 - 波浪

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.

【flutter高階玩法】貝塞爾實戰1 - 波浪

    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即可得到一個不斷運動的波。

【flutter高階玩法】貝塞爾實戰1 - 波浪

canvas.save();
canvas.translate(-2*waveWidth+2*waveWidth*factor, 0);
// 英雄所見...
canvas.restore();
複製程式碼

2. 畫布裁剪

可能現在你還沒有看出什麼,那我現在將紫色矩形框裁一下

【flutter高階玩法】貝塞爾實戰1 - 波浪

  @override
  void paint(Canvas canvas, Size size) {
    canvas.clipRect((Rect.fromCenter(
    center: Offset( waveWidth, 0),width: waveWidth*2,height: 200.0)));
    canvas.save();
    // 英雄所見...
複製程式碼

快速 慢速 寬度
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪

這樣一來,基本的邏輯算是整清了


3. 動畫曲線

既然用了動畫,怎麼能少的了曲線。

fastOutSlowIn easeInQuad linear
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
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
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
  @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();
  }
複製程式碼

下面就來揭密為什麼動畫只是視覺的騙術。 你所見的永動,只是區域性範圍的重複。
把剪裁的區域去掉,也就是下面這醜陋的東西。

【flutter高階玩法】貝塞爾實戰1 - 波浪


5. 圓形剪裁

除了規規整整的矩形,也可以裁成橢圓

圓形 橢圓 圓角矩形
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
【flutter高階玩法】貝塞爾實戰1 - 波浪
---->[圓]----
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元件

核心的原理和思想都說完了,就不廢話了,下面直接貼原始碼,想研究的自己研究一下。不想研究的可以直接拿去用。

【flutter高階玩法】貝塞爾實戰1 - 波浪

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 ~

相關文章