Flutter for OpenHarmony:三方库实战 animations 页面过渡动画详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🎯 前言:为什么需要页面过渡动画?

在移动应用开发中,页面切换动画是提升用户体验的重要手段。

实际痛点

  • 📱 生硬的页面跳转:默认的页面切换缺乏过渡,用户体验差
  • 🎨 缺乏连贯性:用户不清楚新页面与旧页面的关系
  • 视觉断层:突然的内容变化让用户感到不适
  • 🎭 缺少品质感:简单的切换无法体现应用的专业性
  • 🔄 元素跳跃:相同元素在不同页面间位置突变

实际场景需求

  • 场景一:电商应用中,商品卡片平滑展开成详情页
  • 场景二:社交应用中,头像点击后放大显示个人资料
  • 场景三:新闻应用中,文章列表到详情的流畅过渡
  • 场景四:相册应用中,缩略图到大图的自然转换
  • 场景五:设置页面中,选项卡之间的平滑切换

animations 是解决这些问题的完美方案!它提供了:

  • 🎬 容器转换动画:元素平滑展开/收缩
  • 🔄 共享轴过渡:页面沿轴线滑动切换
  • 💫 淡入淡出:优雅的透明度过渡
  • 🎯 Material Design 规范:符合设计规范的动画
  • 高性能:优化的动画性能
  • 🎨 可定制:支持自定义动画参数

🚀 核心能力一览

功能特性 详细说明 OpenHarmony 支持
容器转换 OpenContainer 动画
共享轴过渡 SharedAxisTransition
淡入淡出 FadeThroughTransition
淡出动画 FadeScaleTransition
自定义参数 动画时长、曲线等
模拟器测试 完美支持模拟器
跨平台一致 所有平台行为一致

动画类型说明

动画类型 说明 适用场景
OpenContainer 容器转换动画 列表到详情、卡片展开
SharedAxisTransition 共享轴过渡 标签页切换、步骤导航
FadeThroughTransition 淡入淡出过渡 内容替换、状态切换
FadeScaleTransition 淡出缩放 对话框、弹窗

⚙️ 环境准备

第一步:添加依赖

📄 pubspec.yaml

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  
  # 添加 animations 依赖(OpenHarmony 适配版本)
  animations:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages.git
      path: packages/animations
      ref: br_animations-v2.0.11_ohos

执行命令:

bash 复制代码
flutter pub get

第二步:导入包

在 Dart 文件中导入:

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

📚 基础用法:核心功能

示例1:OpenContainer 容器转换动画

最常用的动画,实现列表到详情的平滑过渡:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('容器转换动画'),
      ),
      body: ListView.builder(
        itemCount: 10,
        padding: const EdgeInsets.all(16),
        itemBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.only(bottom: 16),
            child: OpenContainer(
              closedElevation: 2,
              closedShape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              closedColor: Colors.blue.shade50,
              openColor: Colors.white,
              transitionDuration: const Duration(milliseconds: 500),
              closedBuilder: (context, action) {
                // 关闭状态:列表项
                return ListTile(
                  leading: CircleAvatar(
                    backgroundColor: Colors.blue,
                    child: Text('${index + 1}'),
                  ),
                  title: Text('商品 ${index + 1}'),
                  subtitle: const Text('点击查看详情'),
                  trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                );
              },
              openBuilder: (context, action) {
                // 打开状态:详情页
                return DetailPage(index: index);
              },
            ),
          );
        },
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final int index;
  
  const DetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('商品 ${index + 1} 详情'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.shopping_bag,
              size: 100,
              color: Colors.blue,
            ),
            const SizedBox(height: 20),
            Text(
              '商品 ${index + 1}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            const Text('这是商品的详细信息'),
          ],
        ),
      ),
    );
  }
}

核心要点

  • closedBuilder:关闭状态的UI(列表项)
  • openBuilder:打开状态的UI(详情页)
  • transitionDuration:动画时长
  • 自动处理打开/关闭动画

示例2:SharedAxisTransition 共享轴过渡

适用于标签页切换、步骤导航:

dart 复制代码
class SharedAxisDemo extends StatefulWidget {
  const SharedAxisDemo({super.key});

  @override
  State<SharedAxisDemo> createState() => _SharedAxisDemoState();
}

class _SharedAxisDemoState extends State<SharedAxisDemo> {
  int _currentIndex = 0;
  
  final List<String> _pages = ['首页', '分类', '购物车', '我的'];
  final List<IconData> _icons = [
    Icons.home,
    Icons.category,
    Icons.shopping_cart,
    Icons.person,
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('共享轴过渡'),
      ),
      body: PageTransitionSwitcher(
        duration: const Duration(milliseconds: 300),
        transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
          return SharedAxisTransition(
            animation: primaryAnimation,
            secondaryAnimation: secondaryAnimation,
            transitionType: SharedAxisTransitionType.horizontal,
            child: child,
          );
        },
        child: _buildPage(_currentIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        type: BottomNavigationBarType.fixed,
        items: List.generate(
          _pages.length,
          (index) => BottomNavigationBarItem(
            icon: Icon(_icons[index]),
            label: _pages[index],
          ),
        ),
      ),
    );
  }

  Widget _buildPage(int index) {
    return Container(
      key: ValueKey(index),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(_icons[index], size: 100, color: Colors.blue),
            const SizedBox(height: 20),
            Text(
              _pages[index],
              style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

SharedAxisTransitionType 类型

  • horizontal:水平滑动
  • vertical:垂直滑动
  • scaled:缩放过渡

示例3:FadeThroughTransition 淡入淡出过渡

适用于内容替换、状态切换:

dart 复制代码
class FadeThroughDemo extends StatefulWidget {
  const FadeThroughDemo({super.key});

  @override
  State<FadeThroughDemo> createState() => _FadeThroughDemoState();
}

class _FadeThroughDemoState extends State<FadeThroughDemo> {
  int _selectedIndex = 0;
  
  final List<String> _items = ['图片', '视频', '音乐', '文档'];
  final List<IconData> _icons = [
    Icons.image,
    Icons.video_library,
    Icons.music_note,
    Icons.description,
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('淡入淡出过渡'),
      ),
      body: Column(
        children: [
          // 选项卡
          Container(
            height: 60,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: _items.length,
              padding: const EdgeInsets.all(8),
              itemBuilder: (context, index) {
                final isSelected = index == _selectedIndex;
                return Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 4),
                  child: ChoiceChip(
                    label: Text(_items[index]),
                    selected: isSelected,
                    onSelected: (selected) {
                      if (selected) {
                        setState(() {
                          _selectedIndex = index;
                        });
                      }
                    },
                  ),
                );
              },
            ),
          ),
          
          // 内容区域
          Expanded(
            child: PageTransitionSwitcher(
              duration: const Duration(milliseconds: 300),
              transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
                return FadeThroughTransition(
                  animation: primaryAnimation,
                  secondaryAnimation: secondaryAnimation,
                  child: child,
                );
              },
              child: _buildContent(_selectedIndex),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildContent(int index) {
    return Container(
      key: ValueKey(index),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(_icons[index], size: 120, color: Colors.blue),
            const SizedBox(height: 20),
            Text(
              _items[index],
              style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            Text(
              '这是${_items[index]}的内容区域',
              style: const TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

示例4:FadeScaleTransition 淡出缩放

适用于对话框、弹窗:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('淡出缩放'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            showModal(
              context: context,
              configuration: const FadeScaleTransitionConfiguration(),
              builder: (context) {
                return AlertDialog(
                  title: const Text('提示'),
                  content: const Text('这是一个使用淡出缩放动画的对话框'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('取消'),
                    ),
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('确定'),
                    ),
                  ],
                );
              },
            );
          },
          child: const Text('显示对话框'),
        ),
      ),
    );
  }
}

示例5:自定义动画参数

自定义动画时长、曲线等参数:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('自定义动画参数'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // 快速动画
          _buildAnimationCard(
            context,
            title: '快速动画',
            subtitle: '200ms',
            duration: const Duration(milliseconds: 200),
            color: Colors.red,
          ),
          
          // 标准动画
          _buildAnimationCard(
            context,
            title: '标准动画',
            subtitle: '300ms',
            duration: const Duration(milliseconds: 300),
            color: Colors.blue,
          ),
          
          // 慢速动画
          _buildAnimationCard(
            context,
            title: '慢速动画',
            subtitle: '500ms',
            duration: const Duration(milliseconds: 500),
            color: Colors.green,
          ),
        ],
      ),
    );
  }

  Widget _buildAnimationCard(
    BuildContext context, {
    required String title,
    required String subtitle,
    required Duration duration,
    required Color color,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: OpenContainer(
        transitionDuration: duration,
        closedElevation: 2,
        closedShape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        closedColor: color.withOpacity(0.1),
        openColor: Colors.white,
        closedBuilder: (context, action) {
          return ListTile(
            leading: CircleAvatar(
              backgroundColor: color,
              child: const Icon(Icons.timer, color: Colors.white),
            ),
            title: Text(title),
            subtitle: Text(subtitle),
            trailing: const Icon(Icons.arrow_forward_ios, size: 16),
          );
        },
        openBuilder: (context, action) {
          return Scaffold(
            appBar: AppBar(
              title: Text(title),
              backgroundColor: color,
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.timer, size: 100, color: color),
                  const SizedBox(height: 20),
                  Text(
                    title,
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 10),
                  Text('动画时长: $subtitle'),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

🎯 完整示例:动画展示应用

下面是一个功能完整的动画展示应用,展示了所有核心功能:

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Animations 展示',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const AnimationsHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animations 动画展示'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildDemoCard(
            context,
            title: '容器转换',
            subtitle: 'OpenContainer',
            icon: Icons.open_in_new,
            color: Colors.blue,
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const ContainerTransformDemo(),
                ),
              );
            },
          ),
          
          _buildDemoCard(
            context,
            title: '共享轴过渡',
            subtitle: 'SharedAxisTransition',
            icon: Icons.swap_horiz,
            color: Colors.green,
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const SharedAxisDemo(),
                ),
              );
            },
          ),
          
          _buildDemoCard(
            context,
            title: '淡入淡出',
            subtitle: 'FadeThroughTransition',
            icon: Icons.blur_on,
            color: Colors.orange,
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const FadeThroughDemo(),
                ),
              );
            },
          ),
          
          _buildDemoCard(
            context,
            title: '淡出缩放',
            subtitle: 'FadeScaleTransition',
            icon: Icons.zoom_out,
            color: Colors.purple,
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const FadeScaleDemo(),
                ),
              );
            },
          ),
          
          _buildDemoCard(
            context,
            title: '自定义参数',
            subtitle: '动画时长、曲线',
            icon: Icons.tune,
            color: Colors.teal,
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const CustomAnimationDemo(),
                ),
              );
            },
          ),
        ],
      ),
    );
  }

  Widget _buildDemoCard(
    BuildContext context, {
    required String title,
    required String subtitle,
    required IconData icon,
    required Color color,
    required VoidCallback onTap,
  }) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      elevation: 2,
      child: ListTile(
        contentPadding: const EdgeInsets.all(16),
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: color.withOpacity(0.2),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Icon(icon, color: color, size: 28),
        ),
        title: Text(
          title,
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.arrow_forward_ios, size: 16),
        onTap: onTap,
      ),
    );
  }
}

// ContainerTransformDemo 类(见示例1)
class ContainerTransformDemo extends StatelessWidget {
  const ContainerTransformDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('容器转换动画'),
      ),
      body: ListView.builder(
        itemCount: 10,
        padding: const EdgeInsets.all(16),
        itemBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.only(bottom: 16),
            child: OpenContainer(
              closedElevation: 2,
              closedShape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              closedColor: Colors.blue.shade50,
              openColor: Colors.white,
              transitionDuration: const Duration(milliseconds: 500),
              closedBuilder: (context, action) {
                return ListTile(
                  leading: CircleAvatar(
                    backgroundColor: Colors.blue,
                    child: Text('${index + 1}'),
                  ),
                  title: Text('商品 ${index + 1}'),
                  subtitle: const Text('点击查看详情'),
                  trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                );
              },
              openBuilder: (context, action) {
                return DetailPage(index: index);
              },
            ),
          );
        },
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final int index;
  
  const DetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('商品 ${index + 1} 详情'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(
              Icons.shopping_bag,
              size: 100,
              color: Colors.blue,
            ),
            const SizedBox(height: 20),
            Text(
              '商品 ${index + 1}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            const Text('这是商品的详细信息'),
          ],
        ),
      ),
    );
  }
}

// SharedAxisDemo 类
class SharedAxisDemo extends StatefulWidget {
  const SharedAxisDemo({super.key});

  @override
  State<SharedAxisDemo> createState() => _SharedAxisDemoState();
}

class _SharedAxisDemoState extends State<SharedAxisDemo> {
  int _currentIndex = 0;
  
  final List<String> _pages = ['首页', '分类', '购物车', '我的'];
  final List<IconData> _icons = [
    Icons.home,
    Icons.category,
    Icons.shopping_cart,
    Icons.person,
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('共享轴过渡'),
      ),
      body: PageTransitionSwitcher(
        duration: const Duration(milliseconds: 300),
        transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
          return SharedAxisTransition(
            animation: primaryAnimation,
            secondaryAnimation: secondaryAnimation,
            transitionType: SharedAxisTransitionType.horizontal,
            child: child,
          );
        },
        child: _buildPage(_currentIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        type: BottomNavigationBarType.fixed,
        items: List.generate(
          _pages.length,
          (index) => BottomNavigationBarItem(
            icon: Icon(_icons[index]),
            label: _pages[index],
          ),
        ),
      ),
    );
  }

  Widget _buildPage(int index) {
    return Container(
      key: ValueKey(index),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(_icons[index], size: 100, color: Colors.blue),
            const SizedBox(height: 20),
            Text(
              _pages[index],
              style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

// FadeThroughDemo 类
class FadeThroughDemo extends StatefulWidget {
  const FadeThroughDemo({super.key});

  @override
  State<FadeThroughDemo> createState() => _FadeThroughDemoState();
}

class _FadeThroughDemoState extends State<FadeThroughDemo> {
  int _selectedIndex = 0;
  
  final List<String> _items = ['图片', '视频', '音乐', '文档'];
  final List<IconData> _icons = [
    Icons.image,
    Icons.video_library,
    Icons.music_note,
    Icons.description,
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('淡入淡出过渡'),
      ),
      body: Column(
        children: [
          // 选项卡
          SizedBox(
            height: 60,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: _items.length,
              padding: const EdgeInsets.all(8),
              itemBuilder: (context, index) {
                final isSelected = index == _selectedIndex;
                return Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 4),
                  child: ChoiceChip(
                    label: Text(_items[index]),
                    selected: isSelected,
                    onSelected: (selected) {
                      if (selected) {
                        setState(() {
                          _selectedIndex = index;
                        });
                      }
                    },
                  ),
                );
              },
            ),
          ),
          
          // 内容区域
          Expanded(
            child: PageTransitionSwitcher(
              duration: const Duration(milliseconds: 300),
              transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
                return FadeThroughTransition(
                  animation: primaryAnimation,
                  secondaryAnimation: secondaryAnimation,
                  child: child,
                );
              },
              child: _buildContent(_selectedIndex),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildContent(int index) {
    return Container(
      key: ValueKey(index),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(_icons[index], size: 120, color: Colors.blue),
            const SizedBox(height: 20),
            Text(
              _items[index],
              style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            Text(
              '这是${_items[index]}的内容区域',
              style: const TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

// FadeScaleDemo 类
class FadeScaleDemo extends StatelessWidget {
  const FadeScaleDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('淡出缩放'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            showModal(
              context: context,
              configuration: const FadeScaleTransitionConfiguration(),
              builder: (context) {
                return AlertDialog(
                  title: const Text('提示'),
                  content: const Text('这是一个使用淡出缩放动画的对话框'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('取消'),
                    ),
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('确定'),
                    ),
                  ],
                );
              },
            );
          },
          child: const Text('显示对话框'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('自定义动画参数'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // 快速动画
          _buildAnimationCard(
            context,
            title: '快速动画',
            subtitle: '200ms',
            duration: const Duration(milliseconds: 200),
            color: Colors.red,
          ),
          
          // 标准动画
          _buildAnimationCard(
            context,
            title: '标准动画',
            subtitle: '300ms',
            duration: const Duration(milliseconds: 300),
            color: Colors.blue,
          ),
          
          // 慢速动画
          _buildAnimationCard(
            context,
            title: '慢速动画',
            subtitle: '500ms',
            duration: const Duration(milliseconds: 500),
            color: Colors.green,
          ),
        ],
      ),
    );
  }

  Widget _buildAnimationCard(
    BuildContext context, {
    required String title,
    required String subtitle,
    required Duration duration,
    required Color color,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: OpenContainer(
        transitionDuration: duration,
        closedElevation: 2,
        closedShape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        closedColor: color.withOpacity(0.1),
        openColor: Colors.white,
        closedBuilder: (context, action) {
          return ListTile(
            leading: CircleAvatar(
              backgroundColor: color,
              child: const Icon(Icons.timer, color: Colors.white),
            ),
            title: Text(title),
            subtitle: Text(subtitle),
            trailing: const Icon(Icons.arrow_forward_ios, size: 16),
          );
        },
        openBuilder: (context, action) {
          return Scaffold(
            appBar: AppBar(
              title: Text(title),
              backgroundColor: color,
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.timer, size: 100, color: color),
                  const SizedBox(height: 20),
                  Text(
                    title,
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 10),
                  Text('动画时长: $subtitle'),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

完整示例说明

  1. 主页导航:展示所有动画类型
  2. 容器转换:列表到详情的平滑过渡
  3. 共享轴过渡:标签页之间的滑动切换
  4. 淡入淡出:内容替换的优雅过渡
  5. 淡出缩放:对话框的弹出动画
  6. 自定义参数:不同时长的动画对比

📊 API 详解

OpenContainer 核心参数

dart 复制代码
OpenContainer({
  required OpenContainerBuilder closedBuilder,  // 关闭状态构建器
  required OpenContainerBuilder openBuilder,    // 打开状态构建器
  bool tappable = true,                         // 是否可点击
  Duration transitionDuration = const Duration(milliseconds: 300),  // 动画时长
  Color closedColor = Colors.white,             // 关闭状态颜色
  Color openColor = Colors.white,               // 打开状态颜色
  double closedElevation = 1.0,                 // 关闭状态阴影
  double openElevation = 4.0,                   // 打开状态阴影
  ShapeBorder? closedShape,                     // 关闭状态形状
  ShapeBorder? openShape,                       // 打开状态形状
  Clip clipBehavior = Clip.antiAlias,           // 裁剪行为
})

SharedAxisTransition 核心参数

dart 复制代码
SharedAxisTransition({
  required Animation<double> animation,          // 主动画
  required Animation<double> secondaryAnimation, // 次动画
  required SharedAxisTransitionType transitionType,  // 过渡类型
  required Widget child,                         // 子组件
  bool fillColor = true,                         // 是否填充颜色
})

SharedAxisTransitionType 类型

  • horizontal:水平滑动
  • vertical:垂直滑动
  • scaled:缩放过渡

FadeThroughTransition 核心参数

dart 复制代码
FadeThroughTransition({
  required Animation<double> animation,          // 主动画
  required Animation<double> secondaryAnimation, // 次动画
  required Widget child,                         // 子组件
  bool fillColor = true,                         // 是否填充颜色
})

PageTransitionSwitcher 核心参数

dart 复制代码
PageTransitionSwitcher({
  required Widget child,                         // 子组件
  required PageTransitionSwitcherTransitionBuilder transitionBuilder,  // 过渡构建器
  Duration duration = const Duration(milliseconds: 300),  // 动画时长
  Duration reverseDuration = const Duration(milliseconds: 300),  // 反向时长
})

💡 最佳实践

1. 选择合适的动画类型

dart 复制代码
// ✅ 列表到详情:使用 OpenContainer
OpenContainer(
  closedBuilder: (context, action) => ListItem(),
  openBuilder: (context, action) => DetailPage(),
)

// ✅ 标签页切换:使用 SharedAxisTransition
SharedAxisTransition(
  transitionType: SharedAxisTransitionType.horizontal,
  // ...
)

// ✅ 内容替换:使用 FadeThroughTransition
FadeThroughTransition(
  // ...
)

2. 合理设置动画时长

dart 复制代码
// ✅ 快速交互:200-300ms
OpenContainer(
  transitionDuration: const Duration(milliseconds: 300),
  // ...
)

// ✅ 复杂动画:400-500ms
OpenContainer(
  transitionDuration: const Duration(milliseconds: 500),
  // ...
)

// ❌ 避免过长的动画
OpenContainer(
  transitionDuration: const Duration(seconds: 2),  // 太慢
  // ...
)

3. 使用 Key 确保动画正确

dart 复制代码
// ✅ 正确:使用 ValueKey
PageTransitionSwitcher(
  child: Container(
    key: ValueKey(_currentIndex),  // 重要!
    child: _buildPage(_currentIndex),
  ),
)

// ❌ 错误:没有 Key
PageTransitionSwitcher(
  child: _buildPage(_currentIndex),  // 动画可能不触发
)

4. 优化性能

dart 复制代码
// ✅ 正确:使用 const 构造函数
OpenContainer(
  closedBuilder: (context, action) {
    return const MyWidget();  // const
  },
  // ...
)

// ✅ 正确:避免在动画中重建复杂组件
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});
  
  @override
  Widget build(BuildContext context) {
    // 组件会被缓存
    return const ExpensiveWidget();
  }
}

5. 处理返回导航

dart 复制代码
// ✅ 正确:OpenContainer 自动处理返回
OpenContainer(
  closedBuilder: (context, action) => ListItem(),
  openBuilder: (context, action) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: action,  // 使用 action 关闭
        ),
      ),
      // ...
    );
  },
)

🐛 常见问题与解决方案

问题1:动画不流畅

现象

  • 动画卡顿
  • 掉帧明显

解决方案

dart 复制代码
// 1. 减少动画时长
OpenContainer(
  transitionDuration: const Duration(milliseconds: 250),  // 更短
  // ...
)

// 2. 使用 const 构造函数
closedBuilder: (context, action) {
  return const MyWidget();  // const
}

// 3. 避免在动画中进行耗时操作
openBuilder: (context, action) {
  return FutureBuilder(
    future: _loadData(),  // 异步加载
    builder: (context, snapshot) {
      // ...
    },
  );
}

问题2:动画没有触发

现象

  • PageTransitionSwitcher 没有动画
  • 内容直接切换

解决方案

dart 复制代码
// ✅ 正确:使用 ValueKey
PageTransitionSwitcher(
  child: Container(
    key: ValueKey(_currentIndex),  // 必须有 Key
    child: _buildPage(_currentIndex),
  ),
)

// ✅ 正确:确保 child 改变
setState(() {
  _currentIndex = newIndex;  // 触发重建
});

问题3:颜色闪烁

现象

  • 动画过程中出现白色闪烁
  • 背景色不连贯

解决方案

dart 复制代码
// ✅ 正确:设置一致的颜色
OpenContainer(
  closedColor: Colors.blue.shade50,
  openColor: Colors.blue.shade50,  // 相同颜色
  // ...
)

// ✅ 正确:使用透明色
SharedAxisTransition(
  fillColor: false,  // 不填充颜色
  // ...
)

🎓 总结

通过本文,你已经掌握了:

✅ animations 的核心概念和优势

✅ OpenContainer 容器转换动画

✅ SharedAxisTransition 共享轴过渡

✅ FadeThroughTransition 淡入淡出

✅ FadeScaleTransition 淡出缩放

✅ 自定义动画参数

✅ 完整的动画展示应用

✅ 最佳实践和常见问题解决方案

animations 让页面过渡变得流畅而专业!通过合理使用不同类型的动画,可以大大提升应用的用户体验和品质感。无论是电商应用、社交应用还是新闻应用,都能从中受益。


🔗 相关资源


相关推荐
lqj_本人3 小时前
Flutter三方库适配OpenHarmony【apple_product_name】oh-package.json5配置详解
flutter
2501_921930833 小时前
第三方库引入实战指南 Flutter for OpenHarmony:path_provider 文件路径详解
flutter
2501_921930834 小时前
Flutter for OpenHarmony:第三方库实战 chewie 视频播放器UI组件详解
flutter·ui
Sun_gentle4 小时前
android studio创建flutter项目
android·flutter·android studio
Haha_bj4 小时前
Flutter——List.map()
flutter·app
LawrenceLan6 小时前
30.Flutter 零基础入门(三十):GridView 网格布局 —— 九宫格与商品列表必学
开发语言·前端·flutter·dart
fifiAmx6 小时前
Flutter 接入RevenueCat后台配置相关
flutter
不爱吃糖的程序媛6 小时前
Flutter Orientation 插件在鸿蒙平台的使用指南
flutter·华为·harmonyos