写在前面

最近在做 Flutter 鸿蒙适配的时候,遇到一个需求:实现商品加入购物车的飞入动画。你知道的,就是那种点击"加入购物车"按钮后,商品图标嗖的一下飞到购物车图标的效果。之前在 Android 和 iOS 上用原生实现过,但在 Flutter 里想找个现成的轮子。

搜了一圈,发现了 piu_animation 这个库。试用了一下,效果还不错,而且关键是支持鸿蒙!今天就来分享一下使用心得。
请添加图片描述

为什么选择 piu_animation

说实话,Flutter 生态里做动画的库不少,但 piu_animation 有几个让我眼前一亮的地方:

代码量真的很少。不像某些动画库,光配置就要写一堆代码。piu_animation 几行代码就能搞定一个完整的飞入动画。

跨平台支持很全面。Android、iOS 自不必说,Web、Windows、macOS 都支持,最重要的是支持鸿蒙系统。现在做 Flutter 项目,鸿蒙适配是绕不过去的坎。

动画效果可定制。虽然是轻量级库,但该有的参数都有:动画时长、起点终点、缩放大小等等,基本能满足大部分需求。

这个库特别适合这些场景:

  • 电商 App 的加入购物车动画(这是最常见的)
  • 点赞、收藏这类操作的视觉反馈
  • 文件保存、下载完成的提示动画
  • 任何需要"从 A 点飞到 B 点"的交互效果

快速开始

第一步:添加依赖

打开项目的 pubspec.yaml 文件,在 dependencies 下面加上这一行:

dependencies:
  flutter:
    sdk: flutter
  piu_animation: ^0.0.5

💡 小提示:写这篇文章的时候,最新版本是 0.0.5。你可以去 pub.dev 上看看有没有更新的版本。

保存文件后,在终端执行:

flutter pub get

等待依赖下载完成。如果你用的是 Android Studio 或 VS Code,保存 pubspec.yaml 后编辑器会自动帮你执行这个命令。

第二步:导入包

在你需要使用动画的 Dart 文件顶部,加上这行导入语句:

import 'package:piu_animation/piu_animation.dart';

📝 注意:如果你要用到 loading 动画功能(后面会讲到),还需要额外导入:

import 'package:piu_animation/piu_loading_animation_widget.dart';

不过对于基础的飞入动画,只导入 piu_animation.dart 就够了。

动手实现第一个动画

理解 GlobalKey 的作用

在开始写代码之前,先说说 GlobalKey。这个东西在 piu_animation 里很重要。

简单来说,GlobalKey 就像是给 Widget 贴了个标签,让我们能在代码里找到这个 Widget 的位置和大小。piu_animation 需要知道动画的起点和终点在屏幕上的具体位置,所以必须用 GlobalKey。

我们需要两个 GlobalKey:

  • 一个给根容器(通常是整个页面的容器)
  • 一个给目标位置(比如购物车图标)

搭建基础页面结构

先来写个简单的页面框架:

class PiuAnimationDemo extends StatefulWidget {
  const PiuAnimationDemo({Key? key}) : super(key: key);

  
  State<PiuAnimationDemo> createState() => _PiuAnimationDemoState();
}

class _PiuAnimationDemoState extends State<PiuAnimationDemo> {
  // 根容器的 GlobalKey
  GlobalKey rootKey = GlobalKey();
  
  // 目标按钮的 GlobalKey
  GlobalKey targetButtonKey = GlobalKey();

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        key: rootKey,  // 注意这里绑定了 rootKey
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                triggerAnimation();
              },
              child: const Text("点我触发动画"),
            ),
            const SizedBox(height: 200),
            ElevatedButton(
              key: targetButtonKey,  // 目标位置绑定 key
              onPressed: () {},
              child: const Text("飞到这里"),
            ),
          ],
        ),
      ),
    );
  }
  
  void triggerAnimation() {
    // 动画触发逻辑,马上就写
  }
}

🎯 关键点

  • rootKey 必须绑定到一个包含动画起点和终点的共同父容器上
  • targetButtonKey 绑定到你希望动画飞向的目标 Widget
  • 这两个 Key 的位置关系很重要,如果绑定错了,动画可能不显示或位置不对

实现动画触发逻辑

现在来实现 triggerAnimation 方法,这是整个动画的核心:

void triggerAnimation() {
  // 第一步:定义要飞的 Widget
  Widget piuWidget = Container(
    color: Colors.redAccent,
    child: const FlutterLogo(size: 50),
  );

这里我用了 FlutterLogo 作为演示,你可以换成任何 Widget。比如商品图片、图标、文字等等。

  // 第二步:计算目标位置的坐标
  RenderBox box = targetButtonKey.currentContext!.findRenderObject() as RenderBox;
  var offset = box.localToGlobal(Offset.zero);
  Offset endOffset = Offset(
    offset.dx + box.size.width / 2,
    offset.dy + box.size.height / 2,
  );

🔍 这段代码在干什么?

  • findRenderObject() 获取 Widget 的渲染对象
  • localToGlobal(Offset.zero) 把局部坐标转换成屏幕坐标
  • 加上 width / 2height / 2 是为了让动画飞到按钮的中心点,而不是左上角

这个计算方式是固定套路,直接复制用就行。

  // 第三步:触发动画
  PiuAnimation.addAnimation(
    rootKey,           // 根容器的 Key
    piuWidget,         // 要飞的 Widget
    endOffset,         // 终点坐标
    maxWidth: MediaQuery.of(context).size.width,  // Widget 最大宽度
    millisecond: 1500, // 动画时长 1.5 秒
    doSomethingBeginCallBack: () {
      print("动画开始了");
    },
    doSomethingFinishCallBack: (success) {
      print("动画结束了");
    },
  );
}

⚙️ 参数说明

  • maxWidth:设置成屏幕宽度是个保险做法,防止 Widget 太大超出屏幕
  • millisecond:1500 毫秒(1.5 秒)是个比较舒服的时长,太快会显得仓促,太慢又拖沓
  • 两个回调函数是可选的,用来在动画开始和结束时做一些额外操作

到这里,一个完整的飞入动画就实现了。运行项目,点击"点我触发动画"按钮,你会看到 Flutter Logo 从按钮位置飞向下面的目标按钮。

深入理解参数配置

必需参数

PiuAnimation.addAnimation 有三个必须传的参数,少一个都不行:

rootGlobalKey
这是根容器的 GlobalKey。piu_animation 会在这个容器上创建一个 Overlay 层来显示动画。如果你不太理解 Overlay,可以把它想象成一个浮在页面最上层的透明画布,动画就在这个画布上绘制。

piuWidget
这是要执行动画的 Widget。可以是任何 Widget:图片、图标、文字、甚至是复杂的组合 Widget。不过建议不要太复杂,因为动画过程中会不断重绘,太复杂会影响性能。

endOffset
动画的终点坐标。这是一个 Offset 对象,包含 x 和 y 两个值,表示屏幕上的绝对位置。前面我们用 localToGlobal 方法计算出来的就是这个。

可选参数详解

除了必需参数,还有一些可选参数可以让动画更符合你的需求:

maxWidth - 默认值 500
Widget 的最大宽度。动画开始时,Widget 会以这个宽度显示,然后逐渐缩小。

maxWidth: MediaQuery.of(context).size.width * 0.8,  // 屏幕宽度的 80%

💭 我的经验:设置成屏幕宽度或者屏幕宽度的一定比例比较保险。如果设置太大,在小屏手机上可能会超出屏幕。

minWidth - 默认值 90
Widget 缩放到的最小宽度。动画飞到终点时,Widget 会缩小到这个宽度。

minWidth: 40,  // 缩小到 40 像素

millisecond - 默认值 2000
动画持续时间,单位是毫秒。

millisecond: 1200,  // 1.2 秒

⏱️ 时长选择建议

  • 800-1000ms:快速、干脆,适合简单的点击反馈
  • 1200-1500ms:适中,大部分场景都合适
  • 1800-2000ms:较慢,适合需要用户注意的重要操作

超过 2 秒会让用户觉得拖沓,低于 800ms 可能看不清动画细节。

doSomethingBeginCallBack
动画开始时的回调函数。可以在这里做一些准备工作,比如禁用按钮、显示 loading 等。

doSomethingBeginCallBack: () {
  setState(() {
    isAnimating = true;
  });
  print("动画开始");
},

doSomethingFinishCallBack
动画结束时的回调函数。这个回调会接收一个 bool 参数,表示动画是否成功完成。

doSomethingFinishCallBack: (success) {
  setState(() {
    isAnimating = false;
  });
  if (success) {
    // 更新购物车数量等操作
    updateCartCount();
  }
},

🎪 回调函数的妙用

  • 在开始回调里禁用按钮,防止用户重复点击
  • 在结束回调里更新 UI 状态,比如购物车数量
  • 在结束回调里触发下一个动画,实现连续动画效果

实战案例:九宫格飞入效果

前面的例子比较简单,现在来做个更实用的:九宫格布局,点击任意位置都能飞向中心的购物车按钮。这个效果在电商 App 里很常见。

页面布局设计

先定义需要的 GlobalKey:

class MultiDirectionDemo extends StatefulWidget {
  const MultiDirectionDemo({Key? key}) : super(key: key);

  
  State<MultiDirectionDemo> createState() => _MultiDirectionDemoState();
}

class _MultiDirectionDemoState extends State<MultiDirectionDemo> {
  GlobalKey rootKey = GlobalKey();
  GlobalKey floatingKey = GlobalKey();
  
  // 用 List.generate 批量创建 9 个 Key
  List<GlobalKey> buttonKeys = List.generate(9, (index) => GlobalKey());

🎨 设计思路

  • List.generate 批量创建 Key,比手动写 9 个变量优雅多了
  • floatingKey 绑定到悬浮按钮(购物车图标)
  • 所有商品按钮都飞向这个悬浮按钮

构建九宫格界面

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        key: rootKey,
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            buildButtonRow([0, 1, 2], ["左上", "上", "右上"]),
            buildButtonRow([3, 4, 5], ["左", "中", "右"]),
            buildButtonRow([6, 7, 8], ["左下", "下", "右下"]),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: floatingKey,
        onPressed: () {},
        child: const Icon(Icons.shopping_cart),
      ),
    );
  }

这里用了 spaceAround 让三行按钮均匀分布在屏幕上,这样从不同位置飞向购物车的效果会更明显。

封装按钮行组件

为了避免重复代码,把每一行按钮封装成一个方法:

  Widget buildButtonRow(List<int> indexes, List<String> labels) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(3, (i) {
        return ElevatedButton(
          key: buttonKeys[indexes[i]],
          onPressed: () => addToCart(buttonKeys[indexes[i]]),
          child: Text(labels[i]),
        );
      }),
    );
  }

🔧 代码技巧

  • List.generate 动态生成按钮,代码更简洁
  • 通过 indexes 参数传入 Key 的索引,灵活性更高
  • labels 参数让按钮文字可配置

实现加入购物车动画

现在来写核心的动画逻辑:

  void addToCart(GlobalKey sourceKey) {
    // 自定义飞入的 Widget 样式
    Widget piuWidget = Container(
      decoration: BoxDecoration(
        color: Colors.orange,
        borderRadius: BorderRadius.circular(8),
      ),
      child: const Icon(
        Icons.add_shopping_cart, 
        color: Colors.white, 
        size: 40
      ),
    );

这里我用了一个橙色的圆角容器包裹购物车图标,看起来更有质感。你可以根据自己的 UI 风格调整颜色和样式。

    // 计算悬浮按钮的中心坐标
    RenderBox box = floatingKey.currentContext!.findRenderObject() as RenderBox;
    var offset = box.localToGlobal(Offset.zero);
    Offset endOffset = Offset(
      offset.dx + box.size.width / 2,
      offset.dy + box.size.height / 2,
    );

这段坐标计算和前面一样,不过这次的目标是悬浮按钮。

    PiuAnimation.addAnimation(
      rootKey,
      piuWidget,
      endOffset,
      maxWidth: 80,
      minWidth: 30,
      millisecond: 1200,
      doSomethingBeginCallBack: () {
        print("商品飞入购物车");
      },
      doSomethingFinishCallBack: (success) {
        print("已加入购物车");
        // 这里可以更新购物车数量
        // setState(() { cartCount++; });
      },
    );
  }
}

🎯 参数调整说明

  • maxWidth: 80minWidth: 30:因为是图标,不需要太大
  • millisecond: 1200:1.2 秒的时长,快速但不仓促
  • 从屏幕四个角飞向中心的距离不同,但动画时长一样,所以速度会有差异,这反而让动画更自然

运行效果

现在运行项目,你会看到:

  • 点击任意按钮,都会有一个购物车图标从按钮位置飞向右下角的悬浮按钮
  • 从不同位置点击,飞行的轨迹和速度都不一样
  • 动画过程中有缩放效果,从大变小

这个效果可以直接用在实际项目里,只需要把按钮换成商品卡片,把图标换成商品图片就行。

鸿蒙平台适配实战

环境检查

在开始鸿蒙适配之前,先确认一下开发环境。打开终端,执行:

flutter --version

你应该能看到 Flutter 版本信息。如果你用的是支持鸿蒙的 Flutter SDK,会在输出里看到相关信息。

然后检查鸿蒙设备连接:

flutter devices

如果鸿蒙设备或模拟器连接正常,会在列表里显示出来。

运行到鸿蒙设备

确认设备连接后,运行项目:

flutter run -d <device-id>

<device-id> 替换成你的设备 ID(就是 flutter devices 命令输出的那个)。

📱 真机调试建议

  • 第一次运行建议用真机,模拟器可能有性能问题
  • 动画效果在真机上看起来更流畅
  • 如果遇到签名问题,检查一下鸿蒙开发者账号配置

性能优化技巧

在鸿蒙设备上运行时,我发现几个优化点:

1. 控制动画时长

鸿蒙设备的性能差异比较大,建议动画时长不要太长:

millisecond: 1200,  // 推荐 1000-1500ms

太长的动画在低端设备上可能会卡顿。

2. 简化飞入的 Widget

避免在 piuWidget 里使用复杂的嵌套结构:

// ❌ 不推荐:太复杂
Widget piuWidget = Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(...),
    boxShadow: [...],
  ),
  child: Column(
    children: [
      Image.network(...),  // 网络图片
      Text(...),
      // 更多嵌套...
    ],
  ),
);

// ✅ 推荐:简洁高效
Widget piuWidget = Container(
  color: Colors.orange,
  child: const Icon(Icons.shopping_cart, size: 40),
);

性能提示

  • 优先使用纯色背景,避免渐变和阴影
  • 用本地图标代替网络图片
  • 减少 Widget 嵌套层级

3. 防止重复触发

用户可能会快速连续点击,导致多个动画同时执行。加个开关控制一下:

bool _isAnimating = false;

void triggerAnimation() {
  if (_isAnimating) return;  // 如果正在动画,直接返回
  
  setState(() {
    _isAnimating = true;
  });

  PiuAnimation.addAnimation(
    rootKey,
    piuWidget,
    endOffset,
    doSomethingFinishCallBack: (success) {
      setState(() {
        _isAnimating = false;  // 动画结束,解除锁定
      });
    },
  );
}

这样可以避免多个动画叠加导致的性能问题。

鸿蒙特有的注意事项

权限配置

如果你的 piuWidget 里用到了网络图片,需要在鸿蒙项目配置文件里添加网络权限。

找到 ohos/entry/src/main/module.json5 文件,确保有这个配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

屏幕适配

鸿蒙设备的屏幕尺寸和分辨率差异很大,建议用相对尺寸:

maxWidth: MediaQuery.of(context).size.width * 0.8,  // 屏幕宽度的 80%
minWidth: MediaQuery.of(context).size.width * 0.1,  // 屏幕宽度的 10%

这样在不同设备上都能有比较好的效果。

性能分析

如果觉得动画不够流畅,可以用 Flutter 的性能分析工具:

flutter run --profile

这个命令会启动性能分析模式,可以看到帧率、渲染时间等信息。如果发现掉帧,就需要优化 Widget 结构了。

踩过的坑和解决方案

坑一:动画不显示

刚开始用的时候,我遇到过动画完全不显示的情况。点击按钮没有任何反应。

原因分析
rootKey 绑定的位置不对。我一开始把 rootKey 绑定到了 Scaffold 上,但动画的起点和终点都在 Scaffold 的 body 里。这样 piu_animation 找不到正确的坐标系。

解决方法
把 rootKey 绑定到一个包含所有动画元素的容器上。比如:

Scaffold(
  body: Container(
    key: rootKey,  // 绑定在这里
    child: Column(
      children: [
        // 起点按钮
        // 终点按钮
      ],
    ),
  ),
)

🔍 判断方法:如果动画不显示,先检查 rootKey 是否绑定到了正确的位置。一个简单的判断标准:rootKey 绑定的 Widget 必须是起点和终点的共同祖先。

坑二:终点位置不准

有时候动画能显示,但飞到的位置不对,偏离了目标位置。

原因分析
在 Widget 还没渲染完成的时候就计算坐标了。比如在 initStatebuild 方法里直接计算坐标,这时候 Widget 的位置信息还不准确。

解决方法
一定要在用户交互事件(比如 onPressed)里计算坐标:

ElevatedButton(
  onPressed: () {
    // 在这里计算坐标,确保 Widget 已经渲染完成
    RenderBox box = targetKey.currentContext!.findRenderObject() as RenderBox;
    var offset = box.localToGlobal(Offset.zero);
    // ...
  },
  child: const Text("点击"),
)

如果确实需要在页面加载时自动触发动画,可以用 WidgetsBinding.instance.addPostFrameCallback


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    // 这里可以安全地计算坐标
    triggerAnimation();
  });
}

坑三:动画卡顿

在一些低端设备上,动画会有明显的卡顿感。

原因分析
piuWidget 太复杂了。我之前用了一个包含网络图片、渐变背景、阴影效果的复杂 Widget,在动画过程中不断重绘,导致性能问题。

解决方法
简化 piuWidget 的结构,能用图标就不用图片,能用纯色就不用渐变:

// 简化前
Widget piuWidget = Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.blue, Colors.purple],
    ),
    boxShadow: [
      BoxShadow(blur: 10, color: Colors.black26),
    ],
  ),
  child: Image.network('https://...'),
);

// 简化后
Widget piuWidget = Container(
  color: Colors.blue,
  child: const Icon(Icons.shopping_cart, color: Colors.white),
);

性能提升非常明显。

坑四:多次点击导致动画叠加

用户快速点击按钮时,会同时触发多个动画,看起来很乱。

解决方法
前面提到过,用一个布尔变量控制:

bool _isAnimating = false;

void addToCart() {
  if (_isAnimating) {
    // 可以给用户一个提示
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请稍候...')),
    );
    return;
  }
  
  setState(() => _isAnimating = true);
  
  PiuAnimation.addAnimation(
    // ...
    doSomethingFinishCallBack: (success) {
      setState(() => _isAnimating = false);
    },
  );
}

坑五:在 ListView 中使用时的问题

如果在 ListView 的 item 里触发动画,有时候会遇到坐标计算错误的问题。

原因分析
ListView 的 item 可能还没有完全渲染,或者在滚动过程中位置发生了变化。

解决方法
确保在点击事件里实时计算坐标,不要提前缓存:

ListView.builder(
  itemBuilder: (context, index) {
    return ListTile(
      onTap: () {
        // 每次点击都重新计算坐标
        RenderBox box = cartKey.currentContext!.findRenderObject() as RenderBox;
        var offset = box.localToGlobal(Offset.zero);
        Offset endOffset = Offset(
          offset.dx + box.size.width / 2,
          offset.dy + box.size.height / 2,
        );
        
        PiuAnimation.addAnimation(rootKey, piuWidget, endOffset);
      },
    );
  },
)

进阶技巧

自定义动画 Widget 样式

除了简单的图标,你还可以做一些更有创意的效果:

带数字的徽章

Widget piuWidget = Container(
  width: 60,
  height: 60,
  decoration: BoxDecoration(
    color: Colors.red,
    shape: BoxShape.circle,
  ),
  child: const Center(
    child: Text(
      '+1',
      style: TextStyle(
        color: Colors.white,
        fontSize: 24,
        fontWeight: FontWeight.bold,
      ),
    ),
  ),
);

这个效果适合显示"加入购物车 +1"这样的提示。

商品缩略图

Widget piuWidget = ClipRRect(
  borderRadius: BorderRadius.circular(8),
  child: Image.asset(
    'assets/product_thumb.png',
    width: 80,
    height: 80,
    fit: BoxFit.cover,
  ),
);

💡 注意:如果用图片,建议用本地资源(Image.asset),不要用网络图片(Image.network),因为网络图片加载慢,可能导致动画开始时图片还没加载出来。

连续动画效果

有时候需要连续触发多个动画,比如批量加入购物车:

void addMultipleToCart(List<int> productIds) {
  for (int i = 0; i < productIds.length; i++) {
    Future.delayed(Duration(milliseconds: i * 300), () {
      addToCart(productIds[i]);
    });
  }
}

每个动画间隔 300ms,看起来像是依次飞入,效果很酷。

结合音效

动画配合音效会更有沉浸感:

import 'package:audioplayers/audioplayers.dart';

final player = AudioPlayer();

void addToCartWithSound() {
  player.play(AssetSource('sounds/pop.mp3'));
  
  PiuAnimation.addAnimation(
    rootKey,
    piuWidget,
    endOffset,
    // ...
  );
}

一个轻快的"pop"音效配合飞入动画,用户体验会好很多。

动态调整动画参数

根据距离动态调整动画时长,距离远的飞得慢一点,距离近的飞得快一点:

void addToCart(GlobalKey sourceKey) {
  // 计算起点和终点的距离
  RenderBox sourceBox = sourceKey.currentContext!.findRenderObject() as RenderBox;
  RenderBox targetBox = cartKey.currentContext!.findRenderObject() as RenderBox;
  
  var sourceOffset = sourceBox.localToGlobal(Offset.zero);
  var targetOffset = targetBox.localToGlobal(Offset.zero);
  
  double distance = (targetOffset - sourceOffset).distance;
  
  // 根据距离计算动画时长(距离越远,时长越长)
  int duration = (distance * 2).toInt().clamp(800, 2000);
  
  PiuAnimation.addAnimation(
    rootKey,
    piuWidget,
    endOffset,
    millisecond: duration,
  );
}

这样动画速度会更统一,看起来更自然。

写在最后

用了 piu_animation 一段时间,整体感觉还是很不错的。代码简洁,效果流畅,关键是支持鸿蒙,省去了很多适配的麻烦。

适合使用的场景

  • 电商类 App 的购物车动画(这是最常见的)
  • 社交类 App 的点赞、收藏动效
  • 工具类 App 的操作反馈动画
  • 任何需要"飞入"效果的场景

不太适合的场景

  • 需要复杂路径动画的场景(piu_animation 只支持直线飞入)
  • 需要精确控制动画曲线的场景(库内置的曲线不可自定义)
  • 需要暂停、恢复动画的场景(不支持动画控制)

如果你的需求比较简单,就是想要一个从 A 飞到 B 的效果,那 piu_animation 绝对是个好选择。如果需求比较复杂,可能需要考虑用 Flutter 原生的 AnimationController 自己实现。

一些使用建议

1. 动画时长的选择
根据我的实践经验,1000-1500ms 是最舒服的时长。太快看不清,太慢又显得拖沓。你可以根据实际情况微调,但不建议超过 2 秒。

2. Widget 样式的设计
飞入的 Widget 不要太复杂,简单的图标或图片就够了。如果想要更丰富的效果,可以加个圆角、背景色,但不要加太多阴影和渐变,会影响性能。

3. 防止重复触发
一定要加个开关控制,防止用户快速点击导致多个动画叠加。这不仅影响视觉效果,还可能导致性能问题。

4. 鸿蒙适配注意事项
在鸿蒙设备上测试时,多试几个不同配置的设备。低端设备可能需要降低动画时长或简化 Widget 结构。

后续计划

piu_animation 还有个 loading 动画功能,可以在飞入过程中暂停并执行异步任务。这个功能特别适合需要网络请求的场景,比如真正的加入购物车操作。

下次有机会再写一篇文章,专门讲讲 loading 动画的使用,包括成功和失败两种情况的处理。

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐