Flutter 动画实战:基础动画 + Hero 动画 + 自定义动画

导语:动画是提升 Flutter 应用交互体验的核心手段,流畅的动画能让界面过渡更自然、操作反馈更直观。Flutter 提供了分层的动画 API 体系,从无需手动管理控制器的基础组件(如 AnimatedContainer),到跨页面的共享元素动画(Hero),再到灵活可控的自定义动画(AnimationController),满足不同场景需求。本文通过实战案例手把手教你实现各类动画效果,包含完整可运行代码、核心原理解析及性能优化技巧,让你快速掌握 Flutter 动画开发精髓!

一、核心概念铺垫

在动手实现前,先明确 Flutter 动画的核心基础,避免踩坑:

概念 作用 适用场景
动画组件(AnimatedXXX) 封装好的高阶组件,修改属性自动触发动画 简单属性过渡(尺寸、颜色、透明度等)
Hero 动画 跨页面共享元素的过渡动画 列表页→详情页(图片、卡片等共享元素)
AnimationController 动画核心控制器,管理播放、暂停、时长等 复杂自定义动画(多效果组合、进度控制)
Animation 存储动画值(如 0→1、0→2π),提供插值计算 所有自定义动画场景
AnimatedBuilder 构建动画 UI,仅重建动画相关部分 性能优化,避免整树重建
Curve 动画曲线,控制动画速度变化 调整动画节奏(匀速、加速减速等)

💡 关键原则:优先使用内置动画组件(开发效率高、性能有保障),复杂场景再用自定义控制器。

二、基础动画:AnimatedContainer(零控制器上手)

核心特性

  • 无需手动管理 AnimationController,修改组件属性自动触发过渡动画
  • 支持多属性同时动画:width、height、color、padding、borderRadius、transform 等
  • 核心配置:duration(动画时长)、curve(动画曲线)、onEnd(动画结束回调)

实战案例:点击切换卡片状态(尺寸 + 颜色 + 圆角)

实现点击卡片后,同时发生尺寸放大、颜色切换、圆角变化的组合动画:

dart

复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const AnimatedContainerApp());
}

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

  @override
  State<AnimatedContainerApp> createState() => _AnimatedContainerAppState();
}

class _AnimatedContainerAppState extends State<AnimatedContainerApp> {
  bool _isExpanded = false; // 控制动画状态的开关

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(title: const Text('AnimatedContainer 实战')),
        body: Center(
          child: GestureDetector(
            // 点击触发状态切换
            onTap: () {
              setState(() {
                _isExpanded = !_isExpanded;
              });
            },
            // 核心动画组件
            child: AnimatedContainer(
              // 动画目标属性:尺寸
              width: _isExpanded ? 300 : 150,
              height: _isExpanded ? 300 : 150,
              // 动画目标属性:颜色
              color: _isExpanded ? Colors.blueAccent : Colors.redAccent,
              // 动画目标属性:圆角
              borderRadius: _isExpanded 
                  ? BorderRadius.circular(50) 
                  : BorderRadius.circular(10),
              // 动画目标属性:内边距(新增效果)
              padding: _isExpanded 
                  ? const EdgeInsets.all(30) 
                  : const EdgeInsets.all(10),
              // 动画时长(必须配置)
              duration: const Duration(seconds: 1),
              // 动画曲线(控制速度变化)
              curve: Curves.easeInOut,
              // 动画结束回调(可选)
              onEnd: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(_isExpanded ? '已展开' : '已收缩')),
                );
              },
              child: const Center(
                child: Text(
                  '点击切换',
                  style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

关键说明

  • 动画触发逻辑:通过 setState 修改 _isExpanded 状态,AnimatedContainer 自动检测属性变化并执行过渡

  • 多属性同步:支持同时对多个属性设置动画,无需额外处理同步问题

  • 扩展技巧:可通过 transform 属性添加平移、旋转效果,例如:

    dart

    复制代码
    transform: _isExpanded 
        ? Matrix4.translationValues(0, -50, 0) // 上移50
        : Matrix4.identity(),

三、页面过渡动画:Hero(共享元素无缝跳转)

核心特性

  • 跨页面共享同一个元素的过渡动画,实现「列表页→详情页」的无缝衔接
  • 核心要求:两个页面的共享元素必须设置相同的 tag(唯一标识)
  • 自动处理:Flutter 自动计算元素在两个页面的位置、尺寸变化,生成过渡动画

实战案例:图片列表→详情页过渡

1. 列表页(共享元素源头)

dart

复制代码
class HeroListPage extends StatelessWidget {
  const HeroListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero 动画列表')),
      body: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(10),
        crossAxisSpacing: 10,
        mainAxisSpacing: 10,
        children: List.generate(6, (index) {
          // 每个图片项都是 Hero 动画元素
          return GestureDetector(
            onTap: () {
              // 跳转到详情页,携带图片URL参数
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => HeroDetailPage(
                    imageUrl: 'https://picsum.photos/200/200?random=$index',
                    heroTag: 'image_$index', // 唯一tag,与详情页一致
                  ),
                ),
              );
            },
            child: Hero(
              tag: 'image_$index', // 唯一标识,必须全局唯一
              // 优化:添加过渡动画的形状裁剪
              child: ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(
                  'https://picsum.photos/200/200?random=$index',
                  fit: BoxFit.cover,
                  width: double.infinity,
                  height: double.infinity,
                  // 加载占位
                  loadingBuilder: (context, child, loadingProgress) {
                    if (loadingProgress == null) return child;
                    return const Center(child: CircularProgressIndicator(strokeWidth: 2));
                  },
                ),
              ),
            ),
          );
        }),
      ),
    );
  }
}
2. 详情页(共享元素目标)

dart

复制代码
class HeroDetailPage extends StatelessWidget {
  final String imageUrl;
  final String heroTag; // 接收列表页传递的tag

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      // 点击空白处返回
      body: GestureDetector(
        onTap: () => Navigator.pop(context),
        child: Center(
          child: Hero(
            tag: heroTag, // 与列表页完全一致
            // 优化:添加过渡动画的淡入效果
            child: FadeInImage(
              placeholder: const AssetImage('assets/loading.png'), // 本地占位图(需提前配置)
              image: NetworkImage(imageUrl.replaceAll('200/200', '800/800')), // 高清图
              fit: BoxFit.contain,
              width: double.infinity,
              height: double.infinity,
            ),
          ),
        ),
      ),
    );
  }
}

避坑指南

  • tag 必须全局唯一:如果列表有多个元素,不能使用固定字符串(如示例中用 image_$index 区分)
  • 避免嵌套 Hero:共享元素内部不能再包含 Hero 组件,否则会导致动画异常
  • 占位图优化:网络图片需添加加载占位,避免动画过程中出现空白

四、自定义动画:AnimationController(灵活控制动画进度)

核心概念

  • AnimationController:动画的「总开关」,控制播放、暂停、反向、重复,提供 0.0→1.0 的线性值
  • Animation :通过 Tween(插值器)将 0.0→1.0 映射为实际需要的值(如 0→2π、0.5→1.0)
  • AnimatedBuilder:高效构建动画 UI,仅重建动画相关部分,避免整树重绘
  • SingleTickerProviderStateMixin:提供动画帧回调,用于驱动 AnimationController

实战案例:旋转 + 缩放 + 透明度组合动画

实现一个无限循环的组合动画:旋转 360° + 缩放(0.5→1.0)+ 透明度(0.3→1.0)

dart

复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const CustomAnimationApp());
}

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

  @override
  State<CustomAnimationApp> createState() => _CustomAnimationAppState();
}

class _CustomAnimationAppState extends State<CustomAnimationApp>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller; // 动画控制器
  late Animation<double> _rotationAnim; // 旋转动画
  late Animation<double> _scaleAnim; // 缩放动画
  late Animation<double> _opacityAnim; // 透明度动画

  @override
  void initState() {
    super.initState();
    // 1. 初始化控制器:时长2秒,绑定当前页面的帧回调
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
      lowerBound: 0.0, // 最小值
      upperBound: 1.0, // 最大值
    )..repeat(reverse: true); // 循环播放,反向重复(1.0→0.0→1.0)

    // 2. 配置旋转动画:0→2π(360°)
    _rotationAnim = Tween<double>(begin: 0, end: 2 * 3.1415)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.linear));

    // 3. 配置缩放动画:0.5→1.0
    _scaleAnim = Tween<double>(begin: 0.5, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));

    // 4. 配置透明度动画:0.3→1.0
    _opacityAnim = Tween<double>(begin: 0.3, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('自定义组合动画实战')),
        body: Center(
          // AnimatedBuilder:仅重建builder内部的Widget
          child: AnimatedBuilder(
            animation: _controller, // 绑定控制器
            builder: (context, child) {
              // 组合变换:旋转 + 缩放 + 透明度
              return Opacity(
                opacity: _opacityAnim.value,
                child: Transform(
                  transform: Matrix4.identity()
                    ..rotateZ(_rotationAnim.value) // 旋转
                    ..scale(_scaleAnim.value), // 缩放
                  alignment: Alignment.center, // 变换中心点
                  child: child, // 复用子Widget,减少重建
                ),
              );
            },
            // 静态子Widget:不会随动画重建
            child: Container(
              width: 200,
              height: 200,
              decoration: BoxDecoration(
                color: Colors.purpleAccent,
                borderRadius: BorderRadius.circular(20),
                boxShadow: const [
                  BoxShadow(
                    color: Colors.purple.withOpacity(0.3),
                    blurRadius: 10,
                    offset: Offset(0, 5),
                  )
                ],
              ),
              child: const Center(
                child: Text(
                  '旋转缩放\n透明度变化',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

进阶技巧

  • 动画控制:通过 _controller.play()(播放)、_controller.pause()(暂停)、_controller.reverse()(反向)控制动画状态
  • 进度监听:通过 _controller.addListener(() {}) 监听动画进度,实现自定义逻辑(如进度条同步)
  • 多曲线组合:不同动画可配置不同的 Curve,实现更丰富的节奏变化

五、动画性能优化指南

优化点 实现方式 效果
减少重建范围 使用 AnimatedBuilder 包裹动画部分,将静态 Widget 作为 child 参数传入 仅动画相关部分重建,避免整树重绘
隔离重绘区域 用 RepaintBoundary 包裹动画 Widget,创建独立的重绘层 避免动画重绘影响其他区域
优先使用内置组件 优先选择 AnimatedContainer、AnimatedOpacity 等内置组件,而非自定义 AnimationController 内置组件已做性能优化,开发效率更高
避免复杂计算 不在 AnimatedBuilder 的 builder 方法中执行复杂逻辑(如循环、网络请求) 减少每帧耗时,避免动画卡顿
控制动画帧率 复杂动画可通过 AnimationControllerframeRate 参数限制帧率(如 30fps) 降低性能消耗,适配低端设备
复用动画对象 将 Animation 对象缓存到成员变量,而非在 builder 中重复创建 减少对象创建销毁开销

六、常见问题排查

  1. 动画不生效 :检查是否忘记设置 duration(基础动画)或未调用 _controller.forward()(自定义动画)
  2. 动画卡顿:排查是否在 builder 中做了复杂计算,或未使用 AnimatedBuilder/RepaintBoundary
  3. Hero 动画异常 :确认两个页面的 tag 完全一致,且共享元素的父布局没有影响位置的动态变化
  4. 内存泄漏 :自定义动画必须在 dispose 中调用 _controller.dispose()

七、结语

Flutter 动画系统的设计理念是「分层抽象」,让开发者可以根据需求选择合适的 API :简单场景用内置动画组件快速实现,复杂场景用 AnimationController 灵活定制。本文通过三个核心实战案例,覆盖了大部分日常开发场景,结合性能优化技巧和避坑指南,帮助你在项目中快速落地高质量动画。建议在实际开发中多尝试组合不同动画效果,同时关注动画的性能表现,让应用既美观又流畅!

相关推荐
松☆2 小时前
Flutter + OpenHarmony 实战:构建离线优先的跨设备笔记应用
笔记·flutter
_大学牲4 小时前
Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生
flutter·游戏·游戏开发
kirk_wang5 小时前
Flutter插件在鸿蒙端的开发与部署:跨生态桥梁的架构与实现
flutter·移动开发·跨平台·arkts·鸿蒙
勇气要爆发8 小时前
【第五阶段—高级特性和框架】复杂动画案例分析初体验
flutter
勤劳打代码9 小时前
追本溯源 —— SetState 刷新做了什么
flutter·面试·性能优化
松☆10 小时前
OpenHarmony 后台任务与 Flutter 生命周期协调:构建稳定可靠的混合应用
flutter
松☆11 小时前
Flutter 与 OpenHarmony 深度集成:自定义 MethodChannel 插件开发全指南
flutter·wpf
克喵的水银蛇11 小时前
Flutter 布局实战:掌握 Row/Column/Flex 弹性布局
前端·javascript·flutter