Flutter OpenHarmony 三方库 animations 动画效果适配详解

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

1. 引言

在 Flutter 应用开发中,动画是提升用户体验的重要手段。animations 是 Google 官方提供的 Flutter 动画库,遵循 Material Design 规范,提供了多种预定义的过渡动画效果。在 OpenHarmony 平台上,该库已经完成了适配工作,开发者可以直接引入使用。

当前环境说明:

  • Flutter 版本:3.27.5
  • HarmonyOS 版本:6.0
  • animations 版本:2.0.11(OpenHarmony 适配版)

源码仓库:

2. animations 库概述

2.1 库简介

animations 库提供了以下核心动画组件:

组件名称 功能说明 适用场景
OpenContainer 容器展开/收起动画 列表项展开为详情页
showModal 模态弹窗动画 底部弹窗、对话框
FadeScaleTransition 淡入缩放过渡 弹窗进入/退出
FadeThroughTransition 淡入穿透过渡 底部导航栏页面切换
SharedAxisTransition 共享轴过渡 分步表单、水平/垂直滑动
PageTransitionSwitcher 页面切换器 自定义页面切换动画

2.2 引入方式

pubspec.yaml 文件中添加以下依赖配置:

yaml 复制代码
dependencies:
  animations:
    git:
      url: https://atomgit.com/openharmony-sig/flutter_packages.git
      path: packages/animations
      ref: br_animations-v2.0.11_ohos

说明: 必须使用 OpenHarmony 适配版本,通过 git 方式引入。不能使用官方 pub.dev 的版本,因为官方版本不包含 OpenHarmony 平台支持。

3. 核心 API 讲解

3.1 OpenContainer - 容器展开动画

OpenContaineranimations 库中最具特色的组件之一,实现了 Material Design 中的"转换容器"模式。当用户点击一个容器时,它会平滑地展开并填满整个屏幕,同时展示新的内容。

构造函数
dart 复制代码
const OpenContainer({
  super.key,
  this.closedColor = Colors.white,
  this.openColor = Colors.white,
  this.middleColor,
  this.closedElevation = 1.0,
  this.openElevation = 4.0,
  this.closedShape = const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(4.0)),
  ),
  this.openShape = const RoundedRectangleBorder(),
  this.onClosed,
  required this.closedBuilder,
  required this.openBuilder,
  this.tappable = true,
  this.transitionDuration = const Duration(milliseconds: 300),
  this.transitionType = ContainerTransitionType.fade,
  this.useRootNavigator = false,
  this.routeSettings,
  this.clipBehavior = Clip.antiAlias,
})
核心参数详解
参数 类型 默认值 说明
closedBuilder CloseContainerBuilder 必填 构建关闭状态下的 Widget,接收 (context, action) 参数,action 用于打开容器
openBuilder OpenContainerBuilder<T> 必填 构建打开状态下的 Widget,接收 (context, action) 参数,action 用于关闭容器
closedColor Color Colors.white 关闭状态下的背景颜色
openColor Color Colors.white 打开状态下的背景颜色
middleColor Color? Theme.canvasColor 过渡期间的中间颜色(仅用于 fadeThrough 类型)
closedElevation double 1.0 关闭状态下的阴影高度
openElevation double 4.0 打开状态下的阴影高度
closedShape ShapeBorder 圆角矩形 关闭状态下的形状
openShape ShapeBorder 矩形 打开状态下的形状
tappable bool true 是否可点击整个容器打开
transitionDuration Duration 300ms 过渡动画时长
transitionType ContainerTransitionType fade 过渡类型:fadefadeThrough
onClosed ClosedCallback<T?>? null 容器关闭时的回调,接收返回值
过渡类型说明
dart 复制代码
enum ContainerTransitionType {
  /// 在传出元素上方淡入传入元素
  fade,
  
  /// 先淡出传出元素,完全淡出后再淡入传入元素
  fadeThrough,
}
使用示例
dart 复制代码
OpenContainer<String>(
  closedBuilder: (context, action) {
    return ListTile(
      title: Text('点击展开'),
      subtitle: Text('查看详情'),
    );
  },
  openBuilder: (context, action) {
    return Scaffold(
      appBar: AppBar(title: Text('详情页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => action(returnValue: '返回的数据'),
          child: Text('关闭'),
        ),
      ),
    );
  },
  onClosed: (data) {
    print('容器关闭,返回值: $data');
  },
)

3.2 showModal - 模态弹窗

showModal 用于显示一个模态弹窗,支持自定义过渡动画配置。

函数签名
dart 复制代码
Future<T?> showModal<T>({
  required BuildContext context,
  ModalConfiguration configuration = const FadeScaleTransitionConfiguration(),
  bool useRootNavigator = true,
  required WidgetBuilder builder,
  RouteSettings? routeSettings,
  ui.ImageFilter? filter,
})
参数详解
参数 类型 默认值 说明
context BuildContext 必填 用于查找 Navigator 的上下文
configuration ModalConfiguration FadeScaleTransitionConfiguration() 弹窗配置,定义过渡动画和屏障属性
useRootNavigator bool true 是否使用根 Navigator
builder WidgetBuilder 必填 构建弹窗内容的函数
ModalConfiguration 配置类
dart 复制代码
abstract class ModalConfiguration {
  const ModalConfiguration({
    required this.barrierColor,        // 屏障颜色
    required this.barrierDismissible,  // 是否可点击屏障关闭
    this.barrierLabel,                 // 无障碍标签
    required this.transitionDuration,  // 进入动画时长
    required this.reverseTransitionDuration, // 退出动画时长
  });
  
  Widget transitionBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  );
}
FadeScaleTransitionConfiguration

这是 showModal 的默认配置,实现了 Material Design 的淡入缩放效果:

dart 复制代码
const FadeScaleTransitionConfiguration({
  super.barrierColor = Colors.black54,
  super.barrierDismissible = true,
  super.transitionDuration = const Duration(milliseconds: 150),
  super.reverseTransitionDuration = const Duration(milliseconds: 75),
  String super.barrierLabel = 'Dismiss',
})

动画特点:

  • 进入时:从 80% 缩放到 100%,同时淡入(150ms)
  • 退出时:仅淡出,不缩放(75ms)
  • 这种不对称设计强调新内容的重要性
使用示例
dart 复制代码
showModal(
  context: context,
  configuration: FadeScaleTransitionConfiguration(),
  builder: (context) {
    return Container(
      height: 300,
      child: Center(child: Text('模态弹窗内容')),
    );
  },
)

3.3 FadeScaleTransition - 淡入缩放过渡

FadeScaleTransition 是一个独立的过渡动画 Widget,可以单独使用,也可以配合 showModal 使用。

构造函数
dart 复制代码
const FadeScaleTransition({
  super.key,
  required this.animation,
  this.child,
})
参数详解
参数 类型 说明
animation Animation<double> 驱动过渡的动画控制器
child Widget? 要应用过渡的子 Widget
动画曲线详解
dart 复制代码
// 淡入曲线:0.0 到 0.3 区间内完成
static final Animatable<double> _fadeInTransition = CurveTween(
  curve: const Interval(0.0, 0.3),
);

// 缩放曲线:从 0.80 到 1.00,使用减速曲线
static final Animatable<double> _scaleInTransition = Tween<double>(
  begin: 0.80,
  end: 1.00,
).chain(CurveTween(curve: decelerateEasing));

// 淡出曲线:全程淡出
static final Animatable<double> _fadeOutTransition = Tween<double>(
  begin: 1.0,
  end: 0.0,
);
使用场景

通常与 DualTransitionBuilder 配合使用,区分进入和退出动画:

dart 复制代码
DualTransitionBuilder(
  animation: animation,
  forwardBuilder: (context, animation, child) {
    return FadeScaleTransition(animation: animation, child: child);
  },
  reverseBuilder: (context, animation, child) {
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  },
  child: yourWidget,
)

3.4 FadeThroughTransition - 淡入穿透过渡

FadeThroughTransition 适用于没有强关联关系的 UI 元素之间的过渡,例如底部导航栏的页面切换。

构造函数
dart 复制代码
const FadeThroughTransition({
  super.key,
  required this.animation,
  required this.secondaryAnimation,
  this.fillColor,
  this.child,
})
参数详解
参数 类型 说明
animation Animation<double> 驱动进入动画的主动画
secondaryAnimation Animation<double> 驱动退出动画的次级动画
fillColor Color? 过渡期间的背景颜色,默认为 Theme.canvasColor
child Widget? 要应用过渡的子 Widget
动画特点
  • 退出动画:先淡出(前 30% 时间),然后完全消失
  • 进入动画:等待退出完成后,从 92% 缩放到 100%,同时淡入
  • 缩放仅应用于进入元素,强调新内容
配合 PageTransitionSwitcher 使用
dart 复制代码
PageTransitionSwitcher(
  transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
    return FadeThroughTransition(
      animation: primaryAnimation,
      secondaryAnimation: secondaryAnimation,
      child: child,
    );
  },
  child: yourWidget,
)

3.5 SharedAxisTransition - 共享轴过渡

SharedAxisTransition 适用于具有空间或导航关系的 UI 元素之间的过渡,例如分步表单。

构造函数
dart 复制代码
const SharedAxisTransition({
  super.key,
  required this.animation,
  required this.secondaryAnimation,
  required this.transitionType,
  this.fillColor,
  this.child,
})
过渡类型
dart 复制代码
enum SharedAxisTransitionType {
  /// 垂直(Y 轴)共享轴过渡
  vertical,
  
  /// 水平(X 轴)共享轴过渡
  horizontal,
  
  /// 缩放(Z 轴)共享轴过渡
  scaled,
}
动画参数详解
过渡类型 进入动画 退出动画
horizontal 从右侧 30px 滑入 + 淡入 向左侧 30px 滑出 + 淡出
vertical 从下方 30px 滑入 + 淡入 向上方 30px 滑出 + 淡出
scaled 从 80% 缩放到 100% + 淡入 从 100% 缩放到 110% + 淡出
使用示例
dart 复制代码
PageTransitionSwitcher(
  transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
    return SharedAxisTransition(
      animation: primaryAnimation,
      secondaryAnimation: secondaryAnimation,
      transitionType: SharedAxisTransitionType.horizontal,
      child: child,
    );
  },
  child: yourWidget,
)

3.6 PageTransitionSwitcher - 页面切换器

PageTransitionSwitcher 是一个通用的页面切换容器,当 child 变化时,使用指定的过渡动画进行切换。

构造函数
dart 复制代码
const PageTransitionSwitcher({
  super.key,
  this.duration = const Duration(milliseconds: 300),
  this.reverse = false,
  required this.transitionBuilder,
  this.layoutBuilder = defaultLayoutBuilder,
  this.child,
})
参数详解
参数 类型 默认值 说明
child Widget? null 当前显示的子 Widget
duration Duration 300ms 过渡动画时长
reverse bool false 是否反向切换(新孩子在旧孩子下方)
transitionBuilder PageTransitionSwitcherTransitionBuilder 必填 构建过渡动画的函数
layoutBuilder PageTransitionSwitcherLayoutBuilder defaultLayoutBuilder 布局函数,默认为 Stack 居中
关键机制
  • child 变化时,旧孩子使用 secondaryAnimation 退出,新孩子使用 primaryAnimation 进入
  • 如果切换速度足够快(在 duration 内),可以同时存在多个正在过渡的孩子
  • 必须使用 Key :如果新旧孩子是相同类型但参数不同,需要设置 Key(通常用 ValueKey)来触发过渡
动画方向说明
dart 复制代码
// reverse = false(默认):新孩子覆盖旧孩子
// 类似 push 新页面
- 旧孩子:secondaryAnimation 正向运行
- 新孩子:primaryAnimation 正向运行

// reverse = true:新孩子在旧孩子下方
// 类似 pop 页面
- 旧孩子:primaryAnimation 反向运行
- 新孩子:secondaryAnimation 反向运行

4. 完整应用示例

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 动画演示',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MainScreen(),
    );
  }
}

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

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    ContainerListPage(),
    ModalDemoPage(),
    StepFormPage(),
    SwitcherDemoPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageTransitionSwitcher(
        duration: const Duration(milliseconds: 400),
        transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
          return FadeThroughTransition(
            animation: primaryAnimation,
            secondaryAnimation: secondaryAnimation,
            fillColor: Theme.of(context).scaffoldBackgroundColor,
            child: child,
          );
        },
        child: _pages[_currentIndex],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.view_list),
            label: '容器列表',
          ),
          NavigationDestination(
            icon: Icon(Icons.popup),
            label: '弹窗',
          ),
          NavigationDestination(
            icon: Icon(Icons.steps),
            label: '分步表单',
          ),
          NavigationDestination(
            icon: Icon(Icons.swap_horiz),
            label: '切换器',
          ),
        ],
      ),
    );
  }
}

// ==================== 页面 1:OpenContainer 容器列表 ====================

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('OpenContainer 容器展开'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 10,
        itemBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.only(bottom: 12),
            child: OpenContainer<String>(
              closedColor: Colors.white,
              openColor: Colors.white,
              closedElevation: 2,
              openElevation: 4,
              closedShape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              openShape: const RoundedRectangleBorder(),
              transitionDuration: const Duration(milliseconds: 500),
              transitionType: ContainerTransitionType.fadeThrough,
              closedBuilder: (context, openAction) {
                return _ClosedCard(index: index);
              },
              openBuilder: (context, closeAction) {
                return _OpenDetail(index: index, closeAction: closeAction);
              },
              onClosed: (data) {
                if (data != null) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('收到返回值: $data')),
                  );
                }
              },
            ),
          );
        },
      ),
    );
  }
}

class _ClosedCard extends StatelessWidget {
  final int index;

  const _ClosedCard({required this.index});

  @override
  Widget build(BuildContext context) {
    final colors = [
      Colors.blue,
      Colors.purple,
      Colors.green,
      Colors.orange,
      Colors.red,
    ];
    final color = colors[index % colors.length];

    return Material(
      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(
            Icons.star,
            color: color,
            size: 30,
          ),
        ),
        title: Text(
          '卡片 $index',
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        subtitle: Text('点击展开查看详情 #$index'),
        trailing: Icon(
          Icons.arrow_forward_ios,
          size: 16,
          color: Colors.grey[400],
        ),
      ),
    );
  }
}

class _OpenDetail extends StatelessWidget {
  final int index;
  final CloseContainerActionCallback<String> closeAction;

  const _OpenDetail({
    required this.index,
    required this.closeAction,
  });

  @override
  Widget build(BuildContext context) {
    final colors = [
      Colors.blue,
      Colors.purple,
      Colors.green,
      Colors.orange,
      Colors.red,
    ];
    final color = colors[index % colors.length];

    return Scaffold(
      appBar: AppBar(
        title: Text('详情页 #$index'),
        backgroundColor: color,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => closeAction(returnValue: '来自详情页 #$index 的返回'),
        ),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              width: double.infinity,
              height: 200,
              decoration: BoxDecoration(
                color: color.withOpacity(0.2),
                borderRadius: BorderRadius.circular(16),
              ),
              child: Center(
                child: Icon(
                  Icons.star,
                  size: 80,
                  color: color,
                ),
              ),
            ),
            const SizedBox(height: 24),
            Text(
              '这是详情页 #$index',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
                color: color,
              ),
            ),
            const SizedBox(height: 16),
            const Text(
              '这里展示了 OpenContainer 展开后的详细内容。'
              '当用户点击关闭按钮时,容器会平滑地收缩回原来的大小。',
              style: TextStyle(fontSize: 16, height: 1.6),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () => closeAction(returnValue: '来自详情页 #$index 的返回'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: color,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: const Text('关闭容器', style: TextStyle(fontSize: 16)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ==================== 页面 2:showModal 弹窗演示 ====================

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('showModal 弹窗演示'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildModalButton(
              context,
              label: 'FadeScale 弹窗',
              icon: Icons.zoom_in,
              configuration: const FadeScaleTransitionConfiguration(),
              content: '这是一个使用 FadeScaleTransition 的模态弹窗。'
                  '进入时从 80% 缩放到 100%,退出时仅淡出。',
            ),
            const SizedBox(height: 20),
            _buildModalButton(
              context,
              label: '自定义配置弹窗',
              icon: Icons.settings,
              configuration: const _CustomModalConfiguration(),
              content: '这是一个使用自定义配置的模态弹窗。'
                  '过渡时间更长,屏障颜色不同。',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildModalButton(
    BuildContext context, {
    required String label,
    required IconData icon,
    required ModalConfiguration configuration,
    required String content,
  }) {
    return ElevatedButton.icon(
      onPressed: () {
        showModal(
          context: context,
          configuration: configuration,
          builder: (context) {
            return _ModalContent(
              content: content,
              configuration: configuration,
            );
          },
        );
      },
      icon: Icon(icon),
      label: Text(label),
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
      ),
    );
  }
}

class _ModalContent extends StatelessWidget {
  final String content;
  final ModalConfiguration configuration;

  const _ModalContent({
    required this.content,
    required this.configuration,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(24),
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 20,
            offset: const Offset(0, 10),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            Icons.check_circle,
            size: 60,
            color: Theme.of(context).colorScheme.primary,
          ),
          const SizedBox(height: 16),
          Text(
            '弹窗标题',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const SizedBox(height: 16),
          Text(
            content,
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 16, height: 1.6),
          ),
          const SizedBox(height: 24),
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: () => Navigator.pop(context),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text('关闭弹窗'),
            ),
          ),
        ],
      ),
    );
  }
}

class _CustomModalConfiguration extends ModalConfiguration {
  const _CustomModalConfiguration()
      : super(
          barrierColor: Colors.black87,
          barrierDismissible: true,
          barrierLabel: '关闭',
          transitionDuration: const Duration(milliseconds: 300),
          reverseTransitionDuration: const Duration(milliseconds: 200),
        );

  @override
  Widget transitionBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return FadeScaleTransition(
      animation: animation,
      child: child,
    );
  }
}

// ==================== 页面 3:SharedAxisTransition 分步表单 ====================

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

  @override
  State<StepFormPage> createState() => _StepFormPageState();
}

class _StepFormPageState extends State<StepFormPage> {
  int _currentStep = 0;

  final List<Widget> _steps = [
    const StepContent(
      title: '步骤 1:基本信息',
      icon: Icons.person,
      color: Colors.blue,
      description: '请输入您的基本信息,包括姓名、邮箱等。',
    ),
    const StepContent(
      title: '步骤 2:联系方式',
      icon: Icons.phone,
      color: Colors.green,
      description: '请输入您的联系方式,包括手机号、地址等。',
    ),
    const StepContent(
      title: '步骤 3:确认提交',
      icon: Icons.check,
      color: Colors.orange,
      description: '请确认您填写的信息,然后点击提交。',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SharedAxis 分步表单'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          // 步骤指示器
          _buildStepIndicator(),
          const Divider(),
          // 步骤内容
          Expanded(
            child: PageTransitionSwitcher(
              duration: const Duration(milliseconds: 400),
              reverse: false,
              transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
                return SharedAxisTransition(
                  animation: primaryAnimation,
                  secondaryAnimation: secondaryAnimation,
                  transitionType: SharedAxisTransitionType.horizontal,
                  fillColor: Theme.of(context).scaffoldBackgroundColor,
                  child: child,
                );
              },
              child: _steps[_currentStep],
            ),
          ),
          // 底部按钮
          _buildBottomButtons(),
        ],
      ),
    );
  }

  Widget _buildStepIndicator() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: List.generate(3, (index) {
          final isActive = index == _currentStep;
          final isCompleted = index < _currentStep;
          return Expanded(
            child: Row(
              children: [
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                    color: isActive
                        ? Colors.blue
                        : isCompleted
                            ? Colors.green
                            : Colors.grey[300],
                    shape: BoxShape.circle,
                  ),
                  child: Center(
                    child: isCompleted
                        ? const Icon(Icons.check, color: Colors.white, size: 20)
                        : Text(
                            '${index + 1}',
                            style: TextStyle(
                              color: isActive || isCompleted
                                  ? Colors.white
                                  : Colors.grey[600],
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                  ),
                ),
                if (index < 2)
                  Expanded(
                    child: Container(
                      height: 2,
                      color: index < _currentStep
                          ? Colors.green
                          : Colors.grey[300],
                    ),
                  ),
              ],
            ),
          );
        }),
      ),
    );
  }

  Widget _buildBottomButtons() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          if (_currentStep > 0)
            Expanded(
              child: OutlinedButton(
                onPressed: () {
                  setState(() {
                    _currentStep--;
                  });
                },
                child: const Text('上一步'),
              ),
            ),
          if (_currentStep > 0) const SizedBox(width: 16),
          Expanded(
            child: ElevatedButton(
              onPressed: () {
                if (_currentStep < 2) {
                  setState(() {
                    _currentStep++;
                  });
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('提交成功!')),
                  );
                  setState(() {
                    _currentStep = 0;
                  });
                }
              },
              child: Text(_currentStep < 2 ? '下一步' : '提交'),
            ),
          ),
        ],
      ),
    );
  }
}

class StepContent extends StatelessWidget {
  final String title;
  final IconData icon;
  final Color color;
  final String description;

  const StepContent({
    super.key,
    required this.title,
    required this.icon,
    required this.color,
    required this.description,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              color: color.withOpacity(0.2),
              shape: BoxShape.circle,
            ),
            child: Icon(
              icon,
              size: 50,
              color: color,
            ),
          ),
          const SizedBox(height: 24),
          Text(
            title,
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
          const SizedBox(height: 16),
          Text(
            description,
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 16, height: 1.6),
          ),
        ],
      ),
    );
  }
}

// ==================== 页面 4:PageTransitionSwitcher 切换器演示 ====================

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

  @override
  State<SwitcherDemoPage> createState() => _SwitcherDemoPageState();
}

class _SwitcherDemoPageState extends State<SwitcherDemoPage> {
  int _selectedIndex = 0;
  String _transitionType = 'fadeThrough';

  final List<_ColorItem> _items = [
    _ColorItem(Colors.blue, '蓝色', Icons.water),
    _ColorItem(Colors.red, '红色', Icons.local_fire_department),
    _ColorItem(Colors.green, '绿色', Icons.eco),
    _ColorItem(Colors.orange, '橙色', Icons.wb_sunny),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PageTransitionSwitcher'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          // 切换类型选择
          Padding(
            padding: const EdgeInsets.all(16),
            child: SegmentedButton<String>(
              segments: const [
                ButtonSegment(
                  value: 'fadeThrough',
                  label: Text('FadeThrough'),
                  icon: Icon(Icons.opacity),
                ),
                ButtonSegment(
                  value: 'sharedAxis',
                  label: Text('SharedAxis'),
                  icon: Icon(Icons.swap_horiz),
                ),
              ],
              selected: {_transitionType},
              onSelectionChanged: (Set<String> selection) {
                setState(() {
                  _transitionType = selection.first;
                });
              },
            ),
          ),
          // 切换内容
          Expanded(
            child: PageTransitionSwitcher(
              duration: const Duration(milliseconds: 500),
              reverse: false,
              transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
                if (_transitionType == 'fadeThrough') {
                  return FadeThroughTransition(
                    animation: primaryAnimation,
                    secondaryAnimation: secondaryAnimation,
                    fillColor: Theme.of(context).scaffoldBackgroundColor,
                    child: child,
                  );
                } else {
                  return SharedAxisTransition(
                    animation: primaryAnimation,
                    secondaryAnimation: secondaryAnimation,
                    transitionType: SharedAxisTransitionType.scaled,
                    fillColor: Theme.of(context).scaffoldBackgroundColor,
                    child: child,
                  );
                }
              },
              child: _buildColorCard(_items[_selectedIndex]),
            ),
          ),
          // 颜色选择器
          Padding(
            padding: const EdgeInsets.all(16),
            child: Wrap(
              spacing: 12,
              children: List.generate(_items.length, (index) {
                final isSelected = index == _selectedIndex;
                return GestureDetector(
                  onTap: () {
                    setState(() {
                      _selectedIndex = index;
                    });
                  },
                  child: Container(
                    width: 60,
                    height: 60,
                    decoration: BoxDecoration(
                      color: _items[index].color,
                      shape: BoxShape.circle,
                      border: Border.all(
                        color: isSelected ? Colors.white : Colors.transparent,
                        width: 3,
                      ),
                      boxShadow: [
                        BoxShadow(
                          color: _items[index].color.withOpacity(0.3),
                          blurRadius: 8,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: Icon(
                      _items[index].icon,
                      color: Colors.white,
                      size: 30,
                    ),
                  ),
                );
              }),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildColorCard(_ColorItem item) {
    return Container(
      margin: const EdgeInsets.all(24),
      padding: const EdgeInsets.all(32),
      decoration: BoxDecoration(
        color: item.color.withOpacity(0.1),
        borderRadius: BorderRadius.circular(24),
        border: Border.all(
          color: item.color.withOpacity(0.3),
          width: 2,
        ),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            item.icon,
            size: 80,
            color: item.color,
          ),
          const SizedBox(height: 24),
          Text(
            item.name,
            style: TextStyle(
              fontSize: 32,
              fontWeight: FontWeight.bold,
              color: item.color,
            ),
          ),
          const SizedBox(height: 16),
          Text(
            '这是 ${item.name} 的展示页面',
            style: TextStyle(
              fontSize: 18,
              color: item.color.withOpacity(0.8),
            ),
          ),
        ],
      ),
    );
  }
}

class _ColorItem {
  final Color color;
  final String name;
  final IconData icon;

  _ColorItem(this.color, this.name, this.icon);
}

5. 常见问题解答

Q1: 为什么我的 PageTransitionSwitcher 没有触发动画?

原因: 新旧孩子被认为是同一个 Widget。

解决方案: 为每个孩子设置不同的 Key

dart 复制代码
PageTransitionSwitcher(
  child: Container(
    key: ValueKey<int>(_selectedIndex), // 必须设置 Key
    color: _colors[_selectedIndex],
  ),
)

Q2: OpenContainer 展开后如何返回数据?

解决方案: 使用 action 回调的 returnValue 参数:

dart 复制代码
openBuilder: (context, action) {
  return ElevatedButton(
    onPressed: () => action(returnValue: '返回的数据'),
    child: Text('关闭'),
  );
},
onClosed: (data) {
  print('收到返回值: $data');
},

Q3: 如何自定义 showModal 的过渡动画?

解决方案: 继承 ModalConfiguration 并实现 transitionBuilder

dart 复制代码
class CustomModalConfiguration extends ModalConfiguration {
  const CustomModalConfiguration()
      : super(
          barrierColor: Colors.black54,
          barrierDismissible: true,
          barrierLabel: '关闭',
          transitionDuration: const Duration(milliseconds: 300),
          reverseTransitionDuration: const Duration(milliseconds: 200),
        );

  @override
  Widget transitionBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    // 自定义过渡动画
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0, 1),
        end: Offset.zero,
      ).animate(animation),
      child: child,
    );
  }
}

Q4: SharedAxisTransition 的三种类型有什么区别?

类型 动画方向 适用场景
horizontal 水平滑动 ±30px 左右切换的页面
vertical 垂直滑动 ±30px 上下切换的页面
scaled 缩放 80% → 100% 层级关系页面

Q5: 如何控制 PageTransitionSwitcher 的切换方向?

解决方案: 使用 reverse 参数:

dart 复制代码
// reverse = false(默认):新孩子覆盖旧孩子,类似 push
PageTransitionSwitcher(reverse: false, ...)

// reverse = true:新孩子在旧孩子下方,类似 pop
PageTransitionSwitcher(reverse: true, ...)

Q6: animations 库在 OpenHarmony 上有哪些已知问题?

目前 animations 库在 OpenHarmony 平台上运行正常,没有已知的兼容性问题。该库纯 Dart 实现,不依赖平台特定代码,因此在 OpenHarmony 上的表现与 Android/iOS 一致。

Q7: 如何优化动画性能?

建议:

  1. 避免在动画过程中重建 Widget 树
  2. 使用 const 构造函数减少不必要的重建
  3. 对于复杂动画,考虑使用 RepaintBoundary 隔离重绘区域
  4. 控制动画时长在 200-500ms 之间,过长会影响用户体验

6. 总结

animations 库为 Flutter 应用提供了丰富的 Material Design 动画效果,在 OpenHarmony 平台上已经完成了完整适配。文章介绍了:

  1. OpenContainer:实现容器展开/收起的流畅动画
  2. showModal:显示模态弹窗,支持自定义过渡配置
  3. FadeScaleTransition:淡入缩放过渡效果
  4. FadeThroughTransition:淡入穿透过渡,适合底部导航切换
  5. SharedAxisTransition:共享轴过渡,适合分步表单
  6. PageTransitionSwitcher:通用页面切换器,可组合各种过渡效果

这些动画组件可以单独使用,也可以组合使用,为应用带来更加流畅和专业的用户体验。在实际开发中,建议根据具体的业务场景选择合适的动画效果,并注意保持动画的一致性和性能优化。

相关推荐
Ww.xh2 小时前
KMP与Flutter选型实战指南
flutter
2601_949593653 小时前
Flutter OpenHarmony 三方库 camera 相机拍照录像适配详解
flutter
恋猫de小郭3 小时前
WasmGC 是什么?为什么它对 Dart 和 Kotlin 在 Web 领域很重要?
android·前端·flutter
liulian09164 小时前
【Flutter for OpenHarmony第三方库】Flutter for OpenHarmony 多语言国际化适配实战指南
flutter·华为·学习方法·harmonyos
YF021117 小时前
Flutter 编译卡顿解决方案
android·flutter·ios
IntMainJhy18 小时前
【Flutter for OpenHarmony 】第三方库鸿蒙电商全栈实战:从组件适配到项目完整交付✨
flutter·华为·harmonyos
程序员老刘19 小时前
别慌!GetX只是被误杀,但你的代码可能真的在裸奔
flutter·客户端
IntMainJhy19 小时前
【flutter for open harmony】第三方库Flutter 鸿蒙实战:商品详情页完整实现 + 点击跳转失效问题修复✨
flutter·华为·harmonyos
liulian09161 天前
【Flutter for OpenHarmony第三方库】Flutter for OpenHarmony应用更新检测功能实战指南
flutter·华为·学习方法·harmonyos