Flutter confetti彩纸动画的鸿蒙化适配与实战指南

📅 写作时间:2026-04-29
🏷️ 标签:Flutter OpenHarmony 动画 confetti1


🌟 开篇引导

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


嗨喽铁汁们!👋 我是上海某本科大学计算机专业的大一学生,最近在用Flutter for OpenHarmony开发健康运动App。

说实话,之前我一直觉得App的动画都是"花里胡哨"的东西,没什么实际用处…直到我自己做了一个成就解锁功能,每次达成成就就弹出一个干巴巴的对话框,连个庆祝效果都没有,那成就感…真的是一言难尽啊!😭

然后我就发现了confetti这个神奇的库!加上之后,每次解锁成就,满屏的彩纸飘落,那感觉…真的是太爽了!今天就来给各位铁汁们详细讲讲这个库怎么用!


📱 一、功能引入:为什么要用彩纸动画?

1.1 解决什么问题?

说实话,没有动画的成就解锁真的很无聊:

  • 😤 达成成就就一个对勾图标,一点仪式感都没有
  • 😤 用户不知道发生了什么,完全没有反馈
  • 😤 没有"惊喜"的感觉,成就达成跟普通通知没区别
  • 😤 一点都不想分享,因为截图不好看

所以彩纸动画必须加!

1.2 confetti能做什么?

confetti库可以实现:

效果 说明 场景
🎊 彩纸飘落 五颜六色的纸片从屏幕顶部落下 成就解锁
🎉 庆祝爆发 从屏幕中心向四周爆发 大目标达成
⭐ 星星闪烁 金色星星闪烁效果 VIP升级
🎁 礼物效果 礼盒开启效果 连续打卡奖励

1.3 鸿蒙场景下的痛点

在鸿蒙上用动画,坑也不少:

  1. 性能问题 - 动画掉帧,鸿蒙设备GPU渲染有差异
  2. 内存泄漏 - 动画结束后没有正确释放资源
  3. 兼容性 - 部分Widget在动画过程中不可交互

📦 二、环境与依赖配置

2.1 pubspec.yaml

# pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  # ========== 动画相关 ==========
  confetti: ^0.7.0           # 彩纸动画核心库
  flutter_animate: ^4.5.0     # Flutter动画增强
  
  # ========== 状态管理 ==========
  flutter_bloc: ^8.1.6
  
  # ========== OpenHarmony兼容 ==========
  permission_handler_ohos: any

2.2 依赖说明

依赖 版本 用途 必须
confetti ^0.7.0 彩纸动画核心
flutter_animate ^4.5.0 动画增强辅助

💻 三、分步实现完整代码

3.1 成就动画Widget

// lib/widgets/achievement_unlock_animation.dart

import 'package:flutter/material.dart';
import 'package:confetti/confetti.dart';

/// 成就解锁动画组件
/// 在达成成就时显示,带有彩纸庆祝效果
class AchievementUnlockAnimation extends StatefulWidget {
  /// 成就名称
  final String achievementName;
  
  /// 成就图标
  final String achievementIcon;
  
  /// 成就描述
  final String description;
  
  /// 关闭回调
  final VoidCallback onClose;

  const AchievementUnlockAnimation({
    super.key,
    required this.achievementName,
    required this.achievementIcon,
    required this.description,
    required this.onClose,
  });

  
  State<AchievementUnlockAnimation> createState() => 
      _AchievementUnlockAnimationState();
}

class _AchievementUnlockAnimationState 
    extends State<AchievementUnlockAnimation> {
  /// Confetti控制器
  /// 这个控制器非常重要!控制彩纸的播放
  late ConfettiController _confettiController;
  
  /// 是否显示动画
  bool _showContent = false;
  
  /// 动画缩放值(用于入场动画)
  double _scale = 0.0;

  
  void initState() {
    super.initState();
    
    // 创建Confetti控制器
    // duration: 彩纸持续时间,这里设置3秒
    _confettiController = ConfettiController(
      duration: const Duration(seconds: 3),
    );
    
    // 立即开始彩纸动画!
    // 💡 关键:彩纸要先开始,然后再显示内容
    _confettiController.play();
    
    // 延迟显示内容,制造"先听到声音再看到效果"的感觉
    Future.delayed(const Duration(milliseconds: 300), () {
      if (mounted) {
        setState(() {
          _showContent = true;
          _scale = 1.0;
        });
      }
    });
  }

  
  void dispose() {
    // 重要!释放资源,防止内存泄漏
    _confettiController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Material(
      color: Colors.black54,  // 半透明黑色背景
      child: Stack(
        alignment: Alignment.center,
        children: [
          // ===== 1. 彩纸动画层 =====
          // 放在最上层,从顶部中心落下
          Align(
            alignment: Alignment.topCenter,
            child: ConfettiWidget(
              // 彩纸控制器
              confettiController: _confettiController,
              
              // 彩纸下落方向:向下的弧线
              blastDirectionality: BlastDirectionality.explosive,
              
              // 是否自动播放:否(我们已经手动控制了)
              shouldLoop: false,
              
              // 彩纸颜色列表
              // 🎨 可以自定义颜色,这里用了彩虹色系
              colors: const [
                Color(0xFFFF6B6B),  // 红色
                Color(0xFF4ECDC4),  // 青色
                Color(0xFFFFE66D),  // 黄色
                Color(0xFF95E1D3),  // 绿色
                Color(0xFFF38181),  // 粉色
                Color(0xFFAA96DA),  // 紫色
              ],
              
              // 最小尺寸
              minimumSize: const Size(10, 10),
              
              // 最大尺寸
              maximumSize: const Size(20, 20),
              
              // 重力系数(越大下落越快)
              gravity: 0.2,
              
              // 每秒产生多少个彩纸
              emissionFrequency: 0.05,
              
              // 初始数量
              numberOfParticles: 50,
              
              // 彩纸展开程度
              spreadAngle: 10,
              
              // 旋转速度
              rotate: true,
              
              // 闪烁效果
              shimmer: true,
            ),
          ),

          // ===== 2. 成就卡片内容 =====
          // 带有缩放入场动画
          AnimatedScale(
            scale: _scale,
            duration: const Duration(milliseconds: 400),
            curve: Curves.elasticOut,  // 弹性曲线,更有弹性感
            child: AnimatedOpacity(
              opacity: _showContent ? 1.0 : 0.0,
              duration: const Duration(milliseconds: 300),
              child: _buildAchievementCard(),
            ),
          ),
        ],
      ),
    );
  }

  /// 构建成就卡片
  Widget _buildAchievementCard() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 40),
      padding: const EdgeInsets.all(32),
      decoration: BoxDecoration(
        // 渐变背景
        gradient: const LinearGradient(
          colors: [
            Color(0xFFFF6B6B),  // 金红渐变
            Color(0xFFFFE66D),
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        // 圆角
        borderRadius: BorderRadius.circular(24),
        // 阴影
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.3),
            blurRadius: 20,
            offset: const Offset(0, 10),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // ===== 成就图标 =====
          Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
              boxShadow: [
                BoxShadow(
                  color: Colors.white.withOpacity(0.5),
                  blurRadius: 20,
                  spreadRadius: 5,
                ),
              ],
            ),
            child: Center(
              child: Text(
                widget.achievementIcon,
                style: const TextStyle(fontSize: 50),
              ),
            ),
          ),
          const SizedBox(height: 24),

          // ===== 标题 =====
          const Text(
            '🎉 成就解锁!',
            style: TextStyle(
              fontSize: 16,
              color: Colors.white70,
            ),
          ),
          const SizedBox(height: 8),

          // ===== 成就名称 =====
          Text(
            widget.achievementName,
            textAlign: TextAlign.center,
            style: const TextStyle(
              fontSize: 28,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
          const SizedBox(height: 12),

          // ===== 描述 =====
          Text(
            widget.description,
            textAlign: TextAlign.center,
            style: const TextStyle(
              fontSize: 14,
              color: Colors.white70,
            ),
          ),
          const SizedBox(height: 32),

          // ===== 关闭按钮 =====
          ElevatedButton(
            onPressed: widget.onClose,
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.white,
              foregroundColor: const Color(0xFFFF6B6B),
              padding: const EdgeInsets.symmetric(
                horizontal: 40,
                vertical: 14,
              ),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(30),
              ),
            ),
            child: const Text(
              '太棒了!',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3.2 使用示例

// lib/pages/achievement_demo_page.dart

import 'package:flutter/material.dart';
import '../widgets/achievement_unlock_animation.dart';

/// 成就展示页面
class AchievementDemoPage extends StatefulWidget {
  const AchievementDemoPage({super.key});

  
  State<AchievementDemoPage> createState() => _AchievementDemoPageState();
}

class _AchievementDemoPageState extends State<AchievementDemoPage> {
  /// 当前解锁的成就
  Map<String, dynamic>? _currentAchievement;
  
  /// 预设的成就列表
  final List<Map<String, dynamic>> _achievements = [
    {
      'name': '初次打卡',
      'icon': '🎯',
      'description': '完成第一次运动打卡!',
    },
    {
      'name': '连续7天',
      'icon': '🔥',
      'description': '坚持运动7天,习惯正在养成!',
    },
    {
      'name': '步数破万',
      'icon': '👟',
      'description': '单日步数突破10000!',
    },
    {
      'name': '早起达人',
      'icon': '🌅',
      'description': '连续5天在7点前打卡!',
    },
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('成就展示'),
      ),
      body: Stack(
        children: [
          // 成就列表
          ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: _achievements.length,
            itemBuilder: (context, index) {
              final achievement = _achievements[index];
              return _buildAchievementCard(achievement, index);
            },
          ),
          
          // 动画层
          if (_currentAchievement != null)
            Positioned.fill(
              child: AchievementUnlockAnimation(
                achievementName: _currentAchievement!['name'],
                achievementIcon: _currentAchievement!['icon'],
                description: _currentAchievement!['description'],
                onClose: () {
                  setState(() {
                    _currentAchievement = null;
                  });
                },
              ),
            ),
        ],
      ),
    );
  }

  /// 构建成就卡片
  Widget _buildAchievementCard(Map<String, dynamic> achievement, int index) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        contentPadding: const EdgeInsets.all(16),
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: Colors.amber.withOpacity(0.2),
            shape: BoxShape.circle,
          ),
          child: Center(
            child: Text(
              achievement['icon'],
              style: const TextStyle(fontSize: 28),
            ),
          ),
        ),
        title: Text(
          achievement['name'],
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Text(achievement['description']),
        trailing: ElevatedButton(
          onPressed: () => _unlockAchievement(achievement),
          child: const Text('解锁'),
        ),
      ),
    );
  }

  /// 解锁成就(触发动画)
  void _unlockAchievement(Map<String, dynamic> achievement) {
    setState(() {
      _currentAchievement = achievement;
    });
  }
}

3.3 高级配置:自定义彩纸效果

/// 彩纸效果配置类
/// 封装常用的彩纸效果配置
class ConfettiPresets {
  /// 默认庆祝效果
  /// 比较温和,适合一般成就
  static ConfettiController createDefaultController() {
    return ConfettiController(duration: const Duration(seconds: 3));
  }

  /// 盛大庆祝效果
  /// 持续时间长,粒子多,适合大成就
  static ConfettiController createGrandController() {
    return ConfettiController(duration: const Duration(seconds: 5));
  }

  /// 快速庆祝效果
  /// 短促有力,适合小成就
  static ConfettiController createQuickController() {
    return ConfettiController(duration: const Duration(seconds: 1));
  }

  /// 彩虹色系
  static const List<Color> rainbowColors = [
    Color(0xFFFF6B6B),
    Color(0xFFFFE66D),
    Color(0xFF4ECDC4),
    Color(0xFF95E1D3),
    Color(0xFFF38181),
    Color(0xFFAA96DA),
  ];

  /// 金色系(适合金币/财富类成就)
  static const List<Color> goldColors = [
    Color(0xFFFFD700),
    Color(0xFFFFA500),
    Color(0xFFFFDAB9),
    Color(0xFFFFC125),
  ];

  /// 粉色系(适合女性向/可爱类成就)
  static const List<Color> pinkColors = [
    Color(0xFFFF69B4),
    Color(0xFFFF1493),
    Color(0xFFFFB6C1),
    Color(0xFFFF85C1),
  ];

  /// 科技感配色(适合数据/科技类成就)
  static const List<Color> techColors = [
    Color(0xFF00D4FF),
    Color(0xFF0066FF),
    Color(0xFF7B2FFF),
    Color(0xFFFF00FF),
  ];
}

😤 四、开发踩坑与挫折

4.1 坑一:彩纸看不见!

问题描述
动画播放了,但什么都看不见!

排查过程

  1. 检查控制器是否正确创建
  2. 检查ConfettiController是否调用了play()
  3. 检查Alignment是否正确

解决方案

// ❌ 错误:Alignment不对,彩纸可能在屏幕外面
child: ConfettiWidget(
  confettiController: _confettiController,
  blastDirectionality: BlastDirectionality.explosive,
  // 如果设置成 bottomCenter,彩纸会从底部升起
  alignment: Alignment.bottomCenter,  // ❌ 不对
)

// ✅ 正确:彩纸从顶部中心落下
child: ConfettiWidget(
  confettiController: _confettiController,
  blastDirectionality: BlastDirectionality.explosive,
  alignment: Alignment.topCenter,  // ✅ 从顶部
)

// 或者用 directional 模式
blastDirectionality: BlastDirectionality.directional,
blastDirection: 3.14,  // 向下(弧度)

4.2 坑二:内存泄漏!

问题描述
解锁几次成就后,应用变卡了,内存占用飙升!

原因分析
ConfettiController没有在dispose中释放!

解决方案

// ❌ 错误:没有释放资源
class _MyWidgetState extends State<MyWidget> {
  late ConfettiController _confettiController;
  
  
  void initState() {
    super.initState();
    _confettiController = ConfettiController(...);
  }
  
  // ❌ 缺少 dispose 方法!
}

// ✅ 正确:必须释放资源
class _MyWidgetState extends State<MyWidget> {
  late ConfettiController _confettiController;
  
  
  void initState() {
    super.initState();
    _confettiController = ConfettiController(...);
  }
  
  
  void dispose() {
    // 💡 重要!必须调用 dispose
    _confettiController.dispose();
    super.dispose();
  }
}

4.3 坑三:动画结束后Widget还在!

问题描述
动画播完了,但Widget还覆盖在屏幕上!

原因分析
没有监听动画结束事件!

解决方案

// 方法1:使用 Duration 自动关闭
Future.delayed(const Duration(seconds: 3), () {
  if (mounted) {
    widget.onClose();  // 3秒后自动关闭
  }
});

// 方法2:监听控制器状态
_confettiController.addListener(() {
  if (_confettiController.state == ConfettiControllerState.stopped) {
    // 动画停止,可以关闭了
  }
});

// 方法3:使用 AnimationController 辅助
class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  
  
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    );
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        widget.onClose();
      }
    });
    _animationController.forward();
  }
}

📱 五、鸿蒙专属适配方案

5.1 性能优化

/// 鸿蒙设备上的性能优化配置
class ConfettiWidget optimizedConfetti() {
  return ConfettiWidget(
    confettiController: _confettiController,
    
    // ===== 性能优化参数 =====
    
    // 减少粒子数量(鸿蒙设备GPU较弱)
    numberOfParticles: 30,  // 默认50,鸿蒙上用30
    
    // 降低发射频率
    emissionFrequency: 0.1,  // 默认0.05
    
    // 减小粒子尺寸
    minimumSize: const Size(5, 5),
    maximumSize: const Size(10, 10),
    
    // 关闭不必要的效果
    rotate: false,  // 关闭旋转
    shimmer: false,  // 关闭闪烁
    
    // 提高重力,加快下落速度,减少屏幕停留时间
    gravity: 0.4,
  );
}

5.2 权限配置

<!-- AndroidManifest.xml -->
<!-- confetti本身不需要特殊权限,但如果有拍照等功能需要配置 -->
<manifest>
  <!-- 如果使用动画截图分享 -->
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

🎯 六、最终实现效果

6.1 功能验证

在这里插入图片描述
在这里插入图片描述

验证结果

效果 Android iOS 鸿蒙
彩纸飘落
颜色显示
动画流畅度 ⚠️ 需优化
内存占用
自动关闭

6.2 性能对比

设备 粒子数 帧率
Android高端机 50 60fps
iPhone 12 50 60fps
鸿蒙设备 30 50fps

📚 七、个人学习总结与心得

7.1 收获

搞完彩纸动画这个功能,我学到了很多:

  1. Flutter动画原理 - AnimationController + ConfettiController的配合
  2. 资源管理 - 所有资源都要在dispose中释放
  3. 性能优化 - 不同平台的GPU性能差异很大
  4. 用户体验 - 动画不是花里胡哨,是用户体验的重要组成部分!

7.2 踩坑反思

最大的教训就是:

动画虽美,但性能第一!
在真机上测试之前,一切模拟器上的流畅都是假的!

7.3 后续计划

  • 加入更多动画效果(爆炸、旋转)
  • 支持自定义粒子形状
  • 加入背景音乐
  • 支持录制分享

📎 相关资源

资源 链接
confetti官方文档 https://pub.dev/packages/confetti
flutter_animate https://pub.dev/packages/flutter_animate

好了!confetti彩纸动画就讲到这里!

**如果觉得有帮助,请一键三连!**🙏

📅 发布日期:2026-04-29
✍️ 作者:上海某本科大学大一学生
🏷️ 标签:Flutter / OpenHarmony / confetti / 动画效果


往期推荐

  • 「Flutter三方库sqflite的鸿蒙化适配与实战指南」
  • 「Flutter三方库flutter_bloc的鸿蒙化适配与实战指南」
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐