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


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的抽奖结果庆祝页面。项目中使用 Flutter 第三方库 confetti 实现彩纸庆祝动画,当用户点击“开始抽奖”按钮后,页面会随机生成奖品,并播放庆祝动画。

最终运行效果如下:

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

页面主要包含以下内容:

  • 顶部标题栏;
  • 抽奖活动说明卡片;
  • 当前抽中奖品展示;
  • 彩纸庆祝动画;
  • 奖品列表;
  • 开始抽奖按钮;
  • 重置结果按钮;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

本文重点是演示如何在 Flutter for OpenHarmony 项目中使用 Flutter 第三方库 confetti。项目代码写在 lib/main.dart 中,依赖配置写在 pubspec.yaml 中,符合 Flutter for OpenHarmony 第三方库实践方向。


前言

在移动应用开发中,动画反馈可以提升用户操作后的体验。例如抽奖成功、任务完成、签到成功、成就解锁、订单支付成功等场景,都可以使用庆祝动画增强页面表现力。

如果只是显示一行文字:

恭喜中奖

功能上当然没错,但视觉反馈比较弱。用户点完按钮后看到一行干巴巴的文字,就像生日蛋糕上没有蜡烛,能吃,但少了点仪式感。

本文选择使用 Flutter 第三方库 confetti 实现彩纸庆祝动画。它可以在页面中喷射彩纸粒子,并支持控制粒子数量、方向、速度、重力和颜色。

本项目以“抽奖结果庆祝页面”为例,使用 confetti 实现中奖后的庆祝效果,并结合 Flutter 页面状态更新实现随机抽奖功能。


一、项目目标

本次实践主要实现以下目标:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 confetti
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 confetti
  • 使用 ConfettiController 控制动画播放;
  • 使用 ConfettiWidget 展示彩纸动画;
  • 使用 Flutter Material 组件构建抽奖页面;
  • 实现随机抽奖功能;
  • 实现抽奖结果展示;
  • 实现重置抽奖结果;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 confetti
功能场景 彩纸动画 / 抽奖结果 / 成功反馈
核心组件 ConfettiController / ConfettiWidget
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 confetti

在实际应用中,庆祝动画可以用于很多场景,例如:

  • 抽奖中奖;
  • 签到成功;
  • 任务完成;
  • 学习目标达成;
  • 支付成功;
  • 订单提交成功;
  • 游戏通关;
  • 成就解锁;
  • 活动页奖励发放。

如果完全自己使用 Flutter 原生动画绘制彩纸,需要处理粒子位置、运动方向、重力、随机颜色、生命周期和刷新逻辑。能写,但为了几片彩纸写一套粒子系统,多少有点像为了吃泡面去修炼农业文明。

confetti 已经封装好了彩纸动画组件,可以让开发者快速实现庆祝效果。

在本项目中,confetti 主要完成以下工作:

  • 播放彩纸庆祝动画;
  • 控制动画开始和停止;
  • 设置彩纸喷射方向;
  • 设置彩纸数量;
  • 设置彩纸颜色;
  • 增强抽奖结果的视觉反馈。

四、创建 Flutter for OpenHarmony 项目

在已经配置好 Flutter for OpenHarmony 开发环境的前提下,可以创建一个 Flutter 项目。

示例项目名称:

flutter create lottery_confetti_demo

进入项目目录:

cd lottery_confetti_demo

项目创建完成后,主要关注两个文件:

lottery_confetti_demo
 ├── pubspec.yaml
 └── lib
     └── main.dart

其中:

文件 作用
pubspec.yaml 配置 Flutter 项目依赖
lib/main.dart 编写 Flutter 页面和业务逻辑

五、添加 confetti 第三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中添加 confetti

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  confetti: ^0.8.0

完整结构大致如下:

name: lottery_confetti_demo
description: A Flutter for OpenHarmony confetti demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  confetti: ^0.8.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

添加完成后,在终端执行:

flutter pub get

执行成功后,就可以在 Dart 代码中使用 confetti 了。


六、项目结构

本项目主要修改 lib/main.dart 文件:

lib
 └── main.dart

本项目不需要编写 OpenHarmony 原生 ArkTS 页面,也不需要修改 Index.ets

因为这是 Flutter for OpenHarmony 项目,页面主体应该是 Flutter 代码。审核重点会看:

  • 是否使用 pubspec.yaml 添加 Flutter 第三方库;
  • 是否在 Dart 文件中 import package
  • 是否在 lib/main.dart 中实际调用第三方库;
  • 是否属于 Flutter for OpenHarmony 项目。

看到 pubspec.yamllib/main.dartimport 'package:confetti/confetti.dart';,这才是正确方向。看到 Index.ets,审核大概率又会把文章打回,世界就是这么直接。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 confetti
  2. main.dart 中引入第三方库;
  3. 创建 ConfettiController 控制彩纸动画;
  4. 在页面中使用 ConfettiWidget 显示彩纸;
  5. 定义奖品数据模型;
  6. 点击按钮后随机选择奖品;
  7. 更新当前中奖结果;
  8. 调用 ConfettiController.play() 播放动画;
  9. 点击重置按钮后清空抽奖结果。

第三方库引入代码如下:

import 'package:confetti/confetti.dart';

创建控制器代码如下:

late final ConfettiController _confettiController;

播放动画核心代码如下:

_confettiController.play();

彩纸组件核心代码如下:

ConfettiWidget(
  confettiController: _confettiController,
  blastDirectionality: BlastDirectionality.explosive,
  shouldLoop: false,
)

八、main.dart 完整代码

打开文件:

lib/main.dart

将其中内容替换为下面代码:

import 'dart:math';

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

void main() {
  runApp(const LotteryConfettiApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Lottery Confetti Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.pink,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const LotteryHomePage(),
    );
  }
}

class PrizeItem {
  const PrizeItem({
    required this.name,
    required this.level,
    required this.description,
    required this.icon,
    required this.color,
  });

  final String name;
  final String level;
  final String description;
  final IconData icon;
  final Color color;
}

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

  
  State<LotteryHomePage> createState() => _LotteryHomePageState();
}

class _LotteryHomePageState extends State<LotteryHomePage> {
  late final ConfettiController _confettiController;
  final Random _random = Random();

  final List<PrizeItem> _prizes = const [
    PrizeItem(
      name: '蓝牙耳机',
      level: '一等奖',
      description: '适合通勤、学习和运动使用的实用奖品。',
      icon: Icons.headphones,
      color: Colors.deepPurple,
    ),
    PrizeItem(
      name: '键盘鼠标套装',
      level: '二等奖',
      description: '适合学习和办公场景,码字效率直接拉满。',
      icon: Icons.keyboard,
      color: Colors.blue,
    ),
    PrizeItem(
      name: '咖啡兑换券',
      level: '三等奖',
      description: '适合熬夜学习和赶作业时维持基本人类形态。',
      icon: Icons.coffee,
      color: Colors.brown,
    ),
    PrizeItem(
      name: '学习笔记本',
      level: '幸运奖',
      description: '记录计划、灵感和那些迟早会被拖延的任务。',
      icon: Icons.menu_book,
      color: Colors.teal,
    ),
    PrizeItem(
      name: '随机小礼品',
      level: '参与奖',
      description: '虽然不是大奖,但至少不是空手而归。',
      icon: Icons.card_giftcard,
      color: Colors.orange,
    ),
  ];

  PrizeItem? _currentPrize;
  int _drawCount = 0;

  
  void initState() {
    super.initState();
    _confettiController = ConfettiController(
      duration: const Duration(seconds: 2),
    );
  }

  
  void dispose() {
    _confettiController.dispose();
    super.dispose();
  }

  String get _resultText {
    if (_currentPrize == null) {
      return '点击按钮开始抽奖';
    }

    return '恭喜抽中:${_currentPrize!.name}';
  }

  void _startLottery() {
    final int index = _random.nextInt(_prizes.length);

    setState(() {
      _currentPrize = _prizes[index];
      _drawCount++;
    });

    _confettiController.stop();
    _confettiController.play();
  }

  void _resetLottery() {
    setState(() {
      _currentPrize = null;
      _drawCount = 0;
    });

    _confettiController.stop();
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('抽奖庆祝页面'),
        centerTitle: true,
      ),
      body: Stack(
        alignment: Alignment.topCenter,
        children: [
          SafeArea(
            child: ListView(
              padding: const EdgeInsets.all(16),
              children: [
                _buildIntroCard(theme),
                const SizedBox(height: 16),
                _buildPrizeResultCard(theme),
                const SizedBox(height: 16),
                _buildActionCard(theme),
                const SizedBox(height: 16),
                _buildPrizeListCard(theme),
                const SizedBox(height: 16),
                _buildLibraryCard(theme),
              ],
            ),
          ),
          ConfettiWidget(
            confettiController: _confettiController,
            blastDirectionality: BlastDirectionality.explosive,
            shouldLoop: false,
            emissionFrequency: 0.08,
            numberOfParticles: 28,
            maxBlastForce: 26,
            minBlastForce: 8,
            gravity: 0.18,
            colors: const [
              Colors.pink,
              Colors.orange,
              Colors.blue,
              Colors.green,
              Colors.purple,
              Colors.amber,
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildIntroCard(ThemeData theme) {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Container(
              width: 76,
              height: 76,
              decoration: BoxDecoration(
                color: theme.colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                Icons.celebration,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            Text(
              'Flutter for OpenHarmony',
              style: theme.textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              '使用 confetti 构建抽奖后的彩纸庆祝动画效果',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPrizeResultCard(ThemeData theme) {
    final PrizeItem? prize = _currentPrize;

    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(22),
        child: Column(
          children: [
            Text(
              '抽奖结果',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            AnimatedContainer(
              duration: const Duration(milliseconds: 300),
              width: 112,
              height: 112,
              decoration: BoxDecoration(
                color: prize == null
                    ? theme.colorScheme.surfaceContainerHighest
                    : prize.color.withOpacity(0.16),
                borderRadius: BorderRadius.circular(32),
              ),
              child: Icon(
                prize == null ? Icons.help_outline : prize.icon,
                size: 58,
                color: prize == null
                    ? theme.colorScheme.onSurfaceVariant
                    : prize.color,
              ),
            ),
            const SizedBox(height: 18),
            Text(
              _resultText,
              style: theme.textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
                color: prize == null
                    ? theme.colorScheme.onSurface
                    : prize.color,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              prize == null ? '当前还没有抽奖结果' : prize.level,
              style: theme.textTheme.titleMedium?.copyWith(
                color: theme.colorScheme.primary,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 12),
            Text(
              prize == null
                  ? '点击下方按钮后,系统会随机抽取一个奖品,并播放彩纸动画。'
                  : prize.description,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 14),
            Text(
              '已抽奖 $_drawCount 次',
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildActionCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Row(
          children: [
            Expanded(
              child: ElevatedButton.icon(
                onPressed: _startLottery,
                icon: const Icon(Icons.casino),
                label: const Text('开始抽奖'),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: OutlinedButton.icon(
                onPressed: _resetLottery,
                icon: const Icon(Icons.refresh),
                label: const Text('重置结果'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPrizeListCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '奖品列表',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            ..._prizes.map((prize) {
              final bool selected = _currentPrize?.name == prize.name;

              return Container(
                margin: const EdgeInsets.only(bottom: 10),
                padding: const EdgeInsets.all(14),
                decoration: BoxDecoration(
                  color: selected
                      ? prize.color.withOpacity(0.12)
                      : theme.colorScheme.surfaceContainerHighest,
                  borderRadius: BorderRadius.circular(16),
                  border: Border.all(
                    color: selected ? prize.color : Colors.transparent,
                    width: 1.2,
                  ),
                ),
                child: Row(
                  children: [
                    Icon(
                      prize.icon,
                      color: prize.color,
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            prize.name,
                            style: theme.textTheme.titleMedium?.copyWith(
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(height: 4),
                          Text(
                            prize.level,
                            style: theme.textTheme.bodySmall?.copyWith(
                              color: theme.colorScheme.onSurfaceVariant,
                            ),
                          ),
                        ],
                      ),
                    ),
                    if (selected)
                      Icon(
                        Icons.check_circle,
                        color: prize.color,
                      ),
                  ],
                ),
              );
            }),
          ],
        ),
      ),
    );
  }

  Widget _buildLibraryCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '第三方库说明',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            _buildInfoRow(
              theme,
              title: '库名称',
              value: 'confetti',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value: "import 'package:confetti/confetti.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value: 'ConfettiController / ConfettiWidget',
            ),
            _buildInfoRow(
              theme,
              title: '应用场景',
              value: '中奖结果、任务完成、成就解锁、签到成功、支付成功',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(
    ThemeData theme, {
    required String title,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 82,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 confetti 第三方库

代码开头引入第三方库:

import 'package:confetti/confetti.dart';

这说明项目确实使用了 Flutter 第三方库,而不是 OpenHarmony 原生库。

本项目中主要使用两个对象:

ConfettiController
ConfettiWidget

其中:

对象 作用
ConfettiController 控制彩纸动画播放、停止和释放
ConfettiWidget 在页面中显示彩纸动画

2. 创建 ConfettiController

页面中定义控制器:

late final ConfettiController _confettiController;

initState() 中初始化:

_confettiController = ConfettiController(
  duration: const Duration(seconds: 2),
);

其中 duration 表示彩纸动画的播放时间。


3. 释放 ConfettiController

因为 ConfettiController 是动画控制器,所以页面销毁时需要释放:


void dispose() {
  _confettiController.dispose();
  super.dispose();
}

如果不释放控制器,可能造成资源没有正确回收。程序不是垃圾场,控制器用完就该处理掉,别什么都往内存里扔。


4. 使用 ConfettiWidget 显示彩纸动画

页面中使用 ConfettiWidget 展示彩纸:

ConfettiWidget(
  confettiController: _confettiController,
  blastDirectionality: BlastDirectionality.explosive,
  shouldLoop: false,
)

其中:

参数 作用
confettiController 绑定动画控制器
blastDirectionality 设置彩纸喷射方向
shouldLoop 是否循环播放

本项目使用:

BlastDirectionality.explosive

表示彩纸会向多个方向爆发式喷射。


5. 设置彩纸动画参数

本项目中还设置了多个动画参数:

emissionFrequency: 0.08,
numberOfParticles: 28,
maxBlastForce: 26,
minBlastForce: 8,
gravity: 0.18,

参数说明如下:

参数 作用
emissionFrequency 粒子发射频率
numberOfParticles 每次发射的彩纸数量
maxBlastForce 最大喷射力度
minBlastForce 最小喷射力度
gravity 彩纸下落速度

这些参数可以调整彩纸动画的强弱和速度。


6. 设置彩纸颜色

彩纸颜色通过 colors 参数设置:

colors: const [
  Colors.pink,
  Colors.orange,
  Colors.blue,
  Colors.green,
  Colors.purple,
  Colors.amber,
],

如果不设置颜色,组件也可以使用默认随机颜色。

这里手动设置颜色,是为了让页面风格更统一。


7. 实现随机抽奖

抽奖时使用 Random 生成随机下标:

final int index = _random.nextInt(_prizes.length);

然后根据随机下标获取奖品:

_currentPrize = _prizes[index];

这样每次点击“开始抽奖”按钮,都会随机得到一个奖品。


8. 点击按钮播放彩纸动画

点击抽奖按钮后执行:

void _startLottery() {
  final int index = _random.nextInt(_prizes.length);

  setState(() {
    _currentPrize = _prizes[index];
    _drawCount++;
  });

  _confettiController.stop();
  _confettiController.play();
}

这里先更新抽奖结果,再播放彩纸动画。

先调用:

_confettiController.stop();

再调用:

_confettiController.play();

这样可以保证连续点击按钮时,彩纸动画能重新开始播放。


9. 使用 Stack 叠放页面和动画

页面主体使用 Stack

Stack(
  alignment: Alignment.topCenter,
  children: [
    SafeArea(...),
    ConfettiWidget(...),
  ],
)

这样可以让彩纸动画显示在页面上方,而不会影响下方卡片布局。

如果不用 Stack,彩纸组件可能会占用普通布局空间,页面结构会比较奇怪。彩纸应该飘在页面上,不应该像普通卡片一样排队站好,连彩纸都这么规矩就太悲惨了。


十、运行项目

完成代码后,在终端执行:

flutter pub get

然后连接 OpenHarmony 设备或启动 OpenHarmony 模拟器。

查看设备:

flutter devices

运行项目:

flutter run

如果环境配置正确,应用会运行到 OpenHarmony 设备或模拟器中。

运行成功后,页面会显示“抽奖庆祝页面”。点击“开始抽奖”按钮后,页面会随机显示中奖结果,并播放彩纸庆祝动画。


十一、开发中遇到的问题

1. confetti 依赖没有生效

如果代码中出现找不到 confetti 的问题,可以检查 pubspec.yaml 中是否添加了:

confetti: ^0.8.0

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。编辑器偶尔像没睡醒,依赖明明装好了,它还非要装作没看见,经典软件行为。


2. import 导入报错

如果下面代码报错:

import 'package:confetti/confetti.dart';

通常有几种原因:

  • pubspec.yaml 中没有添加依赖;
  • 没有执行 flutter pub get
  • YAML 缩进错误;
  • 包名写错;
  • 编辑器没有刷新依赖。

其中 YAML 缩进最容易出问题。配置文件对空格的敏感程度,简直像在审讯键盘。


3. 点击按钮后没有彩纸动画

如果点击按钮后没有动画,可以检查:

  • 是否创建了 ConfettiController
  • 是否在页面中添加了 ConfettiWidget
  • ConfettiWidget 是否绑定了正确控制器;
  • 是否调用了 _confettiController.play()
  • ConfettiWidget 是否被其他组件遮挡;
  • 动画颜色是否和背景太接近。

本项目中使用 StackConfettiWidget 放在页面上层,可以避免动画被普通组件挤走。


4. 彩纸动画只播放一次

如果彩纸动画只播放一次,连续点击后不重新播放,可以在播放前先调用:

_confettiController.stop();
_confettiController.play();

这样可以让动画重新开始。


5. 页面销毁时报错

如果页面销毁时出现控制器相关问题,可以检查是否在 dispose() 中释放:

_confettiController.dispose();

动画控制器用完必须释放,不然就像把灯一直开着还假装没人付电费。


6. 彩纸数量太多导致卡顿

如果动画播放时页面卡顿,可以减少以下参数:

numberOfParticles: 28,
emissionFrequency: 0.08,

彩纸数量越多,计算量越大。动画不是越多越高级,太多只会让设备开始怀疑人生。


7. 运行不到 OpenHarmony 设备

如果项目无法运行到 OpenHarmony 设备或模拟器,可以检查:

  • Flutter for OpenHarmony 环境是否配置完成;
  • 设备是否连接成功;
  • flutter devices 是否能识别设备;
  • 是否执行了 flutter pub get
  • 是否选择了正确的运行设备;
  • 项目是否为 Flutter 项目,而不是原生鸿蒙项目。

如果 flutter devices 都识别不到设备,那就先处理环境问题,不要盯着彩纸代码看半小时。彩纸很无辜,至少这次大概率是。


十二、本文和原生鸿蒙项目的区别

本文是 Flutter for OpenHarmony 第三方库实践,不是 OpenHarmony 原生 ArkTS 项目。

主要区别如下:

对比项 本文写法 原生鸿蒙写法
UI 技术 Flutter ArkUI
主要语言 Dart ArkTS
页面入口 lib/main.dart Index.ets
依赖配置 pubspec.yaml oh-package.json5
依赖安装 flutter pub get ohpm install
第三方库 confetti OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / ConfettiWidget @Entry / @Component

因此本文符合 Flutter for OpenHarmony 第三方库实践方向。


十三、总结

本篇完成了一个基于 confetti 的 Flutter for OpenHarmony 抽奖结果庆祝页面。项目通过 Flutter 第三方库实现彩纸庆祝动画,并结合随机抽奖逻辑展示中奖结果。

通过本次实践,我主要完成了以下内容:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 confetti 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入 confetti
  • 使用 ConfettiController 控制动画播放;
  • 使用 ConfettiWidget 展示彩纸动画;
  • 使用 Random 实现随机抽奖;
  • 使用 setState() 更新中奖结果;
  • 使用 Stack 叠放页面内容和彩纸动画;
  • 实现开始抽奖和重置结果功能;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础抽奖页面,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

后续可以在这个基础上继续扩展,例如:

  • 添加真实奖品图片;
  • 添加抽奖次数限制;
  • 添加中奖概率配置;
  • 添加抽奖记录;
  • 添加本地数据保存;
  • 添加用户登录信息;
  • 添加活动倒计时;
  • 添加音效反馈;
  • 添加分享中奖结果;
  • 添加更复杂的粒子形状。

整体来看,confetti 可以帮助 Flutter 开发者快速实现庆祝动画。通过这个项目,可以理解 Flutter for OpenHarmony 中第三方库依赖配置、动画控制器使用和页面状态更新之间的基本关系。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐