Skip to content

隐式动画

隐式动画(Implicit Animation)是 Flutter 中最简单的动画方式。你只需要告诉框架"我要变成什么样子",框架就会自动帮你把过渡动画做好。

一句话理解隐式动画

想象你调灯光亮度——你只管把旋钮拧到目标位置,灯光会自动渐变过去,你不需要手动控制每一帧的亮度。隐式动画就是这个"自动渐变"的过程。

本章内容

  • 隐式动画的工作原理
  • 常用隐式动画组件详解
  • 动画曲线(Curves)的选择
  • 隐式动画的适用场景和局限性

工作原理

隐式动画的核心逻辑只有三步:

1. 你设置一个新值(目标值)
2. 框架发现值变了
3. 框架自动从旧值过渡到新值

不需要创建动画控制器,不需要监听每一帧,不需要手动触发播放。只需要 setState() 修改目标值,剩下的交给框架。

隐式动画的局限

正因为"自动",所以你也无法精细控制

  • 不能暂停、不能倒放、不能循环
  • 不能在动画过程中读取当前值
  • 不能让多个动画同步协调

如果你需要这些能力,请参考 显式动画

通用参数

所有隐式动画组件都接受这三个参数:

参数类型说明
durationDuration动画持续时间,必填
curveCurve动画曲线,默认 Curves.linear
onEndVoidCallback?动画结束时的回调(可选)
dart
import 'package:flutter/material.dart';

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

  @override
  State<AnimatedContainerParamsExample> createState() => _AnimatedContainerParamsExampleState();
}

class _AnimatedContainerParamsExampleState extends State<AnimatedContainerParamsExample> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('隐式动画参数')),
      body: Center(
        // ─── ★ AnimatedContainer ──────────────
        child: AnimatedContainer(
          // ─── ★ 动画持续 300ms ──────────────
          duration: const Duration(milliseconds: 300),
          // ─── ☆ 动画持续 300ms ──────────────
          // ─── ★ 先加速后减速 ──────────────
          curve: Curves.easeInOut,
          // ─── ☆ 先加速后减速 ──────────────
          // ─── ★ 动画结束回调 ──────────────
          onEnd: () {
            // 动画结束时触发
          },
          // ─── ☆ 动画结束回调 ──────────────
          width: _expanded ? 200 : 100,
          height: _expanded ? 200 : 100,
          color: _expanded ? Colors.blue : Colors.orange,
        ),
        // ─── ☆ AnimatedContainer ──────────────
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _expanded = !_expanded),
        child: const Icon(Icons.play_arrow),
      ),
    );
  }
}

duration 怎么选?

  • 100~200ms:微小反馈(按钮缩放、颜色切换),用户几乎感觉不到"在动画"
  • 300~500ms:常规过渡(展开收起、淡入淡出),最常用
  • 500ms+:大型变化(页面切换、大幅度移动),慎用,太慢会感觉卡顿

AnimatedContainer

AnimatedContainer最常用的隐式动画组件。它可以对 Container 的几乎所有属性做动画:

  • 尺寸(widthheight
  • 颜色(colordecoration 中的颜色)
  • 圆角(borderRadius
  • 内外边距(paddingmargin
  • 对齐方式(alignment

最简单的例子:点击变大变小

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

  @override
  State<AnimatedBoxDemo> createState() => _AnimatedBoxDemoState();
}

class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
  bool _expanded = false;  // 状态:是否展开

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),  // 点击切换状态
      // ─── ★ AnimatedContainer 点击变大变小 ──────────────
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        // 根据状态设置不同的值
        width: _expanded ? 200 : 100,
        height: _expanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _expanded ? Colors.blue : Colors.red,
          borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
        ),
        alignment: Alignment.center,
        child: Text(
          _expanded ? '点我收起' : '点我展开',
          style: const TextStyle(color: Colors.white),
        ),
      ),
      // ─── ☆ AnimatedContainer 点击变大变小 ──────────────
    );
  }
}

关键理解

AnimatedContainerContainer 的区别就一个字——"Animated"前缀。当属性值发生变化时,Container 会瞬间变到新值,而 AnimatedContainer 会平滑过渡。

同时动画多个属性

AnimatedContainer 可以同时对多个属性做动画,只需把它们都写进去:

dart
AnimatedContainer(
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeInOut,
  // 以下属性变化时,都会自动做动画
  width: _expanded ? 200 : 100,
  height: _expanded ? 200 : 100,
  padding: _expanded ? const EdgeInsets.all(24) : const EdgeInsets.all(8),
  decoration: BoxDecoration(
    color: _expanded ? Colors.blue : Colors.red,
    borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
    boxShadow: _expanded
        ? [BoxShadow(color: Colors.black26, blurRadius: 16, offset: Offset(0, 4))]
        : [],
  ),
)

装饰器的注意事项

decoration 整体变化时会做动画,但 BoxDecoration 内部并不是每个属性都支持平滑过渡。例如 border(边框)的切换是瞬间完成的,不会渐变。支持平滑过渡的属性有:colorborderRadiusboxShadow 的偏移和模糊半径。

AnimatedOpacity

AnimatedOpacity 专门控制透明度动画,常用于显示/隐藏元素。

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

  @override
  State<FadeDemo> createState() => _FadeDemoState();
}

class _FadeDemoState extends State<FadeDemo> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // ─── ★ AnimatedOpacity 透明度动画 ──────────────
        AnimatedOpacity(
          // ─── ★ 1.0 完全可见,0.0 完全透明 ──────────────
          opacity: _visible ? 1.0 : 0.0,
          // ─── ☆ 1.0 完全可见,0.0 完全透明 ──────────────
          duration: const Duration(milliseconds: 500),
          curve: Curves.easeInOut,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
            child: const Center(
              child: Text('Hello', style: TextStyle(color: Colors.white)),
            ),
          ),
        ),
        // ─── ☆ AnimatedOpacity 透明度动画 ──────────────
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _visible = !_visible),
          child: Text(_visible ? '隐藏' : '显示'),
        ),
      ],
    );
  }
}

AnimatedOpacity vs Opacity

  • Opacity(opacity: 0) —— 元素透明但仍然占位,仍然参与布局、仍然接收点击事件
  • AnimatedOpacity(opacity: 0) —— 同上,只是多了过渡动画

如果你希望元素消失后不占位,应该用条件判断直接移除 Widget:

dart
// 不占位的"隐藏"
if (_visible) MyWidget()

AnimatedSwitcher

AnimatedSwitcher 用于子组件切换时的过渡动画,比如切换不同的文字、不同的图标。

基本用法

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

  @override
  State<SwitcherDemo> createState() => _SwitcherDemoState();
}

class _SwitcherDemoState extends State<SwitcherDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // ─── ★ AnimatedSwitcher 切换动画 ──────────────
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 300),
          // ─── ★ 自定义过渡效果 ──────────────
          transitionBuilder: (child, animation) {
          // ─── ☆ 自定义过渡效果 ──────────────
            return FadeTransition(opacity: animation, child: child);
          },
          child: _showFirst
              ? Container(
                  key: const ValueKey('first'),   // ← 必须设置不同的 key!
                  width: 100,
                  height: 100,
                  color: Colors.red,
                  child: const Center(child: Text('A')),
                )
              : Container(
                  key: const ValueKey('second'),  // ← 必须设置不同的 key!
                  width: 100,
                  height: 100,
                  color: Colors.blue,
                  child: const Center(child: Text('B')),
                ),
        ),
        // ─── ☆ AnimatedSwitcher 切换动画 ──────────────
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: const Text('切换'),
        ),
      ],
    );
  }
}

key 是必须的!

AnimatedSwitcher 通过判断 child 是否变化来触发动画。如果新旧 child 类型相同(比如都是 Container),Flutter 会认为它们是"同一个"Widget,不会触发切换动画

解决方法:给不同的 child 设置不同的 key,通常是 ValueKey('唯一标识')

自定义过渡效果

transitionBuilder 让你完全控制切换动画的效果:

dart
// 缩放切换
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) {
    return ScaleTransition(scale: animation, child: child);
  },
  child: ...,
)

// 滑动切换
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) {
    final offsetAnimation = Tween<Offset>(
      begin: const Offset(1.0, 0.0),  // 从右侧滑入
      end: Offset.zero,
    ).animate(animation);
    return SlideTransition(position: offsetAnimation, child: child);
  },
  child: ...,
)

AnimatedCrossFade

AnimatedCrossFade 用于两个组件之间的交叉淡入淡出。与 AnimatedSwitcher 不同的是,它可以同时显示两个组件的过渡:

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

  @override
  State<CrossFadeDemo> createState() => _CrossFadeDemoState();
}

class _CrossFadeDemoState extends State<CrossFadeDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // ─── ★ AnimatedCrossFade 交叉淡入淡出 ──────────────
        AnimatedCrossFade(
          firstChild: Container(
            width: 200,
            height: 100,
            color: Colors.red,
            child: const Center(child: Text('第一个')),
          ),
          secondChild: Container(
            width: 200,
            height: 100,
            color: Colors.blue,
            child: const Center(child: Text('第二个')),
          ),
          crossFadeState: _showFirst
              ? CrossFadeState.showFirst
              : CrossFadeState.showSecond,
          duration: const Duration(milliseconds: 300),
        ),
        // ─── ☆ AnimatedCrossFade 交叉淡入淡出 ──────────────
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: const Text('切换'),
        ),
      ],
    );
  }
}

AnimatedSwitcher vs AnimatedCrossFade 怎么选?

  • AnimatedSwitcher:切换任意数量的子组件(2个、3个、10个都行),灵活性高
  • AnimatedCrossFade:只在固定的两个组件间切换,写法更简单,但不能扩展到三个以上

大多数场景推荐 AnimatedSwitcher,它更通用。

TweenAnimationBuilder — 自定义动画

当内置的 AnimatedXxx 组件不能满足需求时,TweenAnimationBuilder 可以让你对任意值做动画:

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

  @override
  Widget build(BuildContext context) {
    // ─── ★ TweenAnimationBuilder 自定义动画 ──────────────
    return TweenAnimationBuilder<double>(
      // ─── ★ 值区间 ──────────────
      tween: Tween(begin: 0, end: 1),
      // ─── ☆ 值区间 ──────────────
      duration: const Duration(seconds: 1),
      curve: Curves.easeOut,
      builder: (context, value, child) {
        // value 就是当前动画值,从 0 渐变到 1
        return Opacity(
          opacity: value,                       // 透明度 0 → 1
          child: Transform.scale(
            scale: 0.5 + value * 0.5,           // 缩放 0.5 → 1.0
            child: child,
          ),
        );
      },
      // child 放在这里,避免重复构建
      child: const Icon(Icons.star, size: 100, color: Colors.amber),
    );
    // ─── ☆ TweenAnimationBuilder 自定义动画 ──────────────
  }
}

什么时候用 TweenAnimationBuilder?

  • 内置组件没有对应的 AnimatedXxx 版本时(比如 Transform.rotate 没有 AnimatedRotate——当然新版 Flutter 已有 AnimatedRotation
  • 你需要同时控制多个属性联动时
  • 你需要自定义的动画值(非标准属性)

TweenAnimationBuilder 的本质是:你提供一个值的区间(tween),它帮你自动从起点插值到终点,每一帧把当前值传给你,你来决定怎么用这个值。

实战:数字滚动动画

dart
TweenAnimationBuilder<int>(
  tween: IntTween(begin: 0, end: 100),
  duration: const Duration(seconds: 2),
  curve: Curves.easeOut,
  builder: (context, value, child) {
    return Text(
      '$value%',                              // 数字从 0 滚动到 100
      style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
    );
  },
)

其他隐式动画组件速查

组件动画效果典型场景
AnimatedContainer容器属性变化尺寸、颜色、圆角等过渡
AnimatedOpacity透明度变化显示/隐藏元素
AnimatedPadding内边距变化展开/收起内容
AnimatedPositioned位置变化(在 Stack 中)元素移动
AnimatedScale缩放变化按钮点击反馈
AnimatedRotation旋转变化刷新图标旋转
AnimatedDefaultTextStyle文字样式变化字体大小、颜色过渡
AnimatedSwitcher子组件切换时的过渡切换不同 Widget
AnimatedCrossFade两个组件间的淡入淡出两状态切换
TweenAnimationBuilder自定义动画值任意值动画

Curves 动画曲线

动画曲线决定了动画的速度变化——是匀速、先快后慢、还是弹弹弹?

生活中的类比

曲线类比
Curves.linear电梯匀速上升
Curves.easeOut汽车刹车——开始快,逐渐减速停下
Curves.easeIn汽车起步——慢慢开始,越来越快
Curves.easeInOut汽车起步到刹车——慢→快→慢
Curves.bounceOut球落地弹跳
Curves.elasticOut拉橡皮筋松手——超过目标再弹回来

常用曲线速查

dart
Curves.linear          // 线性(匀速),机械感
Curves.ease            // 缓入缓出(默认推荐)
Curves.easeIn          // 缓入(慢慢开始)
Curves.easeOut         // 缓出(慢慢停止)—— 最常用
Curves.easeInOut       // 缓入缓出
Curves.decelerate      // 减速
Curves.bounceIn        // 弹跳入
Curves.bounceOut       // 弹跳出
Curves.elasticIn       // 弹性入
Curves.elasticOut      // 弹性出

怎么选曲线?

  • 90% 的情况:用 Curves.easeOutCurves.easeInOut 就够了
  • 需要弹跳感Curves.bounceOut(比如按钮点击)
  • 需要弹性感Curves.elasticOut(比如下拉刷新回弹)
  • 不要用 linear:匀速动画看起来很机械,不自然

常见问题

1. 动画没有效果?

最常见原因:忘记了 setState()。隐式动画是靠检测属性变化来触发的,如果你只是改了变量但没有调用 setState(),Widget 不会重建,动画也不会触发。

dart
// ❌ 错误:没有 setState
onTap: () => _expanded = !_expanded

// ✅ 正确:用 setState 包裹
onTap: () => setState(() => _expanded = !_expanded)

2. AnimatedSwitcher 切换没有动画?

原因:新旧 child 的类型相同且没有设置不同的 key,Flutter 认为它们是同一个 Widget。

dart
// ❌ 错误:没有 key,不会触发切换动画
child: _showFirst ? Text('A') : Text('B')

// ✅ 正确:设置不同的 key
child: _showFirst ? Text('A', key: ValueKey('a')) : Text('B', key: ValueKey('b'))

3. 动画太快或太慢?

调整 duration 参数。一般原则:

  • 感觉"闪了一下"→ 太快了,加到 300ms
  • 感觉"卡卡的"→ 太慢了,减到 200ms

4. 想让动画循环播放?

隐式动画不支持循环播放。如果需要循环动画,请使用 显式动画 中的 AnimationController.repeat()

基于 Flutter 官方文档整理