第八讲 进阶动画控制

0 阅读9分钟

前言:

路过一条,不用深究,交互并不是vibecoding的核心,完成数据交互和页面展示才是真正的应用!

一、总览

本讲聚焦 Flutter 显式动画 的核心控制能力,是从基础动画(AnimatedContainer 等隐式动画)到自定义、高性能、组合式动画的进阶内容。通过学习本讲,你将掌握:

  1. AnimationController:动画的「总开关」,控制播放/暂停/反向,必须绑定vsync并手动释放
  2. Tween + Curve:将0.0~1.0的基础值映射到任意类型(尺寸/颜色/偏移),Curve让动画更自然
  3. AnimatedBuilder:优化动画性能,仅重建动画相关部分,静态内容放child参数
  4. 组合动画:通过Interval控制动画顺序,通过Matrix4组合多维度变换,实现复杂视觉效果

image.png

  • AnimationController:动画的「发动机」,生成 0.0 ~ 1.0 的线性数值(绑定屏幕刷新率,默认60帧/秒)
  • Tween:插值器,将 0.0~1.0 映射到自定义范围(如颜色、尺寸、位置)
  • Animation:承载动画的实时值,可添加监听回调响应值变化
  • vsync:防止动画在后台运行(节省性能),通常绑定到StatefulWidget的TickerProvider
  • AnimatedBuilder:动画与UI解耦,仅重建需要动画的部分,而非整个组件树

二、 核心知识点

2.1 AnimationController(动画控制器)

核心作用

控制动画的播放状态(启动、暂停、反向、停止),生成基础的 0.0~1.0 动画值。

核心属性/方法
类别名称说明
初始化属性duration动画时长(必传)
vsync绑定TickerProvider(必传,防后台动画)
lowerBound/upperBound动画值范围(默认0.0~1.0)
initialValue初始值(默认0.0)
控制方法forward()正向播放(从当前值到upperBound)
reverse()反向播放(从当前值到lowerBound)
pause()/resume()暂停/恢复
reset()重置到初始值
repeat()循环播放(可设置reverse参数)
监听方法addListener()监听值变化(每次值更新触发)
addStatusListener()监听状态变化(完成/暂停/反向等)
基础案例
import 'package:flutter/material.dart';

// 主入口
void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const AnimationControllerDemo(),
    );
  }
}

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

  @override
  State<AnimationControllerDemo> createState() =>
      _AnimationControllerDemoState();
}

// 混入 TickerProviderStateMixin 提供 vsync
class _AnimationControllerDemoState extends State<AnimationControllerDemo>
    with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 初始化控制器:时长2秒,绑定vsync
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
      // 可选:设置值范围(默认0.0~1.0)
      lowerBound: 0.1,
      upperBound: 1.0,
    );

    // 监听动画状态变化
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // 动画完成后反向播放
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        // 动画回到初始状态后重新播放
        _controller.forward();
      }
    });
  }

  @override
  void dispose() {
    // 必须释放控制器,避免内存泄漏
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimationController 演示')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 直接使用动画值(基础用法,性能一般)
            AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Container(
                  width: _controller.value * 200,
                  height: _controller.value * 200,
                  color: Colors.red,
                );
              },
            ),
            const SizedBox(height: 20),
            // 控制按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => _controller.forward(), // 正向播放(0→1)
                  // 正向播放(0→1)
                  child: const Text('播放'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => _controller.stop(), // 暂停
                  child: const Text('暂停'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => _controller.reset(), // 重置到初始状态
                  child: const Text('重置'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

注意事项
  1. 必须释放:在State的dispose中调用_controller.dispose(),否则会内存泄漏
  2. vsync绑定:必须混入TickerProviderStateMixin(单控制器)或SingleTickerProviderStateMixin(多控制器)
  3. 状态判断:AnimationStatus包含completed(完成)、dismissed(初始)、forward(正向播放)、reverse(反向播放)

2.2 Tween 插值 + Animation 监听

核心作用
  • Tween:将AnimationController的0.0~1.0映射到任意类型的值(如颜色、偏移、尺寸)
  • Animation:Tween与Controller结合后的「值容器」,提供监听能力
核心知识点
  1. 常用Tween类型

    1. Tween<double>:数值(尺寸、透明度)
    2. ColorTween:颜色
    3. OffsetTween:偏移(位移)
    4. BorderRadiusTween:圆角
    5. Matrix4Tween:3D变换
  2. CurvedAnimation:给动画加「曲线」,让运动更符合物理规律:

    1. Curves.linear:匀速(默认)
    2. Curves.ease:慢进快出
    3. Curves.bounceOut:回弹效果
    4. Curves.elasticIn:弹性进入
基础案例
import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimationDemo> createState() => _TweenAnimationDemoState();
}

class _TweenAnimationDemoState extends State<TweenAnimationDemo> with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnim; // 尺寸动画
  late Animation<Color?> _colorAnim; // 颜色动画
  late Animation<Offset> _offsetAnim; // 偏移动画

  @override
  void initState() {
    super.initState();
    // 1. 初始化控制器
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    );

    // 2. 定义Tween插值器,并绑定到控制器
    _sizeAnim = Tween<double>(begin: 50, end: 200).animate(_controller);
    _colorAnim = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);
    _offsetAnim = Tween<Offset>(
      begin: const Offset(-1, 0), // 初始在屏幕左侧
      end: const Offset(1, 0),   // 最终到屏幕右侧
    ).animate(CurvedAnimation( // 加曲线,让动画更自然
      parent: _controller,
      curve: Curves.easeInOut, // 先慢后快再慢
    ));

    // 3. 监听动画值变化(可选)
    _sizeAnim.addListener(() {
      // 手动触发重建(不推荐,建议用AnimatedBuilder)
      // setState(() {});
      print('当前尺寸:${_sizeAnim.value}');
    });

    // 4. 监听动画状态
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });

    // 启动动画
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tween 插值演示')),
      body: Center(
        child: SlideTransition( // 偏移过渡组件
          position: _offsetAnim,
          child: Container(
            width: _sizeAnim.value,
            height: _sizeAnim.value,
            color: _colorAnim.value,
          ),
        ),
      ),
    );
  }
}

注意事项
  • Tween本身不存储值,必须通过animate()绑定到AnimationController
  • 直接用addListener + setState会导致整个组件重建,性能差,优先用AnimatedBuilder
  • CurvedAnimation是对Animation的包装,不影响原控制器

2.3 AnimatedBuilder 优化重构

核心作用

将「动画逻辑」与「UI渲染」解耦,仅重建需要动画的部分,提升性能。

核心属性/注意事项
属性说明
animation绑定动画控制器/Animation(必传)
builder动画值变化时触发的构建函数
child静态子组件(可选),避免重复重建
优化案例(重构上面的Tween案例)
import 'package:flutter/material.dart';

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

  @override
  State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}

class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo> with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnim;
  late Animation<Color?> _colorAnim;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    );

    _sizeAnim = Tween<double>(begin: 50, end: 200).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    _colorAnim = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);

    _controller.repeat(reverse: true); // 循环反向播放
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedBuilder 优化')),
      body: Center(
        // 核心:AnimatedBuilder 只重建builder内的内容
        child: AnimatedBuilder(
          animation: _controller, // 绑定动画控制器
          builder: (context, child) {
            // 仅这里的代码会在动画值变化时重建
            return Container(
              width: _sizeAnim.value,
              height: _sizeAnim.value,
              color: _colorAnim.value,
              // 复用静态子组件(如果有)
              child: child,
            );
          },
          // 静态子组件:不会随动画重建
          child: const Center(
            child: Text(
              '优化动画',
              style: TextStyle(color: Colors.white, fontSize: 20),
            ),
          ),
        ),
      ),
    );
  }
}

注意事项

  1. AnimatedBuilder 是「无状态组件」,不会触发父组件的setState
  2. 优先将静态内容放在child参数中,减少重建开销
  3. 可以嵌套多个AnimatedBuilder实现多维度动画

2.4 显式动画与组合动画

核心概念
  • 显式动画:开发者手动控制动画生命周期(如AnimationController),区别于隐式动画(AnimatedContainer等自动管理)
  • 组合动画:多个动画协同工作(同时播放、顺序播放、叠加播放)
组合动画案例(顺序+叠加)
import 'package:flutter/material.dart';

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

  @override
  State<CombinedAnimationDemo> createState() => _CombinedAnimationDemoState();
}

class _CombinedAnimationDemoState extends State<CombinedAnimationDemo> with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnim; // 缩放动画
  late Animation<double> _rotateAnim; // 旋转动画
  late Animation<double> _opacityAnim; // 透明度动画

  @override
  void initState() {
    super.initState();
    // 总时长5秒的控制器
    _controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    );

    // 1. 缩放动画:0.5→1.5(全程5秒)
    _scaleAnim = Tween<double>(begin: 0.5, end: 1.5).animate(
      CurvedAnimation(parent: _controller, curve: Curves.linear),
    );

    // 2. 旋转动画:0→360度(延迟1秒开始,持续3秒)
    _rotateAnim = Tween<double>(begin: 0, end: 2 * 3.1415).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.2, 0.8), // 0.2*5=1秒开始,0.8*5=4秒结束
      ),
    );

    // 3. 透明度动画:0→1(前2秒),1→0(后3秒)
    _opacityAnim = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.4), // 0→2秒:0→1
      ),
    );
    // 反向透明度(2→5秒:1→0)
    _opacityAnim = Tween<double>(begin: 1, end: 0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.4, 1.0),
      ),
    );

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('组合动画演示')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform(
              // 组合缩放+旋转变换
              transform: Matrix4.identity()
                ..scale(_scaleAnim.value)
                ..rotateZ(_rotateAnim.value),
              alignment: Alignment.center,
              child: Opacity(
                opacity: _opacityAnim.value,
                child: Container(
                  width: 200,
                  height: 200,
                  color: Colors.purple,
                  child: const Center(
                    child: Text(
                      '组合动画',
                      style: TextStyle(color: Colors.white, fontSize: 24),
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

组合动画核心技巧
  1. 同时播放:多个Animation绑定同一个控制器,同步变化
  2. 顺序播放:使用Interval曲线控制每个动画的时间区间(0.0~1.0对应总时长)
  3. 叠加播放:通过Transform的Matrix4组合缩放、旋转、平移
  4. 并行控制器:多个AnimationController分别控制不同动画,通过监听实现协同

三、动画按钮+卡片入场效果

需求

实现一个「点击按钮触发卡片的组合动画」:

  1. 按钮点击后,按钮本身缩放+变色
  2. 卡片从屏幕右侧滑入,同时伴随缩放、渐显、旋转
  3. 支持反向动画(卡片滑出,按钮恢复)

完整代码

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '进阶动画综合案例',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const CombinedAnimationApp(),
    );
  }
}

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

  @override
  State<CombinedAnimationApp> createState() => _CombinedAnimationAppState();
}

class _CombinedAnimationAppState extends State<CombinedAnimationApp> with TickerProviderStateMixin {
  late AnimationController _mainController;
  // 按钮动画
  late Animation<double> _btnScaleAnim;
  late Animation<Color?> _btnColorAnim;
  // 卡片动画
  late Animation<Offset> _cardOffsetAnim;
  late Animation<double> _cardScaleAnim;
  late Animation<double> _cardOpacityAnim;
  late Animation<double> _cardRotateAnim;

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    // 初始化主控制器(时长800ms)
    _mainController = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );

    // 1. 按钮动画配置
    _btnScaleAnim = Tween<double>(begin: 1.0, end: 0.9).animate(
      CurvedAnimation(parent: _mainController, curve: Curves.easeInOut),
    );
    _btnColorAnim = ColorTween(begin: Colors.blue, end: Colors.red).animate(
      CurvedAnimation(parent: _mainController, curve: Curves.easeInOut),
    );

    // 2. 卡片动画配置
    // 偏移:从右侧屏幕外→屏幕中心
    _cardOffsetAnim = Tween<Offset>(
      begin: const Offset(2, 0), // 右侧外
      end: const Offset(0, 0),  // 中心
    ).animate(CurvedAnimation(parent: _mainController, curve: Curves.easeOut));

    // 缩放:0.5→1.0
    _cardScaleAnim = Tween<double>(begin: 0.5, end: 1.0).animate(
      CurvedAnimation(parent: _mainController, curve: Curves.bounceOut),
    );

    // 透明度:0→1
    _cardOpacityAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _mainController, curve: const Interval(0.2, 1.0)),
    );

    // 旋转:-15°→0°
    _cardRotateAnim = Tween<double>(begin: -0.2618, end: 0.0).animate( // -15°(弧度)
      CurvedAnimation(parent: _mainController, curve: Curves.easeInOut),
    );
  }

  // 切换动画状态
  void _toggleAnimation() {
    setState(() {
      _isExpanded = !_isExpanded;
    });
    if (_isExpanded) {
      _mainController.forward(); // 正向播放(显示卡片)
    } else {
      _mainController.reverse(); // 反向播放(隐藏卡片)
    }
  }

  @override
  void dispose() {
    _mainController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('进阶动画综合案例')),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            // 动画按钮
            AnimatedBuilder(
              animation: _mainController,
              builder: (context, child) {
                return Transform.scale(
                  scale: _btnScaleAnim.value,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _btnColorAnim.value,
                      padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(30),
                      ),
                    ),
                    onPressed: _toggleAnimation,
                    child: Text(
                      _isExpanded ? '收起卡片' : '显示卡片',
                      style: const TextStyle(fontSize: 18, color: Colors.white),
                    ),
                  ),
                );
              },
            ),
            const SizedBox(height: 30),
            // 动画卡片
            Expanded(
              child: AnimatedBuilder(
                animation: _mainController,
                builder: (context, child) {
                  return SlideTransition(
                    position: _cardOffsetAnim,
                    child: Opacity(
                      opacity: _cardOpacityAnim.value,
                      child: Transform(
                        transform: Matrix4.identity()
                          ..scale(_cardScaleAnim.value)
                          ..rotateZ(_cardRotateAnim.value),
                        alignment: Alignment.center,
                        child: Container(
                          width: double.infinity,
                          margin: const EdgeInsets.symmetric(horizontal: 20),
                          padding: const EdgeInsets.all(20),
                          decoration: BoxDecoration(
                            color: Colors.white,
                            borderRadius: BorderRadius.circular(15),
                            boxShadow: [
                              BoxShadow(
                                color: Colors.grey.withOpacity(0.5),
                                blurRadius: 10,
                                offset: const Offset(0, 5),
                              )
                            ],
                          ),
                          child: const Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Icon(Icons.star, color: Colors.amber, size: 60),
                              SizedBox(height: 20),
                              Text(
                                '组合动画演示',
                                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                              ),
                              SizedBox(height: 10),
                              Text(
                                '包含:位移、缩放、旋转、透明度、颜色动画\n基于AnimationController+Tween+AnimatedBuilder实现',
                                textAlign: TextAlign.center,
                                style: TextStyle(fontSize: 16, color: Colors.grey),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

关键注意事项

  • 所有AnimationController必须在dispose中释放,避免内存泄漏
  • 避免用addListener + setState实现动画,优先使用AnimatedBuilder
  • 组合动画时,通过一个主控制器管理多个Animation,简化状态控制