鸿蒙Flutter实战:列表延时错峰入场动画

前言

一个没有动画的列表是"死"的。打开应用,所有列表项齐刷刷同时出现------这种感觉就像走进一间空房间,所有家具瞬间从地面"弹"出来,毫无层次感。

优秀的列表入场动画能给应用注入呼吸感。Google 的 Material Design 指南也特别强调"错峰动画"(Staggered Animation):子元素不应同时出现,而应按顺序依次亮相,每个元素比前一个稍晚一点,形成波浪般的视觉效果。

本文将介绍如何封装一个 AnimatedListItem 组件,为鸿蒙 Flutter 备忘录应用的三大列表(备忘录、待办、日记)注入灵动的入场动画。

项目仓库:todo_flutter_harmony

效果分析

拆解"延时错峰入场"这个需求,实际上包含了三个维度的动画:

  1. 透明度:从 0 到 1 淡入(Opacity 0.0 → 1.0)
  2. 位置:从下方偏移到原位(Y 偏移 30px → 0)
  3. 时序:每项比前一项延迟固定时间启动(错峰)

把这三个维度组合起来,就是我们要实现的效果。

AnimationController 基础

Flutter 中实现自定义动画的核心是 AnimationController。这里有几个关键概念需要先厘清:

dart 复制代码
// 创建一个 AnimationController
late AnimationController _controller;

@override
void initState() {
  super.initState();

  _controller = AnimationController(
    duration: const Duration(milliseconds: 400),  // 动画总时长
    vsync: this,  // 提供 Ticker,与屏幕刷新率同步
  );

  // 启动动画
  _controller.forward();
}

AnimationController 的值在 0.0 到 1.0 之间线性变化(默认 lowerBound: 0.0upperBound: 1.0)。想要非线性变化?用 CurvedAnimation 包裹:

dart 复制代码
final curvedAnimation = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeOut,  // 先快后慢
);

Curves.easeOut 的物理意义是"减速运动"------元素从下方快速弹出,到达原位置时逐渐减速,模拟自然界的惯性感。这比线性运动更符合人类的视觉认知。

错峰延时的实现

错峰的核心在于"每个元素比前一个晚一点启动动画"。最容易想到的方案是 Future.delayed

dart 复制代码
// 不推荐
Future.delayed(Duration(milliseconds: index * 50), () {
  _controller.forward();
});

但这个方案有两个问题:一是引入了额外的异步开销,二是延迟期��元素处于不可见状态(透明度为 0),用户会看到短暂的"空白"。

更好的做法是用延迟启动 AnimationController

dart 复制代码
_controller.forward(from: 0);

结合组件参数,我们让每个 AnimatedListItem 接收一个 delay 参数:

dart 复制代码
class AnimatedListItem extends StatefulWidget {
  final Widget child;
  final int delay;       // 延时量,单位为毫秒
  final Curve curve;     // 可选,动画曲线

  const AnimatedListItem({
    super.key,
    required this.child,
    this.delay = 0,
    this.curve = Curves.easeOut,
  });
}

initState 中,通过 Future.delayed 延迟启动动画------但和上面的反例不同,这次我们只在动画启动前隐藏元素,启动后立即显示:

dart 复制代码
class _AnimatedListItemState extends State<AnimatedListItem>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacityAnimation;
  late Animation<Offset> _slideAnimation;
  bool _started = false;

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

    _opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: widget.curve),
    );

    _slideAnimation = Tween<Offset>(
      begin: const Offset(0, 0.3),
      end: Offset.zero,
    ).animate(
      CurvedAnimation(parent: _controller, curve: widget.curve),
    );

    // 最关键的一行:延时启动
    Future.delayed(Duration(milliseconds: widget.delay), () {
      if (mounted) {
        setState(() => _started = true);
        _controller.forward();
      }
    });
  }

注意 if (mounted) 检查------这在延迟回调中至关重要。如果用户在动画延迟期间导航离开,widget 已经被 dispose,此时调用 setState_controller.forward() 会抛出异常。

构建视图

dart 复制代码
  @override
  Widget build(BuildContext context) {
    if (!_started) {
      // 动画启动前:完全透明,避免闪烁
      return Opacity(
        opacity: 0.0,
        child: widget.child,
      );
    }

    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacityAnimation.value,
          child: Transform.translate(
            offset: Offset(
              0,
              _slideAnimation.value.dy * 30,  // 将 0-0.3 映射到 0-30px
            ),
            child: child,
          ),
        );
      },
      child: widget.child,
    );
  }

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

这里做了两层优化:

  1. 动画启动前用 Opacity(opacity: 0.0):保持布局占位,避免列表跳动
  2. AnimatedBuilderchild 参数 :把 widget.child 作为预构建 child 传入,避免每次动画帧都重建子树

在列表中使用

ListView.builder 中,用法极其简洁:

dart 复制代码
ListView.builder(
  itemCount: memos.length,
  itemBuilder: (context, index) {
    return AnimatedListItem(
      delay: index * 60,  // 每项比前一项晚 60ms
      child: SlideActionTile(
        leftActions: _buildLeftActions(memos[index]),
        rightActions: _buildRightActions(memos[index]),
        child: MemoCard(memo: memos[index]),
      ),
    );
  },
)

index * 60 意味着:

  • 第 0 项:延迟 0ms,立即出现
  • 第 1 项:延迟 60ms
  • 第 2 项:延迟 120ms
  • 第 N 项:延迟 N × 60ms

对于 10 个备忘录的列表,最后一个元素的延迟是 540ms,加上 400ms 的动画时长,整个入场序列约 1 秒完成------节奏刚好,不会让用户觉得拖沓。

与 Provider 数据加载的配合

备忘录应用使用 Provider 管理状态,数据在页面 initState 中异步加载:

dart 复制代码
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    context.read<MemoProvider>().loadMemos();
  });
}

这里的 addPostFrameCallback 确保了在首帧渲染完成后再触发数据加载。当 Provider 的 notifyListeners() 触发 UI 重建时,ListView.builder 会创建新的 AnimatedListItem 实例,每个实例按自己的延时启动动画。

关键点 :数据刷新(如下拉刷新)会触发列表重建,此时所有 AnimatedListItem 重新创建并重新播放入场动画------这是预期行为,重新加载的数据理应重新入场。

如果想让动画只在首次加载时播放,可以在 Provider 中加一个 _hasInitialLoaded 标记,后续加载时传递 delay: 0

性能考量

这个方案的性能开销很低:

  1. 每个 AnimatedListItem 只有一个 AnimationController,且动画时长仅 400ms,完成后停止 tick
  2. AnimatedBuilder 只重建自身子树,不触发父级 widget 重建
  3. 预构建 child 避免重复创建 widget

在鸿蒙 OHOS 真机上测试,20 个列表项的错峰动画帧率稳定在 60fps,无任何卡顿。

进阶技巧:交错动画

如果想让透明度动画和位移动画有不同的时序,可以组合两个 Tween 并设置不同的曲线区间:

dart 复制代码
// 透明度先于位移完成
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Interval(0.0, 0.6, curve: Curves.easeOut),  // 前60%时间完成
  ),
);

_slideAnimation = Tween<Offset>(
  begin: const Offset(0, 0.5),
  end: Offset.zero,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Interval(0.0, 1.0, curve: Curves.easeOutCubic),  // 完整周期
  ),
);

Interval 允许你为同一个 AnimationController 的不同 property 分配不同的时间区间,创建更复杂的交错节奏。

总结

延时错峰入场动画的实现可以浓缩为三要素:

  1. AnimationController:为每个列表项创建独立控制器,控制 400ms 的动画周期
  2. Future.delayed + mounted 检查index * delay 实现错峰启动,mounted 防止异步回调崩溃
  3. Opacity + Transform.translate :透明度淡入 + Y 轴位移组合,easeOut 曲线模拟减速运动

封装后的 AnimatedListItem 仅仅是对 AnimationController 的一层薄封装,但它为整个应用的列表注入了生命力------而这生命力完全由 Flutter 框架层提供,在鸿蒙平台上零额外适配。

完整项目代码见:todo_flutter_harmony

相关推荐
Flynt3 小时前
升级Flutter 3.44,我踩了HCPP和AGP 9的坑
android·flutter·dart
程序员老刘4 小时前
Flutter 3.44 更新要点:很重要但暂时先别升级
flutter·ai编程·客户端
●VON6 小时前
BodyAR 从零开始:开发环境搭建与完整项目配置指南
华为·harmonyos·鸿蒙·新特性
小飞象—木兮6 小时前
解析华为-企业经营分析会如何开及如何写经营报告(附华为经营分析会指标体系与评价体系 、报告模板、数据源···)
华为
2301_780356707 小时前
加入开源鸿蒙生态:全视通与开鸿启源共建智慧医康养新场景
harmonyos
fuquxiaoguang7 小时前
1.58-bit的AI突围:面壁智能×华为昇腾如何改写大模型训练规则
人工智能·华为·清华大学·面壁智能
程序员老刘·7 小时前
Flutter版本选择指南:3.44惊艳发布但需观望 | 2026年5月
flutter·ai编程·跨平台开发·客户端开发
●VON8 小时前
鸿蒙Flutter实战:Emoji心情选择器组件
flutter·华为·harmonyos
前端不太难8 小时前
鸿蒙 PC 为什么需要新的组件体系?
华为·状态模式·harmonyos