如何用Flutter Custom Paint实现带Tween动画的水位指示器?
Flutter 水位指示器(CustomPaint)优化方案
一、核心优化实现代码
以下是针对波浪自然度、动画流畅性、性能的综合优化方案,可直接替换你的原有实现:
import 'package:flutter/material.dart'; import 'dart:math' as Math; class WaterLevelIndicator extends StatefulWidget { final double targetPercentage; // 目标水位百分比(0-1) final Duration animationDuration; const WaterLevelIndicator({ super.key, required this.targetPercentage, this.animationDuration = const Duration(seconds: 2), }); @override State<WaterLevelIndicator> createState() => _WaterLevelIndicatorState(); } class _WaterLevelIndicatorState extends State<WaterLevelIndicator> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _waterLevelAnimation; double _waveOffset = 0.0; // 波浪滚动偏移量 @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.animationDuration, ); // 用缓入缓出曲线让水位过渡更自然 _waterLevelAnimation = Tween<double>(begin: 0, end: widget.targetPercentage).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); // 启动波浪滚动动画 _startWaveScroll(); _controller.forward(); } void _startWaveScroll() { Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 30)); if (mounted) { setState(() { _waveOffset += 0.05; if (_waveOffset > 2 * Math.pi) _waveOffset = 0; }); } return mounted; }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // 用RepaintBoundary隔离重绘区域,提升性能 return RepaintBoundary( child: AnimatedBuilder( animation: _waterLevelAnimation, builder: (context, child) { return CustomPaint( size: const Size(200, 200), painter: _WaterPainter( waterLevel: _waterLevelAnimation.value, waveOffset: _waveOffset, ), ); }, ), ); } } class _WaterPainter extends CustomPainter { final double waterLevel; // 0=空,1=满 final double waveOffset; _WaterPainter({required this.waterLevel, required this.waveOffset}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2; // 绘制水箱边框 final borderPaint = Paint() ..color = Colors.grey.shade300 ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle(center, radius, borderPaint); // 绘制波浪水位 final waterPaint = Paint() ..color = Colors.blue.shade400 ..style = PaintingStyle.fill; // 双波叠加,模拟自然水波效果 Path waterPath = Path(); waterPath.moveTo(0, size.height); for (double x = 0; x <= size.width; x += 2) { // 主波浪(低频率、大振幅) double y1 = size.height * (1 - waterLevel) + 10 * Math.sin((x / size.width) * 2 * Math.pi + waveOffset); // 次级波浪(高频率、小振幅) double y2 = 5 * Math.sin((x / size.width) * 4 * Math.pi + waveOffset * 1.5); double y = y1 + y2; waterPath.lineTo(x, y); } waterPath.lineTo(size.width, size.height); waterPath.close(); // 裁剪出圆形区域,确保水位不超出水箱边框 canvas.clipPath(Path()..addOval(Rect.fromCircle(center: center, radius: radius))); canvas.drawPath(waterPath, waterPaint); } @override bool shouldRepaint(covariant _WaterPainter oldDelegate) { // 仅当水位或波浪偏移变化时重绘,减少性能消耗 return oldDelegate.waterLevel != waterLevel || oldDelegate.waveOffset != waveOffset; } } // 使用示例 class WaterLevelDemo extends StatelessWidget { const WaterLevelDemo({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Padding( padding: EdgeInsets.all(16.0), child: WaterLevelIndicator(targetPercentage: 0.44), // 44%水位 ), Padding( padding: EdgeInsets.all(16.0), child: WaterLevelIndicator(targetPercentage: 0.56), // 56%水位 ), ], ), ), ); } }
二、优化细节说明
- 自然波浪效果: 通过叠加两条不同频率、振幅的正弦波,避免单一波浪的生硬感,更贴近真实水波的起伏
- 平滑动画过渡: 用
Curves.easeInOut曲线替代默认线性动画,水位上升节奏符合视觉预期 - 动态波浪滚动: 持续更新波浪偏移量,让水位呈现流动状态,而非静止波形
- 性能优化:
- 用
RepaintBoundary包裹指示器,隔离重绘区域,避免父组件刷新时触发不必要的重绘 - 在
shouldRepaint中精准判断重绘条件,减少画布刷新次数
- 用
- 精准边界裁剪: 通过
clipPath确保水位严格限制在圆形水箱内部,避免溢出
三、可自定义调整点
- 修改
waterPaint的color参数,切换水位颜色 - 调整
10、5这两个振幅值,控制波浪起伏幅度 - 修改
0.05偏移增量,调整波浪滚动速度 - 替换
Curves.easeInOut为Curves.elasticOut等曲线,实现弹性上升的特殊效果
内容的提问来源于stack exchange,提问作者Md. Sabik Alam Rahat
相关产品推荐
相关产品推荐

