【flutter for open harmony】第三方库Flutter成就解锁彩纸动画的鸿蒙化适配与实战指南

Flutter成就解锁彩纸动画的鸿蒙化适配与实战指南

📅 写作时间:2026-04-29

🏷️ 标签:Flutter OpenHarmony 成就系统 动画效果


🌟 开篇引导

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


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

你们有没有这种感觉:App里的成就系统,做了跟没做一样,解锁了连个提示都没有,完全没有"成就感"!😭

所以我决定做一个超有仪式感的成就解锁动画!解锁成就的时候,满屏彩纸飘落,配合弹窗卡片,那感觉...一个字:爽!今天就来给各位铁汁们详细讲讲怎么实现!


📱 一、功能引入:为什么要做成就解锁动画?

1.1 解决什么问题?

没有动画的成就系统:

  • 😤 解锁了完全没感觉
  • 😤 用户不知道发生了什么
  • 😤 没有分享的欲望
  • 😤 成就徽章积灰了也没人点

有动画的成就系统:

  • 🎉 满满的仪式感!
  • 🎊 满屏彩纸庆祝
  • 🏆 让人想截图分享
  • 🔥 解锁更多成就的动力!

1.2 动画效果设计

效果 说明 触发时机
🎊 彩纸飘落 confetti动画 成就解锁时
🏆 徽章展示 成就卡片弹窗 成就解锁时
⭐ 闪光效果 背景闪烁 大成就解锁
🔔 音效提示 庆祝音效 成就解锁时

💻 二、完整代码实现

2.1 成就数据模型

dart 复制代码
// lib/models/achievement_model.dart

import 'package:equatable/equatable.dart';

/// 成就类型
enum AchievementType {
  first,       // 🏅 首次类
  streak,     // 🔥 连续类
  milestone,  // 🎯 里程碑类
  special,    // ⭐ 特殊类
}

/// 成就模型
class Achievement extends Equatable {
  final String id;              // 唯一ID
  final String name;            // 成就名称
  final String icon;            // 图标
  final String description;     // 描述
  final AchievementType type;   // 类型
  final int? targetValue;       // 目标值
  final int currentValue;       // 当前进度
  final bool isUnlocked;        // 是否已解锁
  final DateTime? unlockedAt;    // 解锁时间
  final int order;              // 显示顺序

  const Achievement({
    required this.id,
    required this.name,
    required this.icon,
    required this.description,
    required this.type,
    this.targetValue,
    this.currentValue = 0,
    this.isUnlocked = false,
    this.unlockedAt,
    this.order = 0,
  });

  /// 完成百分比
  double get progress {
    if (targetValue == null || targetValue == 0) return 0;
    return (currentValue / targetValue!).clamp(0.0, 1.0);
  }

  /// 获取类型名称
  String get typeName {
    switch (type) {
      case AchievementType.first: return '首次';
      case AchievementType.streak: return '连续';
      case AchievementType.milestone: return '里程碑';
      case AchievementType.special: return '特殊';
    }
  }

  /// 转换为Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'icon': icon,
      'description': description,
      'type': type.name,
      'target_value': targetValue,
      'current_value': currentValue,
      'is_unlocked': isUnlocked ? 1 : 0,
      'unlocked_at': unlockedAt?.toIso8601String(),
      'order_num': order,
    };
  }

  /// 从Map恢复
  factory Achievement.fromMap(Map<String, dynamic> map) {
    return Achievement(
      id: map['id'] as String,
      name: map['name'] as String,
      icon: map['icon'] as String,
      description: map['description'] as String,
      type: AchievementType.values.firstWhere(
        (e) => e.name == map['type'],
        orElse: () => AchievementType.special,
      ),
      targetValue: map['target_value'] as int?,
      currentValue: map['current_value'] as int? ?? 0,
      isUnlocked: map['is_unlocked'] == 1,
      unlockedAt: map['unlocked_at'] != null
          ? DateTime.parse(map['unlocked_at'] as String)
          : null,
      order: map['order_num'] as int? ?? 0,
    );
  }

  @override
  List<Object?> get props => [id, name, icon, description, type, targetValue, currentValue, isUnlocked, unlockedAt, order];
}

2.2 成就解锁动画组件

dart 复制代码
// lib/widgets/achievement_unlock_dialog.dart

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

/// 成就解锁弹窗
/// 带有彩纸动画效果
class AchievementUnlockDialog extends StatefulWidget {
  /// 要解锁的成就
  final Achievement achievement;
  
  /// 关闭回调
  final VoidCallback onClose;
  
  /// 是否是连续解锁(多个成就)
  final bool isSequential;
  
  /// 下一个成就(如果有)
  final Achievement? nextAchievement;

  const AchievementUnlockDialog({
    super.key,
    required this.achievement,
    required this.onClose,
    this.isSequential = false,
    this.nextAchievement,
  });

  @override
  State<AchievementUnlockDialog> createState() => _AchievementUnlockDialogState();
}

class _AchievementUnlockDialogState extends State<AchievementUnlockDialog>
    with TickerProviderStateMixin {
  /// 彩纸控制器
  late ConfettiController _confettiController;
  
  /// 缩放动画控制器
  late AnimationController _scaleController;
  late Animation<double> _scaleAnimation;
  
  /// 闪烁动画控制器
  late AnimationController _glowController;
  
  /// 是否显示内容
  bool _showContent = false;
  
  /// 旋转角度
  double _rotation = 0;

  @override
  void initState() {
    super.initState();
    
    // 初始化彩纸控制器
    _confettiController = ConfettiController(duration: const Duration(seconds: 3));
    
    // 初始化缩放动画
    _scaleController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 600),
    );
    _scaleAnimation = CurvedAnimation(
      parent: _scaleController,
      curve: Curves.elasticOut,
    );
    
    // 初始化发光动画
    _glowController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1500),
    )..repeat(reverse: true);
    
    // 开始动画序列
    _startAnimationSequence();
  }

  @override
  void dispose() {
    _confettiController.dispose();
    _scaleController.dispose();
    _glowController.dispose();
    super.dispose();
  }

  /// 开始动画序列
  Future<void> _startAnimationSequence() async {
    // 1. 等待一小段时间
    await Future.delayed(const Duration(milliseconds: 200));
    
    // 2. 开始彩纸动画
    _confettiController.play();
    
    // 3. 显示内容
    if (mounted) {
      setState(() => _showContent = true);
    }
    
    // 4. 开始缩放动画
    _scaleController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.black.withOpacity(0.7),
      child: Stack(
        children: [
          // ===== 1. 彩纸动画层 =====
          Positioned.fill(
            child: _buildConfetti(),
          ),
          
          // ===== 2. 内容层 =====
          Center(
            child: _showContent
                ? _buildContent()
                : const SizedBox.shrink(),
          ),
        ],
      ),
    );
  }

  /// 彩纸动画
  Widget _buildConfetti() {
    return ConfettiWidget(
      confettiController: _confettiController,
      blastDirectionality: BlastDirectionality.explosive,
      shouldLoop: false,
      colors: _getConfettiColors(),
      numberOfParticles: 50,
      gravity: 0.2,
      emissionFrequency: 0.05,
      minimumSize: const Size(10, 10),
      maximumSize: const Size(20, 20),
      rotate: true,
      shimmer: true,
    );
  }

  /// 获取彩纸颜色
  List<Color> _getConfettiColors() {
    // 根据成就类型选择颜色
    switch (widget.achievement.type) {
      case AchievementType.first:
        return const [
          Color(0xFFFFD700),  // 金色
          Color(0xFFFFA500),  // 橙色
          Color(0xFFFFE066),  // 浅金
        ];
      case AchievementType.streak:
        return const [
          Color(0xFFFF6B6B),  // 红色
          Color(0xFFFF8E53),  // 橙红
          Color(0xFFFFE66D),  // 黄色
        ];
      case AchievementType.milestone:
        return const [
          Color(0xFF4ECDC4),  // 青色
          Color(0xFF95E1D3),  // 浅青
          Color(0xFF00B4D8),  // 蓝色
        ];
      case AchievementType.special:
        return const [
          Color(0xFFAA96DA),  // 紫色
          Color(0xFFFCBF49),  // 金黄
          Color(0xFFEF476F),  // 粉色
        ];
    }
  }

  /// 内容区域
  Widget _buildContent() {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 32),
        padding: const EdgeInsets.all(24),
        decoration: _buildBoxDecoration(),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 装饰光环
            _buildGlowRing(),
            
            const SizedBox(height: 20),
            
            // 解锁标题
            const Text(
              '🎉 成就解锁!',
              style: TextStyle(
                fontSize: 18,
                color: Colors.white70,
              ),
            ),
            
            const SizedBox(height: 16),
            
            // 成就图标
            _buildAchievementIcon(),
            
            const SizedBox(height: 16),
            
            // 成就名称
            Text(
              widget.achievement.name,
              textAlign: TextAlign.center,
              style: const TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
            
            const SizedBox(height: 12),
            
            // 成就描述
            Text(
              widget.achievement.description,
              textAlign: TextAlign.center,
              style: const TextStyle(
                fontSize: 14,
                color: Colors.white70,
              ),
            ),
            
            const SizedBox(height: 24),
            
            // 解锁时间
            if (widget.achievement.unlockedAt != null)
              Text(
                '解锁于 ${_formatDate(widget.achievement.unlockedAt!)}',
                style: const TextStyle(
                  fontSize: 12,
                  color: Colors.white54,
                ),
              ),
            
            const SizedBox(height: 24),
            
            // 关闭按钮
            _buildCloseButton(),
          ],
        ),
      ),
    );
  }

  /// 背景装饰
  BoxDecoration _buildBoxDecoration() {
    return BoxDecoration(
      gradient: LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [
          _getAccentColor().withOpacity(0.8),
          _getAccentColor().withOpacity(0.4),
        ],
      ),
      borderRadius: BorderRadius.circular(24),
      border: Border.all(
        color: Colors.white.withOpacity(0.3),
        width: 2,
      ),
      boxShadow: [
        BoxShadow(
          color: _getAccentColor().withOpacity(0.5),
          blurRadius: 30,
          spreadRadius: 5,
        ),
      ],
    );
  }

  /// 发光光环
  Widget _buildGlowRing() {
    return AnimatedBuilder(
      animation: _glowController,
      builder: (context, child) {
        return Container(
          width: 120,
          height: 120,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            boxShadow: [
              BoxShadow(
                color: _getAccentColor().withOpacity(0.3 + _glowController.value * 0.3),
                blurRadius: 20 + _glowController.value * 10,
                spreadRadius: 5 + _glowController.value * 5,
              ),
            ],
          ),
        );
      },
    );
  }

  /// 成就图标
  Widget _buildAchievementIcon() {
    return AnimatedBuilder(
      animation: _glowController,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotation,
          child: Container(
            width: 80,
            height: 80,
            decoration: BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
              boxShadow: [
                BoxShadow(
                  color: _getAccentColor().withOpacity(0.5),
                  blurRadius: 15,
                  spreadRadius: 2,
                ),
              ],
            ),
            child: Center(
              child: Text(
                widget.achievement.icon,
                style: const TextStyle(fontSize: 40),
              ),
            ),
          ),
        );
      },
    );
  }

  /// 关闭按钮
  Widget _buildCloseButton() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (widget.nextAchievement != null) ...[
          TextButton(
            onPressed: () {
              // 显示下一个成就
              // TODO: 实现连续解锁
            },
            child: const Text(
              '下一个 →',
              style: TextStyle(color: Colors.white70),
            ),
          ),
          const SizedBox(width: 16),
        ],
        ElevatedButton(
          onPressed: widget.onClose,
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.white,
            foregroundColor: _getAccentColor(),
            padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(24),
            ),
          ),
          child: const Text(
            '太棒了!',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
      ],
    );
  }

  /// 获取主题色
  Color _getAccentColor() {
    switch (widget.achievement.type) {
      case AchievementType.first:
        return const Color(0xFFFFD700);
      case AchievementType.streak:
        return const Color(0xFFFF6B6B);
      case AchievementType.milestone:
        return const Color(0xFF4ECDC4);
      case AchievementType.special:
        return const Color(0xFFAA96DA);
    }
  }

  /// 格式化日期
  String _formatDate(DateTime date) {
    return '${date.month}月${date.day}日 ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
  }
}

2.3 成就服务

dart 复制代码
// lib/services/achievement_service.dart

import '../models/achievement_model.dart';
import 'database_service.dart';

/// 成就服务
class AchievementService {
  static final AchievementService _instance = AchievementService._internal();
  static AchievementService get instance => _instance;

  AchievementService._internal();

  final DatabaseService _databaseService = DatabaseService.instance;

  /// 获取所有成就
  Future<List<Achievement>> getAllAchievements() async {
    final db = await _databaseService.database;
    final List<Map<String, dynamic>> maps = await db.query(
      'achievements',
      orderBy: 'order_num ASC',
    );
    return maps.map((map) => Achievement.fromMap(map)).toList();
  }

  /// 获取已解锁的成就
  Future<List<Achievement>> getUnlockedAchievements() async {
    final db = await _databaseService.database;
    final List<Map<String, dynamic>> maps = await db.query(
      'achievements',
      where: 'is_unlocked = ?',
      whereArgs: [1],
      orderBy: 'unlocked_at DESC',
    );
    return maps.map((map) => Achievement.fromMap(map)).toList();
  }

  /// 解锁成就
  Future<Achievement?> unlockAchievement(String id) async {
    final db = await _databaseService.database;
    
    // 检查是否已解锁
    final existing = await db.query(
      'achievements',
      where: 'id = ?',
      whereArgs: [id],
    );
    
    if (existing.isEmpty) return null;
    if (existing.first['is_unlocked'] == 1) return null;  // 已解锁
    
    // 更新为已解锁
    await db.update(
      'achievements',
      {
        'is_unlocked': 1,
        'unlocked_at': DateTime.now().toIso8601String(),
      },
      where: 'id = ?',
      whereArgs: [id],
    );
    
    // 返回更新后的成就
    final updated = await db.query(
      'achievements',
      where: 'id = ?',
      whereArgs: [id],
    );
    
    return Achievement.fromMap(updated.first);
  }

  /// 更新成就进度
  Future<void> updateProgress(String id, int currentValue) async {
    final db = await _databaseService.database;
    
    await db.update(
      'achievements',
      {'current_value': currentValue},
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  /// 检查是否可以解锁
  Future<Achievement?> checkAndUnlock(String id) async {
    final achievements = await getAllAchievements();
    final achievement = achievements.firstWhere(
      (a) => a.id == id,
      orElse: () => throw Exception('成就不存在'),
    );
    
    // 检查是否达到解锁条件
    if (achievement.targetValue != null &&
        achievement.currentValue >= achievement.targetValue! &&
        !achievement.isUnlocked) {
      return await unlockAchievement(id);
    }
    
    return null;
  }
}

/// 预设成就数据
class PresetAchievements {
  static List<Achievement> get all => [
    const Achievement(
      id: 'first_workout',
      name: '初次打卡',
      icon: '🎯',
      description: '完成第一次运动!',
      type: AchievementType.first,
      order: 1,
    ),
    const Achievement(
      id: 'first_week',
      name: '坚持一周',
      icon: '🔥',
      description: '连续运动7天!',
      type: AchievementType.streak,
      targetValue: 7,
      currentValue: 0,
      order: 2,
    ),
    const Achievement(
      id: 'steps_10k',
      name: '万步达人',
      icon: '👟',
      description: '单日步数突破10000!',
      type: AchievementType.milestone,
      targetValue: 10000,
      currentValue: 0,
      order: 3,
    ),
    const Achievement(
      id: 'streak_30',
      name: '一个月坚持',
      icon: '🏆',
      description: '连续运动30天!',
      type: AchievementType.streak,
      targetValue: 30,
      currentValue: 0,
      order: 4,
    ),
    const Achievement(
      id: 'early_bird',
      name: '早起达人',
      icon: '🌅',
      description: '连续5天7点前打卡!',
      type: AchievementType.special,
      targetValue: 5,
      currentValue: 0,
      order: 5,
    ),
  ];
}

😤 三、开发踩坑与挫折

3.1 坑一:彩纸动画位置不对!

问题描述

彩纸从屏幕中间爆发,而不是从顶部落下!

解决方案

dart 复制代码
// ❌ 错误:使用 explosive 会从中心爆发
ConfettiWidget(
  blastDirectionality: BlastDirectionality.explosive,
  blastDirection: 3.14 / 2,  // 向下
)

// ✅ 正确:使用 directional 从顶部落下
ConfettiWidget(
  blastDirectionality: BlastDirectionality.directional,
  blastDirection: 3.14 / 2,  // 向下(弧度)
  minSpeed: 100,
  maxSpeed: 200,
)

3.2 坑二:动画结束后内存泄漏!

问题描述

多次解锁成就后,应用越来越卡!

解决方案

dart 复制代码
// ✅ 一定要在dispose中释放
@override
void dispose() {
  _confettiController.dispose();
  _scaleController.dispose();
  _glowController.dispose();
  super.dispose();
}

📱 四、最终实现效果

成就类型 动画效果 状态
首次类 金色彩纸
连续类 火焰色调
里程碑 青蓝色调
特殊类 彩虹色

好了!成就解锁彩纸动画就讲到这里!

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

📅 发布日期:2026-04-29

✍️ 作者:上海某本科大学大一学生

🏷️ 标签:Flutter / OpenHarmony / confetti / 成就系统


往期推荐

  • 「Flutter confetti彩纸动画的鸿蒙化适配与实战指南」
  • 「Flutter三方库flutter_bloc的鸿蒙化适配与实战指南」
相关推荐
Lanren的编程日记2 小时前
任务77:Flutter 鸿蒙应用视频录制功能实战:视频录制+录制控制+视频编辑,打造完整视频处理能力
flutter·音视频·harmonyos
Hello__77772 小时前
开源鸿蒙 Flutter 实战|进度条组件全流程实现
flutter·开源·harmonyos
IntMainJhy2 小时前
【flutter for open harmony】第三方库 Flutter分享卡片的鸿蒙化适配与实战指南
flutter·华为·harmonyos
Lanren的编程日记2 小时前
任务76:Flutter 鸿蒙应用音频录制功能实战:音频录制+录音管理+录音编辑,打造完整音频处理能力
flutter·华为·音视频·harmonyos
前端不太难2 小时前
鸿蒙游戏的“帧”到底是什么?
游戏·状态模式·harmonyos
IntMainJhy2 小时前
【flutter for open harmony】第三方库 Flutter运动计时器的鸿蒙化适配与实战指南
flutter·华为·信息可视化·数据库开发·harmonyos
Hello__77772 小时前
开源鸿蒙 Flutter 实战|徽章组件全流程实现
flutter·开源·harmonyos
IntMainJhy3 小时前
【flutter for open harmony】 第三方库 Flutter饮食记录的鸿蒙化适配与实战指南
flutter·华为·信息可视化·数据库开发·harmonyos
Lanren的编程日记3 小时前
Flutter 鸿蒙应用数据统计分析功能实战:数据统计+数据可视化+报表生成,打造全链路数据分析能力
flutter·华为·信息可视化·harmonyos