【Flutter 專題】83 圖解自定義 ACEWave 波浪 Widget (一)|8月更文挑戰

阿策小和尚發表於2021-08-05

      “這是我參與8月更文挑戰的第5天,活動詳情檢視: 8月更文挑戰” juejin.cn/post/698796…

      小菜今天嘗試一下繪製波浪的效果,雖然 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);
}
複製程式碼


      至此,一個基本的波浪模型基本完成,但還有很多優化的方面,小菜在下篇中進一步繪製波浪效果;如有錯誤,請多多指導!

來源: 阿策小和尚

相關文章