第七讲 基础过渡动画

2 阅读9分钟

前言:

这一讲对vibecoding没那么重要,可以跳过,精细化过渡可以看看,只是为了整个知识体系的完备度。

一、核心内容

  1. 核心动画组件:AnimatedContainer(通用属性动画)、AnimatedOpacity(透明度专用)、AnimatedSwitcher(组件切换)、Hero(跨页面共享元素),均为「隐式动画」,无需手动管理控制器;
  2. 关键规则:AnimatedSwitcher 子组件需设唯一 key、Hero 动画需相同 tag、动画曲线(Curves)可优化动画的速度变化;
  3. 实战思路:单一动画聚焦核心属性,复合动画可组合多个隐式动画组件,优先满足「流畅性」而非「过度特效」。

本讲聚焦 Flutter 中基础过渡动画的实现,核心是让你掌握「无需手动控制动画控制器」的轻量化动画方案,解决页面元素状态变化(尺寸、透明度)、组件切换、页面间元素共享时的生硬跳转问题,让 UI 交互更流畅自然。

Flutter 基础过渡动画的核心是「状态驱动 + 自动动画」,底层逻辑可拆解为以下结构。

image.png

  1. 隐式动画组件(如 AnimatedContainer)内部封装了 AnimationControllerTween,无需手动管理动画生命周期;
  2. 当组件的「可动画属性」(如 width、opacity)发生变化时,组件自动触发动画,按指定曲线和时长完成属性插值;
  3. Hero 动画是「跨页面共享元素动画」,核心是通过 tag 关联两个页面的相同元素,Flutter 自动计算元素在两个页面的位置/尺寸差值并生成过渡动画。

二、核心技术点:案例、属性、注意事项

1. 隐式动画:AnimatedContainer

功能

监听容器属性(尺寸、颜色、圆角等)变化,自动生成过渡动画。

核心属性
属性名类型说明
durationDuration动画时长(必传)
curveCurve动画曲线(默认线性)
width/heightdouble容器尺寸(变化时触发动画)
decorationDecoration背景、圆角、边框(变化时触发动画)
onEndVoidCallback动画结束回调
案例代码
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 AnimatedContainerDemo(),
    );
  }
}

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

  @override
  State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  // 控制容器状态的变量
  double _width = 100;
  double _height = 100;
  Color _color = Colors.blue;
  double _borderRadius = 8;

  // 切换容器状态
  void _toggleState() {
    setState(() {
      _width = _width == 100 ? 200 : 100;
      _height = _height == 100 ? 150 : 100;
      _color = _color == Colors.blue ? Colors.red : Colors.blue;
      _borderRadius = _borderRadius == 8 ? 30 : 8;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedContainer 案例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 核心:AnimatedContainer 替代普通 Container
            AnimatedContainer(
              width: _width,
              height: _height,
              decoration: BoxDecoration(
                color: _color,
                borderRadius: BorderRadius.circular(_borderRadius),
              ),
              duration: const Duration(milliseconds: 500), // 动画时长
              curve: Curves.easeInOut, // 缓入缓出曲线
              onEnd: () => print('动画结束'),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _toggleState,
              child: const Text('切换状态'),
            )
          ],
        ),
      ),
    );
  }
}

注意事项
  • 必须通过 setState 修改属性值才能触发动画(这里涉及到后面的内容,只需要知道,这是让变化更加丝滑就行了);
  • 仅监听「AnimatedContainer 自身的可动画属性」,子组件变化不会触发容器动画;
  • 避免同时修改过多属性导致动画卡顿(建议单动画聚焦1-2个属性)。

2. 隐式动画:AnimatedOpacity

功能

专门用于控制组件透明度变化的隐式动画,比 AnimatedContainer 更轻量。(用处不大)

核心属性
属性名类型说明
opacitydouble透明度(0=完全透明,1=完全不透明,必传)
durationDuration动画时长(必传)
curveCurve动画曲线
childWidget要控制透明度的子组件
案例代码
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: "AnimatedOpacity",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AnimatedOpacityDemo(),
    );
  }
}

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

  @override
  State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}

class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
  double _opacity = 1.0; // 初始完全不透明

  void _toggleOpacity() {
    setState(() {
      _opacity = _opacity == 1.0 ? 0.2 : 1.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedOpacity 案例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedOpacity(
              opacity: _opacity,
              duration: const Duration(milliseconds: 300),
              curve: Curves.easeOut,
              child: const Text(
                '透明度动画',
                style: TextStyle(fontSize: 30),
              ),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _toggleOpacity,
              child: const Text('切换透明度'),
            )
          ],
        ),
      ),
    );
  }
}

注意事项
  • opacity 值必须在 0~1 之间,超出会报错;
  • 透明状态下组件仍占据布局空间(区别于 Visibility 组件);
  • 可结合 AnimatedSwitcher 实现「透明+切换」复合动画。

3. 组件切换:AnimatedSwitcher

功能

监听子组件替换时,为「旧组件退出」和「新组件进入」添加过渡动画,解决组件切换生硬问题。

核心属性
属性名类型说明
durationDuration动画时长(必传)
reverseDurationDuration反向动画时长(可选)
transitionBuilderWidget Function(Widget, Animation)自定义切换动画(如缩放、平移)
childWidget要切换的子组件(必须有唯一 key)
案例代码
import 'package:flutter/material.dart';

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

  @override
  State<AnimatedSwitcherDemo> createState() => _AnimatedSwitcherDemoState();
}

class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
  bool _isFirstChild = true;

  void _toggleChild() {
    setState(() {
      _isFirstChild = !_isFirstChild;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedSwitcher 案例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 核心:AnimatedSwitcher 包裹切换的子组件
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 500),
              reverseDuration: const Duration(milliseconds: 300),
              // 自定义切换动画:缩放+渐隐
              transitionBuilder: (child, animation) {
                return ScaleTransition(
                  scale: animation,
                  child: FadeTransition(
                    opacity: animation,
                    child: child,
                  ),
                );
              },
              // 子组件必须设置唯一 key
              child: _isFirstChild
                  ? Text(
                      '第一个组件',
                      key: const ValueKey(1),
                      style: const TextStyle(fontSize: 30),
                    )
                  : Container(
                      key: const ValueKey(2),
                      width: 150,
                      height: 150,
                      color: Colors.green,
                      child: const Center(
                        child: Text('第二个组件', style: TextStyle(fontSize: 20, color: Colors.white)),
                      ),
                    ),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _toggleChild,
              child: const Text('切换组件'),
            )
          ],
        ),
      ),
    );
  }
}

注意事项
  • 切换的子组件必须设置唯一 key(否则 Flutter 认为是同一个组件,不会触发动画);
  • transitionBuilder 可组合多个动画(如 Scale + Fade + Slide);
  • 若子组件是相同类型,需通过 key 区分(如 ValueKey 传不同值)。

4. 共享元素:Hero 动画

功能

实现「跨页面」的元素共享过渡(如列表项图片跳转到详情页图片),是页面跳转的核心动画之一。

核心规则
  • 两个页面的共享元素必须设置相同的 heroTag(唯一标识);
  • Hero 组件需包裹要共享的元素(如 Image、Container);
  • 动画由 Flutter 自动计算,无需手动控制。
案例代码
页面1(列表页)
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero 动画 - 页面1')),
      body: Center(
        child: GestureDetector(
          // 点击跳转到详情页
          onTap: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => const HeroSecondPage()),
          ),
          // 共享元素:Hero 包裹图片
          child: Hero(
            tag: 'image_tag', // 唯一标识,两个页面必须相同
            child: Image.network(
              'https://picsum.photos/200/200',
              width: 100,
              height: 100,
              fit: BoxFit.cover,
            ),
          ),
        ),
      ),
    );
  }
}
页面2(详情页)
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero 动画 - 页面2')),
      body: Center(
        // 共享元素:相同 tag 的 Hero 包裹大图
        child: Hero(
          tag: 'image_tag', // 与页面1一致
          child: Image.network(
            'https://picsum.photos/200/200',
            width: 300,
            height: 300,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

注意事项
  • heroTag 必须全局唯一(多个 Hero 动画需用不同 tag);
  • 共享元素建议是「静态组件」(如 Image、Container),避免包裹动态变化的组件;
  • 页面跳转需用 MaterialPageRoute(CupertinoPageRoute 也支持,但动画效果不同)。

5. 动画曲线 Curves

功能

控制动画的「速度变化规律」,让动画更贴近真实物理效果(如加速、减速、回弹)。

常用曲线
曲线名称效果适用场景
Curves.linear匀速机械感动画(如进度条)
Curves.easeIn慢→快进入动画(如弹窗弹出)
Curves.easeOut快→慢退出动画(如弹窗消失)
Curves.easeInOut慢→快→慢通用过渡(如容器尺寸变化)
Curves.bounceOut回弹弹性效果(如按钮点击反馈)
Curves.elasticOut弹性夸张的弹性效果(如卡片弹出)
案例:不同曲线对比
AnimatedContainer(
  width: _width,
  height: _height,
  color: _color,
  duration: const Duration(milliseconds: 800),
  curve: Curves.bounceOut, // 替换为不同曲线测试效果
);
注意事项
  • 曲线仅影响「速度变化」,不改变动画时长;
  • 避免过度使用弹性曲线(如 bounceOut),易导致视觉疲劳;
  • 自定义曲线需继承 Curve 类(新手建议先用内置曲线)。

四、综合应用案例:动画版商品卡片+详情页

功能说明

整合本章节所有技术,实现:

  1. 商品卡片点击时,AnimatedContainer 触发尺寸/颜色动画;
  2. 卡片内价格标签用 AnimatedOpacity 控制显隐;
  3. 卡片切换状态时用 AnimatedSwitcher 切换图标;
  4. 点击卡片跳转到详情页,用 Hero 实现图片共享动画;
  5. 所有动画使用 Curves 优化速度曲线。

完整代码

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 ProductCardPage(),
    );
  }
}

// 商品卡片页面
class ProductCardPage extends StatefulWidget {
  const ProductCardPage({super.key});

  @override
  State<ProductCardPage> createState() => _ProductCardPageState();
}

class _ProductCardPageState extends State<ProductCardPage> {
  bool _isExpanded = false; // 卡片是否展开
  bool _showPrice = true; // 是否显示价格
  bool _isFavorite = false; // 是否收藏

  // 切换卡片展开状态
  void _toggleExpand() {
    setState(() {
      _isExpanded = !_isExpanded;
      _showPrice = !_showPrice; // 同步切换价格显隐
      _isFavorite = !_isFavorite; // 同步切换收藏状态
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('商品列表')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            // 跳转到详情页
            Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const ProductDetailPage()),
            );
          },
          child: AnimatedContainer(
            width: _isExpanded ? 300 : 250,
            height: _isExpanded ? 400 : 300,
            margin: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              color: _isExpanded ? Colors.white : Colors.blue[50],
              borderRadius: BorderRadius.circular(_isExpanded ? 20 : 10),
              boxShadow: [
                BoxShadow(
                  color: Colors.grey.withOpacity(0.3),
                  blurRadius: _isExpanded ? 10 : 5,
                  offset: const Offset(0, 3),
                )
              ],
            ),
            duration: const Duration(milliseconds: 600),
            curve: Curves.easeInOut, // 缓入缓出曲线
            child: Column(
              children: [
                // Hero 共享图片
                Hero(
                  tag: 'product_image',
                  child: Image.network(
                    'https://picsum.photos/300/200',
                    width: double.infinity,
                    height: _isExpanded ? 200 : 150,
                    fit: BoxFit.cover,
                  ),
                ),
                const SizedBox(height: 10),
                // 价格透明度动画
                AnimatedOpacity(
                  opacity: _showPrice ? 1.0 : 0.0,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeOut,
                  child: const Text(
                    '¥99.00',
                    style: TextStyle(fontSize: 20, color: Colors.red),
                  ),
                ),
                const SizedBox(height: 10),
                // 收藏图标切换动画
                AnimatedSwitcher(
                  duration: const Duration(milliseconds: 400),
                  transitionBuilder: (child, animation) {
                    return RotationTransition(
                      turns: animation,
                      child: FadeTransition(opacity: animation, child: child),
                    );
                  },
                  child: Icon(
                    _isFavorite ? Icons.favorite : Icons.favorite_border,
                    key: ValueKey(_isFavorite),
                    color: _isFavorite ? Colors.red : Colors.grey,
                    size: 30,
                  ),
                ),
                const Spacer(),
                // 展开/收起按钮
                ElevatedButton(
                  onPressed: _toggleExpand,
                  child: Text(_isExpanded ? '收起卡片' : '展开卡片'),
                ),
                const SizedBox(height: 20),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// 商品详情页
class ProductDetailPage extends StatelessWidget {
  const ProductDetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('商品详情')),
      body: Column(
        children: [
          // Hero 共享图片(与列表页同tag)
          Hero(
            tag: 'product_image',
            child: Image.network(
              'https://picsum.photos/300/200',
              width: double.infinity,
              height: 300,
              fit: BoxFit.cover,
            ),
          ),
          const Padding(
            padding: EdgeInsets.all(20),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '商品名称',
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 10),
                Text(
                  '商品详情描述:这是一个带所有基础过渡动画的商品卡片案例,包含AnimatedContainer、AnimatedOpacity、AnimatedSwitcher、Hero动画。',
                  style: TextStyle(fontSize: 16, color: Colors.grey),
                ),
                SizedBox(height: 20),
                Text(
                  '¥99.00',
                  style: TextStyle(fontSize: 22, color: Colors.red),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

效果说明

  1. 点击「展开/收起卡片」:AnimatedContainer 触发尺寸、背景、阴影动画;AnimatedOpacity 控制价格显隐;AnimatedSwitcher 切换收藏图标(带旋转+渐隐动画);
  2. 点击卡片任意位置:跳转到详情页,Hero 动画实现图片从小到大的平滑过渡;
  3. 所有动画均使用 Curves.easeInOut/Curves.easeOut 优化,效果更自然。