You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:07:32