Flutter---波形动画

效果图

动画原理
Dart 复制代码
动画控制器每16ms触发一次,每3帧(约50ms)执行一次数据更新:删除最左边的柱子,
在最右边添加新柱子,通过 setState 触发界面重绘,形成柱子不断向左滚动的视觉效果。

关键点:

持续的时间驱动(AnimationController.repeat())

持续的数据变化(不断删除和添加柱子)

缺一不可:

有时间驱动没数据变化 → 界面静止(白刷新)

有数据变化没时间驱动 → 只变一次(不连续)
关键代码
Dart 复制代码
//移除最左边的柱子
heightValue.removeAt(0);
//在右边添加柱子
isHaveVoice ? heightValue.add(_random.nextDouble()*10) : heightValue.add(1.5);
动画循环图
Dart 复制代码
┌─────────────────────────────────────────────────────────┐
│                    动画循环(每16ms)                     │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 1. AnimationController 触发 addListener                 │
│    (屏幕每刷新一次就触发一次,约16ms/次)                  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 2. 调用 _updateBars()                                   │
│    _frameCount++ (计数器+1)                              │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 3. 判断:_frameCount - _lastAddFrame >= 3 ?             │
│    (每3帧执行一次,约50ms)                               │
└─────────────────────────────────────────────────────────┘
                          ↓ 是
┌─────────────────────────────────────────────────────────┐
│ 4. setState() 触发重绘                                  │
│    - heightValue.removeAt(0) 删除左边                   │
│    - heightValue.add(新高度)  添加右边                   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 5. Flutter 重新执行 build() 方法                        │
│    - Row 根据新的 heightValue 重建所有柱子               │
│    - AnimatedContainer 平滑过渡到新高度                  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ 6. 用户看到:柱子向左移动了1格,右边出现新柱子            │
└─────────────────────────────────────────────────────────┘
                          ↓
                    回到步骤1(无限循环)
数据变化过程
Dart 复制代码
// 初始状态
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ..., 0]  (35个0)

// 第1次更新(32ms)
删除左边第1个0 → [0, 0, ..., 0] (34个0)
添加新高度5.2   → [0, 0, ..., 0, 5.2] (34个0 + 1个5.2)

// 第2次更新(80ms)
删除左边第1个0 → [0, ..., 0, 5.2] (33个0 + 1个5.2)
添加新高度8.1   → [0, ..., 0, 5.2, 8.1] (33个0 + 2个有高度的)

// 第3次更新(128ms)
删除左边第1个0 → [0, ..., 5.2, 8.1] (32个0 + 2个有高度的)
添加新高度3.5   → [0, ..., 5.2, 8.1, 3.5] (32个0 + 3个有高度的)

// 持续执行...
// 有高度的柱子逐渐向左移动
// 最终35个柱子都有高度(全是非0值)

疑问点

Dart 复制代码
1.heightValue = List.generate(barCount, (index) => 0.0);
可变的列表初始化时什么意思?如果更好的理解List.generate?

答:可变列表是指可以增加和删减数据。

// 基本语法
List.generate(长度, 生成器函数)

// 示例1:最简单的用法
List.generate(5, (index) => 0)
// 结果:[0, 0, 0, 0, 0]


一开始都是0,所有看着是没有图形,但是是有数据的

2.为什么使用Row构建UI,而不是Stack,为什么不需要切换小柱子的x轴,就可以直接挪动实现动画?

答:Row 自动布局,Stack 需要手动定位。
 Row 会自动根据数据顺序重新计算每个柱子的位置。你只需要改变数据(删除第一个,添加最后一个),
Row 就会自动完成所有位置计算和重新排列,你完全不需要手动操作 x 轴。

代码示例

Dart 复制代码
import 'package:flutter/material.dart';
import 'dart:math';

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<StatefulWidget> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> with SingleTickerProviderStateMixin {

  List<double> heightValue = []; //存储每个柱子的高度值
  final Random _random = Random();
  late AnimationController _animationController;

  // 柱子配置
  final int barCount = 35; // 同时显示的柱子数量
  final double barWidth = 2; //柱子宽度
  final double spacing = 5; //柱子之间的间距

  bool isHaveVoice = false; //控制是否波动
  
  int _lastAddFrame = 0; // 上次添加新柱子的帧数
  int _frameCount = 0;  //总帧数计数器

  @override
  void initState() {
    super.initState();

    // 使用可变的列表初始化
    heightValue = List.generate(barCount, (index) => 0.0);

    // 创建动画控制器
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 16), // 每帧触发
    )..addListener(() { //添加监听器
      // 每帧都会调用,更新柱子的位置
      if(mounted){
        _updateBars();
      }
    });

    // 启动动画
    _animationController.repeat();
  }



  //================================更新柱子的位置==============================
  void _updateBars() {

    _frameCount++;

     if(!mounted)return;


    // 每3帧添加一个新柱子(约50ms,接近40ms)
    if (_frameCount - _lastAddFrame >= 3) {
      _lastAddFrame = _frameCount;//更新上次添加的帧数

      setState(() {
        // 移除最左边的柱子(移出屏幕)
        heightValue.removeAt(0);
        // 在右边添加新的随机高度柱子
        isHaveVoice ? heightValue.add(_random.nextDouble() * 10) : heightValue.add(1.5);
      });
    }

  }

   @override
  void dispose() {
    // TODO: implement dispose

    _animationController.stop();
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        leading: IconButton(
          onPressed: () {
            Navigator.pop(context);
          },
          icon: const Icon(Icons.arrow_back_ios),
        ),
        title: const Text("录音动画"),
      ),
      body: Column(
        children: [
          Container(
            height: 100,
            width: double.infinity,
            margin: const EdgeInsets.all(20),
            color: Colors.lightBlueAccent.withOpacity(0.3),
            child: Center(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: List.generate(heightValue.length, (index) {
                  final height = 200 * heightValue[index] / 100;

                  return Container(
                    width: barWidth,
                    margin: EdgeInsets.symmetric(horizontal: spacing / 2),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 50), // 过渡动画时长
                      curve: Curves.easeOut,
                      height: height,
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  );
                }),
              ),
            ),
          ),

          SizedBox(height: 100,),
          //按钮
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [

              //无声音
              GestureDetector(
                onTap: (){
                  setState(() {
                    isHaveVoice = false;
                  });
                },
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.grey,
                    child: Center(
                      child: Text("无波动"),
                    ),
                  ),
              ),

              SizedBox(width: 30,),

              //有声音
              GestureDetector(
                onTap: (){
                  setState(() {
                    isHaveVoice = true;
                  });
                },
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.blue,
                  child: Center(
                    child: Text("有波动"),
                  ),
                ),
              ),
            ],
          )
        ],
      )
    );
  }
}
相关推荐
BG8 分钟前
利用Codex GPT-5.5 基于extended_image新增图片透视变换功能
前端·flutter
帅次3 小时前
LazyColumn 懒加载、items 与 key
android·flutter·kotlin·android studio·webview
恋猫de小郭6 小时前
经典,Flutter iOS 又修复了一个构建问题,还是很抽象
android·前端·flutter
我这一生如履薄冰~6 小时前
flutter开发适配底部导航条样式
android·flutter
张风捷特烈7 小时前
状态管理大乱斗#07 | Signals 源码评析 - 暗流涌动
android·前端·flutter
Justin在掘金21 小时前
Riverpod 实战指南
flutter
MonkeyKing71551 天前
Flutter Riverpod 2.x 设计思想与最佳实践
前端·flutter
梦想不只是梦与想1 天前
Flutter中 yield*关键字
flutter·生成器函数
用户游民1 天前
Flutter GetX实现原理
前端·flutter