Flutter for OpenHarmony数独游戏App实战:胜利弹窗
胜利弹窗的关键设计要点:准确检测游戏完成、丰富的成绩展示(用时、提示次数、星级评价)、动画效果增强庆祝感、详细统计帮助玩家分析、排行榜激励追求更好成绩、多种后续操作选项。胜利弹窗是对玩家努力的肯定,一个好的胜利弹窗可以给玩家带来成就感,激励他们继续挑战。通过精心设计的视觉效果和丰富的信息展示,我们可以让这个时刻变得更加难忘。欢迎加入开源鸿蒙跨平台社区:https://openharmonycros
胜利弹窗是数独游戏完成时的重要反馈。当玩家成功解决谜题时,应该给予充分的肯定和庆祝。一个好的胜利弹窗不仅要展示成绩,还要激励玩家继续挑战。今天我们来详细实现数独游戏的胜利弹窗。
游戏完成检测
在显示胜利弹窗之前,我们需要准确检测游戏是否完成。
void _checkCompletion() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == 0 || board[i][j] != solution[i][j]) {
return;
}
}
}
isComplete = true;
update();
}
_checkCompletion遍历整个棋盘,检查每个单元格是否都填入了正确的数字。如果有空格或错误,直接返回。只有当所有单元格都正确时,才将isComplete设为true并触发UI更新。
监听完成状态
在GamePage中监听完成状态,在适当时机显示胜利弹窗。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('数独')),
body: GetBuilder<GameController>(
builder: (ctrl) {
if (ctrl.isComplete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showVictoryDialog();
});
}
当isComplete变为true时,使用addPostFrameCallback在当前帧渲染完成后显示胜利弹窗。这避免了在build过程中调用showDialog导致的错误。
继续构建游戏界面
return ctrl.isPaused
? _buildPausedScreen()
: _buildGameScreen();
},
),
);
}
根据isPaused状态决定显示暂停界面还是游戏界面。GetBuilder会在controller调用update()时自动重建。
基本胜利弹窗
void _showVictoryDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('🎉 恭喜!'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('你完成了数独!'),
SizedBox(height: 10.h),
Text('用时: ${controller.formattedTime}'),
Text('提示次数: ${controller.hintsUsed}'),
],
),
barrierDismissible设为false防止玩家点击对话框外部关闭它,确保玩家看到自己的成绩。对话框内容包括祝贺文字、用时和提示次数。
弹窗操作按钮
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
_showNewGameDialog();
},
child: const Text('新游戏'),
),
],
),
);
}
点击新游戏按钮后关闭弹窗并显示难度选择对话框。Navigator.pop关闭当前对话框,然后调用_showNewGameDialog让玩家选择新游戏的难度。
增强版胜利弹窗
使用自定义Dialog替代AlertDialog,可以更灵活地控制样式。
void _showVictoryDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Dialog组件比AlertDialog更灵活,可以完全自定义内容布局。shape设置圆角,padding设置内边距。mainAxisSize.min让Column只占用必要的高度。
奖杯图标和标题
Icon(
Icons.emoji_events,
size: 60.sp,
color: Colors.amber,
),
SizedBox(height: 16.h),
Text(
'恭喜完成!',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
金色奖杯图标增强庆祝感。使用.sp单位确保字体大小适配不同屏幕。粗体标题让祝贺语更加醒目。
成绩信息展示
SizedBox(height: 24.h),
_buildStatRow('难度', controller.difficulty),
_buildStatRow('用时', controller.formattedTime),
_buildStatRow('提示', '${controller.hintsUsed}次'),
成绩信息使用整齐的行布局展示难度、用时和提示次数。_buildStatRow是一个辅助方法,用于构建统一样式的信息行。
操作按钮区域
SizedBox(height: 24.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('关闭'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_showNewGameDialog();
},
child: const Text('新游戏'),
),
],
),
],
),
),
),
);
}
提供关闭和新游戏两个选项。ElevatedButton比TextButton更突出,引导玩家开始新游戏。spaceEvenly让两个按钮均匀分布。
成绩行构建方法
Widget _buildStatRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
Text(value, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
],
),
);
}
_buildStatRow构建标签-值对的行布局。标签使用灰色,值使用粗体黑色,形成视觉对比。spaceBetween让标签和值分别靠左右对齐。
弹窗动画效果
使用showGeneralDialog可以自定义弹窗的过渡动画。
void _showVictoryDialog() {
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 300),
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Curves.elasticOut,
),
child: child,
);
},
showGeneralDialog允许自定义过渡动画。使用elasticOut曲线产生弹跳效果,让弹窗出现更有活力。transitionDuration设置动画时长为300毫秒。
弹窗内容构建
pageBuilder: (context, animation, secondaryAnimation) {
return Center(
child: Material(
color: Colors.transparent,
child: _buildVictoryContent(),
),
);
},
);
}
pageBuilder构建弹窗的实际内容。Material组件确保子组件可以使用Material Design的效果。Center让弹窗居中显示。
胜利弹窗内容容器
Widget _buildVictoryContent() {
return Container(
width: 300.w,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
Container定义弹窗的外观。固定宽度300确保在不同设备上大小一致。boxShadow添加阴影效果,让弹窗有悬浮感。
内容主体
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildConfetti(),
Icon(Icons.emoji_events, size: 60.sp, color: Colors.amber),
SizedBox(height: 16.h),
Text(
'恭喜完成!',
style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold),
),
Column垂直排列所有内容。_buildConfetti添加彩带动画效果。奖杯图标和标题是弹窗的视觉焦点。
祝贺语和成绩
SizedBox(height: 8.h),
Text(
_getCompletionMessage(),
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
_buildStatRow('难度', controller.difficulty),
_buildStatRow('用时', controller.formattedTime),
_buildStatRow('提示', '${controller.hintsUsed}次'),
_getCompletionMessage根据成绩生成个性化的祝贺语。成绩信息紧随其后,让玩家一目了然地看到自己的表现。
新纪录标识
if (_isNewRecord()) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
'🏆 新纪录!',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Colors.amber.shade800,
),
),
),
],
如果创造了新纪录,显示特殊的金色标识。使用if配合展开运算符…[]条件性地添加组件。这可以激励玩家追求更好的成绩。
操作按钮
SizedBox(height: 24.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_showNewGameDialog();
},
child: const Text('新游戏'),
),
],
),
],
),
);
}
底部的操作按钮让玩家可以关闭弹窗或开始新游戏。ElevatedButton更突出,引导玩家继续游戏。
生成祝贺语
根据成绩生成不同的祝贺语,让反馈更加个性化。
String _getCompletionMessage() {
int seconds = controller.elapsedSeconds;
int hints = controller.hintsUsed;
if (hints == 0 && seconds < 300) {
return '太厉害了!不用提示,5分钟内完成!';
} else if (hints == 0) {
return '完美!没有使用任何提示!';
} else if (seconds < 300) {
return '速度很快!继续保持!';
} else {
return '做得好!继续挑战更高难度吧!';
}
}
根据用时和提示次数生成不同的祝贺语。不用提示且用时短的玩家获得最高评价。这种个性化反馈让玩家感到被认可。
检查新纪录
bool _isNewRecord() {
String difficulty = controller.difficulty;
int currentTime = controller.elapsedSeconds;
int? bestTime = statsController.bestTimeByDifficulty[difficulty];
return bestTime == null || currentTime < bestTime;
}
比较当前用时与该难度的最佳记录。如果是首次完成该难度或打破了之前的记录,返回true。这个检查用于决定是否显示新纪录标识。
分享功能
让玩家可以将成绩分享到社交媒体。
ElevatedButton.icon(
onPressed: () {
_shareResult();
},
icon: const Icon(Icons.share),
label: const Text('分享'),
),
void _shareResult() {
String text = '我在数独游戏中完成了${controller.difficulty}难度的谜题!\n'
'用时: ${controller.formattedTime}\n'
'提示: ${controller.hintsUsed}次';
Share.share(text);
}
Share.share是share_plus包提供的方法,可以调用系统分享功能。分享文本包含难度、用时和提示次数,让朋友了解玩家的成绩。
彩带动画组件
class ConfettiWidget extends StatefulWidget {
const ConfettiWidget({super.key});
State<ConfettiWidget> createState() => _ConfettiWidgetState();
}
class _ConfettiWidgetState extends State<ConfettiWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
List<Confetti> confettiList = [];
ConfettiWidget创建彩带下落的动画效果。使用SingleTickerProviderStateMixin提供动画的vsync。confettiList存储所有彩带的数据。
初始化彩带动画
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
);
for (int i = 0; i < 50; i++) {
confettiList.add(Confetti(
x: Random().nextDouble() * 300,
y: -Random().nextDouble() * 100,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
size: Random().nextDouble() * 8 + 4,
speed: Random().nextDouble() * 2 + 1,
));
}
_controller.forward();
}
创建50个彩带,每个有随机的位置、颜色、大小和速度。动画时长3秒,调用forward()开始播放。彩带从屏幕上方随机位置开始下落。
彩带数据模型
class Confetti {
double x;
double y;
Color color;
double size;
double speed;
Confetti({
required this.x,
required this.y,
required this.color,
required this.size,
required this.speed,
});
}
Confetti类存储单个彩带的属性。x和y是位置,color是颜色,size是大小,speed是下落速度。这些属性在初始化时随机生成。
彩带绘制器
class ConfettiPainter extends CustomPainter {
final List<Confetti> confettiList;
final double progress;
ConfettiPainter({required this.confettiList, required this.progress});
void paint(Canvas canvas, Size size) {
for (var confetti in confettiList) {
double currentY = confetti.y + progress * size.height * confetti.speed;
if (currentY > size.height) continue;
ConfettiPainter使用CustomPainter绘制彩带。progress是动画进度(0到1),用于计算彩带的当前位置。超出画布范围的彩带跳过不绘制。
绘制彩带
Paint paint = Paint()
..color = confetti.color.withOpacity(1 - progress * 0.5)
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(confetti.x, currentY),
confetti.size,
paint,
);
}
}
bool shouldRepaint(ConfettiPainter oldDelegate) => true;
}
每个彩带绘制为圆形。透明度随动画进度降低,产生渐隐效果。shouldRepaint返回true确保每帧都重绘。这种庆祝效果让胜利时刻更加欢乐。
星星评级
根据成绩显示1-3颗星的评级。
Widget _buildStarRating() {
int stars = _calculateStars();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
bool isFilled = index < stars;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Icon(
isFilled ? Icons.star : Icons.star_border,
size: 32.sp,
color: Colors.amber,
),
);
}),
);
}
List.generate创建3个星星图标。根据计算出的星数决定显示实心还是空心星星。金色星星是游戏中常见的评级方式。
计算星级
int _calculateStars() {
int seconds = controller.elapsedSeconds;
int hints = controller.hintsUsed;
String difficulty = controller.difficulty;
int threeStarTime = _getThreeStarTime(difficulty);
int twoStarTime = threeStarTime * 2;
if (hints == 0 && seconds <= threeStarTime) {
return 3;
} else if (hints <= 2 && seconds <= twoStarTime) {
return 2;
} else {
return 1;
}
}
三星需要不用提示且在目标时间内完成,两星允许少量提示和较长时间,一星是基本完成。这种评级系统给玩家明确的目标。
获取三星时间标准
int _getThreeStarTime(String difficulty) {
switch (difficulty) {
case 'Easy': return 180;
case 'Medium': return 300;
case 'Hard': return 600;
case 'Expert': return 900;
default: return 180;
}
}
不同难度有不同的三星时间标准。简单难度3分钟,中等5分钟,困难10分钟,专家15分钟。这些标准可以根据实际测试调整。
可展开的详细统计
class ExpandableStats extends StatefulWidget {
final GameController controller;
const ExpandableStats({super.key, required this.controller});
State<ExpandableStats> createState() => _ExpandableStatsState();
}
class _ExpandableStatsState extends State<ExpandableStats> {
bool _isExpanded = false;
ExpandableStats提供可展开的详细统计信息。默认只显示基本信息,点击可以展开查看更多细节。_isExpanded控制展开状态。
展开控制
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'详细统计',
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
color: Colors.grey,
),
],
),
),
点击"详细统计"切换展开状态。图标根据状态显示向上或向下的箭头,提示用户可以展开或收起。
展开动画
AnimatedCrossFade(
firstChild: const SizedBox(),
secondChild: _buildDetailedStats(),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
);
}
AnimatedCrossFade提供平滑的展开动画。收起时显示空的SizedBox,展开时显示详细统计。200毫秒的动画时长让过渡自然流畅。
详细统计内容
Widget _buildDetailedStats() {
return Padding(
padding: EdgeInsets.only(top: 12.h),
child: Column(
children: [
_buildStatRow('填入数字', '${81 - _countInitialClues()}个'),
_buildStatRow('平均每格', '${_calculateAverageTimePerCell()}秒'),
_buildStatRow('错误次数', '${widget.controller.errorCount}次'),
_buildStatRow('撤销次数', '${widget.controller.undoCount}次'),
],
),
);
}
详细统计包括填入的数字数量、平均每格用时、错误次数和撤销次数。这些数据帮助玩家分析自己的解题过程。
计算初始线索数
int _countInitialClues() {
int count = 0;
for (var row in widget.controller.isFixed) {
for (var cell in row) {
if (cell) count++;
}
}
return count;
}
遍历isFixed数组统计初始给出的数字数量。isFixed为true的单元格是谜题初始就有的数字,玩家不能修改。
计算平均用时
String _calculateAverageTimePerCell() {
int filledCells = 81 - _countInitialClues();
if (filledCells == 0) return '0';
double average = widget.controller.elapsedSeconds / filledCells;
return average.toStringAsFixed(1);
}
}
用总时间除以玩家填入的数字数量得到平均每格用时。toStringAsFixed(1)保留一位小数。这个指标反映玩家的解题速度。
排行榜入口
Widget _buildLeaderboardEntry() {
int rank = _calculateRank();
return Container(
margin: EdgeInsets.only(top: 16.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.leaderboard, color: Colors.grey, size: 20.sp),
SizedBox(width: 8.w),
Text('排名', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
排行榜入口显示当前成绩在历史记录中的排名。使用灰色背景和圆角让它与其他内容区分开。
排名显示
Text(
'#$rank',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: rank <= 3 ? Colors.amber : Colors.black,
),
),
],
),
);
}
前三名使用金色显示,其他名次使用黑色。#符号是排名的常见表示方式。这可以激励玩家追求更好的成绩。
计算排名
int _calculateRank() {
String difficulty = controller.difficulty;
int time = controller.elapsedSeconds;
List<int> allTimes = statsController.getAllTimes(difficulty);
allTimes.add(time);
allTimes.sort();
return allTimes.indexOf(time) + 1;
}
获取该难度的所有历史用时,加入当前用时后排序,找到当前用时的位置就是排名。indexOf返回的是从0开始的索引,所以加1得到实际排名。
再来一局按钮
Widget _buildPlayAgainButton() {
return ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
controller.generateNewGame(controller.difficulty);
},
icon: const Icon(Icons.replay),
label: const Text('再来一局'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
),
);
}
再来一局按钮让玩家可以快速开始同难度的新游戏。绿色按钮表示积极的行动。ElevatedButton.icon同时显示图标和文字。
挑战更高难度
Widget _buildChallengeButton() {
String? nextDifficulty = _getNextDifficulty();
if (nextDifficulty == null) return const SizedBox();
return TextButton.icon(
onPressed: () {
Navigator.pop(context);
controller.generateNewGame(nextDifficulty);
},
icon: const Icon(Icons.trending_up),
label: Text('挑战$nextDifficulty'),
);
}
如果当前不是最高难度,显示挑战更高难度的按钮。这鼓励玩家不断进步,尝试更具挑战性的谜题。
获取下一难度
String? _getNextDifficulty() {
switch (controller.difficulty) {
case 'Easy': return '中等';
case 'Medium': return '困难';
case 'Hard': return '专家';
default: return null;
}
}
根据当前难度返回下一个难度级别。专家难度是最高级别,返回null表示没有更高难度。
总结
胜利弹窗的关键设计要点:准确检测游戏完成、丰富的成绩展示(用时、提示次数、星级评价)、动画效果增强庆祝感、详细统计帮助玩家分析、排行榜激励追求更好成绩、多种后续操作选项。
胜利弹窗是对玩家努力的肯定,一个好的胜利弹窗可以给玩家带来成就感,激励他们继续挑战。通过精心设计的视觉效果和丰富的信息展示,我们可以让这个时刻变得更加难忘。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)