小菜今天嘗試一下繪制波浪的效果,雖然 pub 倉庫中已經有成熟的插件,但小菜還是準備用之前學習的 Canvas 和 Animation 嘗試自定義一個 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. 循環動畫
小菜使用最常用的平移動畫來讓曲線動起來,其中注意的是:
- 當第一次動畫結束時,通過 controller.repeat() 來實作循環播放;
- 動畫需要使用 Curves.linear 線性動畫,否則在循環播放過程中銜接不順暢;
- 使用動畫時均需在生命周期結束時 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);
}
至此,一個基本的波浪模型基本完成,但還有很多優化的方面,小菜在下篇中進一步繪制波浪效果;如有錯誤,請多多指導!
來源: 阿策小和尚