← Frontend / Flutter

11_动画系统

Flutter 动画系统和动画组件

Flutter 动画分为隐式动画和显式动画,以及页面间共享元素的 Hero 动画。


隐式动画(推荐首选)

只需改变属性值,Widget 自动播放过渡动画,类似 CSS transition:

class AnimatedCardDemo extends StatefulWidget {
  @override
  State<AnimatedCardDemo> createState() => _AnimatedCardDemoState();
}

class _AnimatedCardDemoState extends State<AnimatedCardDemo> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 350),
        curve: Curves.easeInOutCubic,        // 缓动函数(类比 CSS timing-function)
        width: _expanded ? 300 : 150,
        height: _expanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _expanded ? Colors.blue : Colors.grey.shade300,
          borderRadius: BorderRadius.circular(_expanded ? 24 : 8),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(_expanded ? 0.3 : 0.1),
              blurRadius: _expanded ? 20 : 4,
              offset: const Offset(0, 4),
            ),
          ],
        ),
        child: Center(
          child: Text(
            _expanded ? '收起' : '展开',
            style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }
}

常用隐式动画 Widget 速查:

Widget 动画效果 类比 CSS
AnimatedContainer 尺寸、颜色、装饰变化 transition: all
AnimatedOpacity 透明度变化 transition: opacity
AnimatedPadding 内边距变化 transition: padding
AnimatedAlign 对齐位置变化 —
AnimatedSwitcher 子 Widget 切换过渡 —
AnimatedDefaultTextStyle 文字样式变化 transition: font-*
TweenAnimationBuilder 任意值的自定义过渡 @keyframes
// AnimatedSwitcher:切换不同 Widget 时自动过渡(key 必须不同!)
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) => FadeTransition(
    opacity: animation,
    child: child,
  ),
  child: _isLoading
      ? const CircularProgressIndicator(key: ValueKey('loading'))
      : Text('已加载', key: ValueKey('content')),
)

// TweenAnimationBuilder:数字增减动画
TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: _targetValue),
  duration: const Duration(milliseconds: 800),
  curve: Curves.easeOut,
  builder: (context, value, _) {
    return Text(
      value.toStringAsFixed(0),
      style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
    );
  },
)

显式动画(精细控制)

需要循环、暂停、精确控制进度时使用 AnimationController:

class PulsingButton extends StatefulWidget {
  @override
  State<PulsingButton> createState() => _PulsingButtonState();
}

class _PulsingButtonState extends State<PulsingButton>
    with SingleTickerProviderStateMixin {  // ← mixin 提供 vsync

  late AnimationController _controller;
  late Animation<double> _scaleAnim;
  late Animation<double> _opacityAnim;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,            // vsync 同步帧率,节省资源
    );

    _scaleAnim = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _opacityAnim = Tween<double>(begin: 1.0, end: 0.6).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _controller.repeat(reverse: true); // 来回循环
  }

  @override
  void dispose() {
    _controller.dispose();  // 必须释放!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(             // 监听动画值重建 UI
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnim.value,
          child: Opacity(
            opacity: _opacityAnim.value,
            child: child,
          ),
        );
      },
      child: ElevatedButton(           // child 不随动画重建(静态部分优化)
        onPressed: () {},
        child: const Text('点击我'),
      ),
    );
  }
}

// AnimationController 常用方法
_controller.forward();          // 正向播放(0 → 1)
_controller.reverse();          // 反向播放(1 → 0)
_controller.repeat();           // 正向循环
_controller.repeat(reverse: true); // 来回循环
_controller.stop();             // 暂停
_controller.reset();            // 重置到 0
_controller.value = 0.5;        // 跳到指定进度

多动画协调(TweenSequence)

// 分段动画:先放大,再恢复
final scaleAnim = TweenSequence<double>([
  TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.5), weight: 50),
  TweenSequenceItem(tween: Tween(begin: 1.5, end: 1.0), weight: 50),
]).animate(_controller);

// 多个动画各自指定时间区间(Interval)
final fadeAnim = Tween<double>(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.0, 0.5, curve: Curves.easeIn), // 前 50% 时间
  ),
);
final slideAnim = Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.3, 1.0, curve: Curves.easeOut), // 30%-100% 时间
  ),
);

Hero 动画(页面间共享元素)

两个页面间的共享元素过渡,tag 相同的 Widget 会自动产生飞翔效果:

// 列表页
GestureDetector(
  onTap: () => Navigator.push(context, MaterialPageRoute(
    builder: (_) => ProductDetailPage(product: product),
  )),
  child: Hero(
    tag: 'product_${product.id}', // tag 必须唯一,且两页面相同
    child: Image.network(product.imageUrl, width: 80, height: 80, fit: BoxFit.cover),
  ),
)

// 详情页(tag 保持一致)
Hero(
  tag: 'product_${product.id}',
  child: Image.network(product.imageUrl, width: double.infinity, height: 300, fit: BoxFit.cover),
)

Curves 缓动函数速查

// 对应 CSS timing-function
Curves.linear            → linear
Curves.ease              → ease
Curves.easeIn            → ease-in
Curves.easeOut           → ease-out
Curves.easeInOut         → ease-in-out
Curves.easeInOutCubic    → cubic-bezier(0.645, 0.045, 0.355, 1.0)(常用)
Curves.fastOutSlowIn     → Material Design 标准缓动
Curves.bounceOut         → 弹跳效果
Curves.elasticIn         → 弹性进入
Curves.decelerate        → 减速