Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第16章 - 动画入门

嗨,朋友!我是长安。

一个好的 App 不仅要功能完善,还要有流畅的动画效果来提升用户体验。这一章,我们来学习 Flutter 中的动画,从简单的隐式动画到复杂的显式动画。

🎬 动画基础概念

Flutter 动画分类

类型特点适用场景
隐式动画简单易用,自动处理简单的属性变化
显式动画完全控制,更灵活复杂的动画效果
Hero 动画页面间共享元素图片放大、列表详情
物理动画模拟真实物理效果弹簧、摩擦等效果

✨ 隐式动画(最简单)

隐式动画是 Flutter 中最简单的动画方式,只需要改变属性值,Flutter 自动处理过渡动画。

AnimatedContainer

最常用的隐式动画 Widget,可以动画化几乎所有 Container 属性。

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

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

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedContainer')),
      body: Center(
        child: GestureDetector(
          onTap: () => setState(() => _expanded = !_expanded),
          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 ? 100 : 16),
              boxShadow: [
                BoxShadow(
                  color: Colors.black26,
                  blurRadius: _expanded ? 20 : 5,
                  offset: Offset(0, _expanded ? 10 : 2),
                ),
              ],
            ),
            child: Center(
              child: Text(
                _expanded ? '收缩' : '展开',
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

AnimatedOpacity

透明度渐变动画。

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

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

class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedOpacity')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedOpacity(
              duration: const Duration(milliseconds: 500),
              opacity: _visible ? 1.0 : 0.0,
              child: Container(
                width: 150,
                height: 150,
                color: Colors.blue,
                child: const Center(
                  child: Text('我会消失', style: TextStyle(color: Colors.white)),
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => setState(() => _visible = !_visible),
              child: Text(_visible ? '隐藏' : '显示'),
            ),
          ],
        ),
      ),
    );
  }
}

AnimatedPositioned

位置动画,必须在 Stack 中使用。

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

  @override
  State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState();
}

class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
  bool _moved = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedPositioned')),
      body: Stack(
        children: [
          AnimatedPositioned(
            duration: const Duration(milliseconds: 500),
            curve: Curves.elasticOut,
            left: _moved ? 200 : 50,
            top: _moved ? 300 : 100,
            child: GestureDetector(
              onTap: () => setState(() => _moved = !_moved),
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.purple,
                  borderRadius: BorderRadius.circular(16),
                ),
                child: const Center(
                  child: Text('点我移动', style: TextStyle(color: Colors.white)),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

AnimatedCrossFade

两个 Widget 之间的淡入淡出切换。

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

  @override
  State<AnimatedCrossFadeDemo> createState() => _AnimatedCrossFadeDemoState();
}

class _AnimatedCrossFadeDemoState extends State<AnimatedCrossFadeDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedCrossFade')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedCrossFade(
              duration: const Duration(milliseconds: 300),
              crossFadeState: _showFirst
                  ? CrossFadeState.showFirst
                  : CrossFadeState.showSecond,
              firstChild: Container(
                width: 200,
                height: 200,
                color: Colors.blue,
                child: const Center(
                  child: Icon(Icons.favorite, color: Colors.white, size: 60),
                ),
              ),
              secondChild: Container(
                width: 200,
                height: 200,
                color: Colors.red,
                child: const Center(
                  child: Icon(Icons.star, color: Colors.white, size: 60),
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => setState(() => _showFirst = !_showFirst),
              child: const Text('切换'),
            ),
          ],
        ),
      ),
    );
  }
}

AnimatedSwitcher

更灵活的 Widget 切换动画。

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

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

class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedSwitcher')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 300),
              transitionBuilder: (child, animation) {
                return ScaleTransition(
                  scale: animation,
                  child: child,
                );
              },
              child: Text(
                '$_count',
                // 关键:必须设置不同的 key
                key: ValueKey<int>(_count),
                style: const TextStyle(fontSize: 60),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => setState(() => _count++),
              child: const Text('增加'),
            ),
          ],
        ),
      ),
    );
  }
}

更多隐式动画 Widget

// 尺寸动画
AnimatedSize(
  duration: const Duration(milliseconds: 300),
  child: Container(
    width: _expanded ? 200 : 100,
    height: _expanded ? 200 : 100,
    color: Colors.blue,
  ),
)

// 内边距动画
AnimatedPadding(
  duration: const Duration(milliseconds: 300),
  padding: EdgeInsets.all(_expanded ? 50 : 10),
  child: Container(color: Colors.blue),
)

// 对齐动画
AnimatedAlign(
  duration: const Duration(milliseconds: 300),
  alignment: _alignLeft ? Alignment.centerLeft : Alignment.centerRight,
  child: Container(width: 50, height: 50, color: Colors.blue),
)

// 默认文字样式动画
AnimatedDefaultTextStyle(
  duration: const Duration(milliseconds: 300),
  style: TextStyle(
    fontSize: _large ? 30 : 16,
    color: _large ? Colors.red : Colors.black,
  ),
  child: const Text('Hello'),
)

// 主题动画
AnimatedTheme(
  duration: const Duration(milliseconds: 300),
  data: _dark ? ThemeData.dark() : ThemeData.light(),
  child: ...,
)

🎮 显式动画(完全控制)

显式动画提供更精细的控制,需要使用 AnimationController。

AnimationController 基础

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

  @override
  State<ExplicitAnimationDemo> createState() => _ExplicitAnimationDemoState();
}

class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    // 创建控制器
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,  // 需要 TickerProviderStateMixin
    );

    // 创建动画(使用 Tween)
    _animation = Tween<double>(begin: 0, end: 300).animate(_controller);

    // 监听动画值变化
    _animation.addListener(() {
      setState(() {});  // 触发重建
    });

    // 监听动画状态
    _animation.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('显式动画')),
      body: Center(
        child: Container(
          width: _animation.value,
          height: _animation.value,
          color: Colors.blue,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isAnimating) {
            _controller.stop();
          } else {
            _controller.forward();
          }
        },
        child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}

使用 AnimatedBuilder(推荐)

避免整个 Widget 重建,性能更好。

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

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

class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;

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

    _sizeAnimation = Tween<double>(begin: 50, end: 200).animate(
      CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
    );

    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @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: _sizeAnimation.value,
              height: _sizeAnimation.value,
              decoration: BoxDecoration(
                color: _colorAnimation.value,
                borderRadius: BorderRadius.circular(_sizeAnimation.value / 4),
              ),
              child: child,  // child 不会重建
            );
          },
          child: const Center(
            child: Text('不会重建', style: TextStyle(color: Colors.white)),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: const Icon(Icons.play_arrow),
      ),
    );
  }
}

Tween 动画

Tween 定义动画的起始值和结束值。

// 数值
Tween<double>(begin: 0, end: 100)

// 颜色
ColorTween(begin: Colors.blue, end: Colors.red)

// 偏移
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(1, 1))

// 边框
BorderRadiusTween(
  begin: BorderRadius.circular(0),
  end: BorderRadius.circular(50),
)

// 装饰
DecorationTween(
  begin: BoxDecoration(color: Colors.blue),
  end: BoxDecoration(color: Colors.red),
)

// 自定义(使用 chain)
Tween<double>(begin: 0, end: 1)
  .chain(CurveTween(curve: Curves.easeInOut))
  .animate(_controller)

CurvedAnimation(曲线动画)

使用不同的曲线控制动画节奏。

// 使用曲线
final curvedAnimation = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeInOut,        // 正向曲线
  reverseCurve: Curves.easeOut,   // 反向曲线
);

final animation = Tween<double>(begin: 0, end: 100).animate(curvedAnimation);

// 常用曲线
Curves.linear        // 线性
Curves.easeIn        // 慢入
Curves.easeOut       // 慢出
Curves.easeInOut     // 慢入慢出
Curves.bounceIn      // 弹跳入
Curves.bounceOut     // 弹跳出
Curves.elasticIn     // 弹性入
Curves.elasticOut    // 弹性出
Curves.fastOutSlowIn // 快出慢入(Material 推荐)

内置 Transition Widgets

Flutter 提供了一些内置的过渡动画 Widget:

// 位移动画
SlideTransition(
  position: Tween<Offset>(
    begin: const Offset(-1, 0),  // 从左边滑入
    end: Offset.zero,
  ).animate(_controller),
  child: Container(...),
)

// 缩放动画
ScaleTransition(
  scale: Tween<double>(begin: 0, end: 1).animate(_controller),
  child: Container(...),
)

// 旋转动画
RotationTransition(
  turns: Tween<double>(begin: 0, end: 2).animate(_controller),  // 旋转2圈
  child: Container(...),
)

// 透明度动画
FadeTransition(
  opacity: Tween<double>(begin: 0, end: 1).animate(_controller),
  child: Container(...),
)

// 大小动画
SizeTransition(
  sizeFactor: Tween<double>(begin: 0, end: 1).animate(_controller),
  child: Container(...),
)

// 装饰动画
DecoratedBoxTransition(
  decoration: DecorationTween(
    begin: const BoxDecoration(color: Colors.blue),
    end: const BoxDecoration(color: Colors.red),
  ).animate(_controller),
  child: Container(width: 100, height: 100),
)

组合多个动画

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

  @override
  State<MultiAnimationDemo> createState() => _MultiAnimationDemoState();
}

class _MultiAnimationDemoState extends State<MultiAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _rotationAnimation;
  late Animation<Offset> _slideAnimation;

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

    // 0-50% 时间:缩放
    _scaleAnimation = Tween<double>(begin: 0.5, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0, 0.5, curve: Curves.easeOut),
      ),
    );

    // 25-75% 时间:旋转
    _rotationAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.25, 0.75, curve: Curves.easeInOut),
      ),
    );

    // 50-100% 时间:滑动
    _slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(0.5, 0),
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.5, 1, curve: Curves.easeIn),
      ),
    );
  }

  @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 SlideTransition(
              position: _slideAnimation,
              child: RotationTransition(
                turns: _rotationAnimation,
                child: ScaleTransition(
                  scale: _scaleAnimation,
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.blue,
                  ),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: const Icon(Icons.play_arrow),
      ),
    );
  }
}

🦸 Hero 动画

Hero 动画用于页面间的共享元素过渡,最常见的场景是图片从列表到详情页的放大效果。

// 列表页
class HeroListPage extends StatelessWidget {
  const HeroListPage({super.key});

  final List<String> images = const [
    'https://picsum.photos/200/200?random=1',
    'https://picsum.photos/200/200?random=2',
    'https://picsum.photos/200/200?random=3',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero 动画')),
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
        ),
        itemCount: images.length,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => HeroDetailPage(
                    imageUrl: images[index],
                    tag: 'hero-$index',
                  ),
                ),
              );
            },
            child: Hero(
              tag: 'hero-$index',  // 两个页面的 tag 必须相同
              child: ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: Image.network(
                  images[index],
                  fit: BoxFit.cover,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

// 详情页
class HeroDetailPage extends StatelessWidget {
  final String imageUrl;
  final String tag;

  const HeroDetailPage({
    super.key,
    required this.imageUrl,
    required this.tag,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Center(
        child: Hero(
          tag: tag,  // 与列表页相同的 tag
          child: Image.network(
            imageUrl,
            fit: BoxFit.contain,
          ),
        ),
      ),
    );
  }
}

自定义 Hero 动画

Hero(
  tag: 'custom-hero',
  // 自定义飞行过程中的 Widget
  flightShuttleBuilder: (
    flightContext,
    animation,
    flightDirection,
    fromHeroContext,
    toHeroContext,
  ) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(
              (1 - animation.value) * 12,  // 圆角渐变
            ),
            boxShadow: [
              BoxShadow(
                color: Colors.black26,
                blurRadius: animation.value * 20,  // 阴影渐变
              ),
            ],
          ),
          child: ClipRRect(
            borderRadius: BorderRadius.circular((1 - animation.value) * 12),
            child: Image.network(imageUrl, fit: BoxFit.cover),
          ),
        );
      },
    );
  },
  child: Image.network(imageUrl),
)

🔄 页面转场动画

使用 PageRouteBuilder

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return const DetailPage();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 淡入淡出
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    },
    transitionDuration: const Duration(milliseconds: 300),
  ),
);

// 滑动进入
transitionsBuilder: (context, animation, secondaryAnimation, child) {
  final offsetAnimation = Tween<Offset>(
    begin: const Offset(1, 0),  // 从右边滑入
    end: Offset.zero,
  ).animate(CurvedAnimation(
    parent: animation,
    curve: Curves.easeOutCubic,
  ));
  
  return SlideTransition(
    position: offsetAnimation,
    child: child,
  );
}

// 缩放进入
transitionsBuilder: (context, animation, secondaryAnimation, child) {
  return ScaleTransition(
    scale: Tween<double>(begin: 0.8, end: 1).animate(
      CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
    ),
    child: FadeTransition(
      opacity: animation,
      child: child,
    ),
  );
}

封装通用转场动画

class FadePageRoute<T> extends PageRouteBuilder<T> {
  final Widget page;

  FadePageRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(opacity: animation, child: child);
          },
          transitionDuration: const Duration(milliseconds: 300),
        );
}

class SlidePageRoute<T> extends PageRouteBuilder<T> {
  final Widget page;
  final AxisDirection direction;

  SlidePageRoute({
    required this.page,
    this.direction = AxisDirection.right,
  }) : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            Offset begin;
            switch (direction) {
              case AxisDirection.up:
                begin = const Offset(0, 1);
                break;
              case AxisDirection.down:
                begin = const Offset(0, -1);
                break;
              case AxisDirection.left:
                begin = const Offset(1, 0);
                break;
              case AxisDirection.right:
                begin = const Offset(-1, 0);
                break;
            }
            return SlideTransition(
              position: Tween<Offset>(begin: begin, end: Offset.zero).animate(
                CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
              ),
              child: child,
            );
          },
          transitionDuration: const Duration(milliseconds: 300),
        );
}

// 使用
Navigator.push(context, FadePageRoute(page: const DetailPage()));
Navigator.push(context, SlidePageRoute(page: const DetailPage(), direction: AxisDirection.up));

🎨 Lottie 动画

Lottie 可以播放 AE 导出的 JSON 动画文件,效果精美。

添加依赖

dependencies:
  lottie: ^3.1.0

基本用法

import 'package:lottie/lottie.dart';

// 从 assets 加载
Lottie.asset(
  'assets/animations/loading.json',
  width: 200,
  height: 200,
)

// 从网络加载
Lottie.network(
  'https://assets.lottiefiles.com/packages/xxx.json',
)

// 循环播放
Lottie.asset(
  'assets/animations/loading.json',
  repeat: true,
)

// 单次播放
Lottie.asset(
  'assets/animations/success.json',
  repeat: false,
)

控制 Lottie 动画

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

  @override
  State<LottieControlDemo> createState() => _LottieControlDemoState();
}

class _LottieControlDemoState extends State<LottieControlDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Lottie 控制')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Lottie.asset(
              'assets/animations/heart.json',
              controller: _controller,
              onLoaded: (composition) {
                _controller.duration = composition.duration;
              },
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  icon: const Icon(Icons.play_arrow),
                  onPressed: () => _controller.forward(),
                ),
                IconButton(
                  icon: const Icon(Icons.pause),
                  onPressed: () => _controller.stop(),
                ),
                IconButton(
                  icon: const Icon(Icons.replay),
                  onPressed: () {
                    _controller.reset();
                    _controller.forward();
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.repeat),
                  onPressed: () => _controller.repeat(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

获取 Lottie 动画资源:

  • LottieFiles - 免费动画库
  • Icons8 Animated Icons

🎯 实战:加载动画组件

// lib/widgets/loading_widget.dart
import 'package:flutter/material.dart';

class LoadingWidget extends StatefulWidget {
  final double size;
  final Color color;

  const LoadingWidget({
    super.key,
    this.size = 50,
    this.color = Colors.blue,
  });

  @override
  State<LoadingWidget> createState() => _LoadingWidgetState();
}

class _LoadingWidgetState extends State<LoadingWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1500),
      vsync: this,
    )..repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.size,
      height: widget.size,
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Stack(
            alignment: Alignment.center,
            children: List.generate(3, (index) {
              final delay = index * 0.2;
              final animation = Tween<double>(begin: 0, end: 1).animate(
                CurvedAnimation(
                  parent: _controller,
                  curve: Interval(delay, delay + 0.6, curve: Curves.easeInOut),
                ),
              );
              return Transform.scale(
                scale: animation.value,
                child: Container(
                  width: widget.size,
                  height: widget.size,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: widget.color.withOpacity(1 - animation.value),
                  ),
                ),
              );
            }),
          );
        },
      ),
    );
  }
}

// 使用
const LoadingWidget()
LoadingWidget(size: 80, color: Colors.red)

📝 小结

这一章我们学习了 Flutter 动画:

隐式动画(简单场景)

  • AnimatedContainer - 容器属性动画
  • AnimatedOpacity - 透明度动画
  • AnimatedPositioned - 位置动画
  • AnimatedCrossFade - 切换动画

显式动画(复杂场景)

  • AnimationController - 动画控制器
  • Tween - 定义动画值范围
  • CurvedAnimation - 曲线动画
  • AnimatedBuilder - 高效重建

特殊动画

  • Hero - 页面间共享元素
  • PageRouteBuilder - 页面转场
  • Lottie - AE 动画

动画使用建议

  1. 简单场景用隐式动画 - 代码少,易维护
  2. 复杂场景用显式动画 - 完全控制
  3. 记得释放 Controller - 在 dispose 中调用
  4. 使用 AnimatedBuilder - 避免不必要的重建
  5. 选择合适的曲线 - 让动画更自然

💪 练习题

  1. 使用隐式动画实现一个点赞按钮(点击放大变色)
  2. 使用显式动画实现一个旋转的加载指示器
  3. 实现一个带 Hero 动画的图片列表

🚀 下一步

学会了动画后,下一章我们来学习 调试与性能优化!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
第15章 - 状态管理进阶
Next
第17章 - 常用第三方包