鸿蒙原生开发——从零构建趣味问答
一、引言
问答(Quiz)是人类最古老的知识测试形式——从科举考试到电视智力竞赛,从驾照科目一到社交网络上的性格测试,问答的核心公式从未改变:给出一个问题,提供若干选项,选出正确答案,最后看分数。
从技术角度看,趣味问答是一个三阶段状态机:开始界面 → 答题循环 → 结果展示。与井字棋(回合制博弈)和记忆翻牌(实时翻牌+延时)不同,问答的交互是线性推进的——每道题的选择→反馈→下一题,10 道题构成一条不可逆的直线。这种线性结构简化了状态管理,但带来了新的挑战:即时反馈的视觉呈现(绿色正确/红色错误)、自动推进的节奏控制(1.5s 等待)、以及最终评级的设计(A-D 四个等级)。
本文用 ArkUI 从零构建一个趣味问答应用,包含 10 道涵盖编程与常识的选择题、即时正误反馈、自动推进下一题、计时计分和 A-D 评级。整个应用是一个三页面的条件渲染——开始页、答题页和结果页共享同一个 build() 方法,通过 quizStarted 和 quizFinished 两个布尔变量切换。
阅读完本文,你将能够:
- 用条件渲染实现三阶段页面切换(开始 / 答题中 / 结果)
- 管理问答状态机(选题 → 反馈 → 自动推进)
- 实现即时正误反馈(绿色/红色高亮,1.5s 后自动下一题)
- 用进度条 + ABCD 评级展示答题成绩
- 用 Fisher-Yates 洗牌随机化题目顺序
二、应用设计
2.1 三阶段状态机
应用有三个互斥的界面阶段:
开始界面 答题界面 结果界面
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 📝 图标 │ 点击 │ 第 3/10 │ 答完 │ 🎉 图标 │
│ 趣味问答 │ ─────→ │ 问题... │ ─────→ │ 答对 8/10 │
│ 开始答题 │ │ A. 选项 │ │ 等级 B │
│ │ │ B. 选项 │ │ 用时 1:23 │
│ │ │ C. 选项 │ │ 再来一次 │
│ │ │ D. 选项 │ │ │
└──────────┘ └──────────┘ └──────────┘
用两个布尔 @State 变量控制:
@State quizStarted: boolean = false; // 是否已开始
@State quizFinished: boolean = false; // 是否已结束
三种界面用 if / else if / else 条件渲染:
if (!this.quizStarted && !this.quizFinished) {
// 开始界面
} else if (this.quizFinished) {
// 结果界面
} else {
// 答题界面
}
两个布尔变量的组合产生三种有效状态。第四种组合(quizStarted=true 且 quizFinished=true)不应该出现,但即使出现也会走到结果界面——这是一种防御性设计。
2.2 交互流程
一局完整的问答包含 4 个交互点:
- 开始答题:点击"开始答题"按钮 → 初始化题库、打乱顺序、启动计时
- 选择答案:点击 A/B/C/D 其中一个选项 → 即时显示正误反馈
- 自动推进:1.5s 后自动跳到下一题(如果是最后一题则进入结果页)
- 再来一次:查看成绩和评级后,点击"再来一次"重新开始
注意"自动推进"不是用户主动触发的——它是 setInterval 驱动的被动行为。这意味着在选择答案后,用户有 1.5s 的时间观察反馈(哪道对了、哪道错了、正确答案是什么),然后无操作地进入下一题。这种设计给了用户"喘一口气"的时间,同时保持了答题的流畅节奏。
三、题库设计
3.1 Question 接口
每道题的数据结构:
interface Question {
question: string; // 题目文字
options: string[]; // 四个选项
answer: number; // 正确答案的索引(0-3)
}
answer 是正确选项在 options 数组中的索引。这个设计比"存储正确选项的文字"更健壮——因为索引不会随着选项顺序的调整而变化,且比较时只需要一次整数相等判断(optIdx === q.answer)。
3.2 题库与答案分布
题库包含 10 道题,涵盖编程、科学、常识三个领域:
const Q_BANK: Question[] = [
{ question: '鸿蒙系统的开发语言是?',
options: ['ArkTS', 'Java', 'Swift', 'Kotlin'], answer: 0 },
{ question: 'typeof null 在JavaScript中的结果是?',
options: ['"null"', '"object"', '"undefined"', '"boolean"'], answer: 1 },
{ question: 'HTTP状态码 404 表示什么?',
options: ['服务器错误', '重定向', '资源未找到', '请求成功'], answer: 2 },
// ... 共 10 题
];
一个细微但重要的设计点:正确答案的位置不是全部放在 A(索引 0),而是分散在 A/B/C/D 四个位置。题库中 answer 的分布为 0、0、1、2、0、1、0、1、0、1——不均匀但分散,确保用户不会形成"总是选 A"的肌肉记忆。
3.3 题目随机化
每次点击"开始答题"时,使用 Fisher-Yates 洗牌算法打乱题目顺序:
startQuiz(): void {
const pool = [...Q_BANK];
this.shuffle(pool);
this.questions = pool;
// ...
}
shuffle(arr: Question[]): void {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
洗牌只打乱题目的出现顺序,不打乱每道题内的选项顺序。这是因为选项顺序在题库设计中已经做好了分散(A/B/C/D 四个位置的答案分布不同),再打乱选项会导致用户困惑——例如看到"A. 太平洋"和"B. 大西洋"时,"太平洋"是正确答案,但换了顺序后"A. 大西洋"和"B. 太平洋"会让熟悉题目顺序的用户选错(尽管答案仍然是"太平洋"对应的新位置)。因此选项顺序保持题库中的定义,题目顺序随机化以增加重复玩耍的趣味性。
四、答题交互
4.1 selectOption 方法
用户点击某个选项后,执行以下逻辑:
selectOption(optIdx: number): void {
if (this.selectedOption !== -1) return; // 已选择,忽略重复点击
const q = this.questions[this.currentIdx];
if (optIdx === q.answer) this.score++; // 正确则加分
this.selectedOption = optIdx; // 记录选择,触发反馈 UI
this.feedbackTimerId = setInterval(() => {
clearInterval(this.feedbackTimerId);
this.feedbackTimerId = -1;
this.selectedOption = -1; // 清除反馈状态
if (this.currentIdx < this.questions.length - 1) {
this.currentIdx++; // 下一题
} else {
clearInterval(this.timerId); // 最后一题 → 结束
this.timerId = -1;
this.quizFinished = true;
}
}, 1500);
}
整个方法的逻辑可以分为四个阶段:
阶段一(守卫):selectedOption !== -1 意味着用户已经做出了选择,正在进行 1.5s 的反馈展示。在此期间点击任何选项都被忽略,防止"改答案"或"快速连点跳过"。
阶段二(计分):比较 optIdx === q.answer,如果选中了正确选项则 score++。这个比较只在用户首次点击时执行一次。
阶段三(反馈):设置 selectedOption = optIdx,触发 UI 的颜色变化。此时所有四个选项变为:正确选项绿底、错误选择红底(如果选错)、其余灰色。
阶段四(推进):setInterval 在 1.5s 后清除反馈状态并推进到下一题。使用 setInterval 而非 setTimeout 是基于记忆翻牌的经验教训——setInterval 在 ArkTS 中经过验证可靠。
4.2 即时反馈
反馈期间的选项颜色由 optionBg() 方法动态计算:
optionBg(optIdx: number): string {
if (this.selectedOption === -1) return '#F5F5FA'; // 未选择:浅灰
const correct = this.questions[this.currentIdx].answer;
if (optIdx === correct) return '#C8E6C9'; // 正确:浅绿
if (optIdx === this.selectedOption && optIdx !== correct)
return '#FFCDD2'; // 选错:浅红
return '#E8E8EE'; // 其余:更浅灰
}
三种反馈颜色传递的信息:
| 选项状态 | 背景色 | 文字色 | 边框 | 含义 |
|---|---|---|---|---|
| 正确答案 | #C8E6C9 浅绿 |
#2E7D32 深绿 |
绿色边框 | “这是对的” |
| 选错答案 | #FFCDD2 浅红 |
#C62828 深红 |
红色边框 | “你选了这个,但它不对” |
| 未选错误 | #E8E8EE 浅灰 |
#BBBBBB |
无 | “无关选项” |
这种三色反馈同时达到了两个目的:(1) 让用户知道自己是否答对,(2) 如果答错,让用户知道正确答案是什么。1.5s 的展示时间足够用户消化这个信息。
4.3 选项标签
每个选项前有一个圆形标签(A/B/C/D),在反馈阶段也跟随变色:
Text(label)
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor(
this.selectedOption === -1 ? '#E8E8EE' : // 未选:灰圆
(oi === correct ? '#C8E6C9' : // 正确:绿圆
(oi === this.selectedOption ? '#FFCDD2' : '#E8E8EE'))) // 选错:红圆
.textAlign(TextAlign.Center)
圆标签的引入不仅仅是为了美观——它将四个选项从"无差别文字块"变成"A/B/C/D 四个可区分的实体",降低了用户的视觉搜索成本。用户可以在扫一眼之后直接记住"答案是 C",而不需要重读选项文字来确认位置。
五、计时与计分
5.1 计时器
计时从"开始答题"按钮被点击时启动,在最后一题的反馈结束后停止:
// startQuiz() 中:
this.timerId = setInterval(() => { this.elapsedSec++; }, 1000);
// selectOption() 中,最后一题反馈后:
clearInterval(this.timerId);
计时器只在答题阶段运行,不在开始界面和结果界面运转。这样用户可以在开始界面从容阅读说明,在结果界面仔细查看成绩,而不必担心计时器空转。
5.2 进度条
页面顶部有一个细长的进度条,实时反映答题进度:
Row() {
Row() {
}
.width(`${(this.currentIdx / this.questions.length) * 100}%`)
.height(4)
.backgroundColor('#667eea')
.borderRadius(2)
}
.width('100%')
.height(4)
.backgroundColor('#E8E8EE')
.borderRadius(2)
外层的灰色 Row 是进度条背景,内层的蓝色 Row 是填充部分。填充宽度使用百分比字符串动态计算——currentIdx / questions.length * 100。例如第 5 题时,进度为 5/10 = 50%。
进度条的上方还有三个信息项:当前题号(第 X/10 题)、用时(MM:SS)和当前得分(✓ X),形成一个紧凑的信息行。
5.3 评级系统
结果页面使用 A-D 四个等级评价成绩:
grade(): string {
const pct = this.score / this.questions.length;
if (pct >= 0.9) return 'A';
if (pct >= 0.7) return 'B';
if (pct >= 0.5) return 'C';
return 'D';
}
gradeColor(): string {
const pct = this.score / this.questions.length;
if (pct >= 0.9) return '#52C41A'; // A → 绿色
if (pct >= 0.7) return '#1677FF'; // B → 蓝色
if (pct >= 0.5) return '#FAAD14'; // C → 黄色(背景用,非白字叠底)
return '#FF4D4F'; // D → 红色
}
等级跨度:
- A(90%+):9-10 题正确,卓越
- B(70-89%):7-8 题正确,良好
- C(50-69%):5-6 题正确,及格
- D(<50%):0-4 题正确,继续努力
等级用 48sp 大号粗体字展示,颜色随等级变化。下方有一个水平进度条,填充宽度等于正确率百分比,颜色与等级颜色一致——在分数和视觉上双重呈现成绩。
注意 C 级使用 #FAAD14(黄色),但仅用于评级文字和进度条,不作为白色文字的叠加背景——遵循项目中"黄色不给白字做背景"的颜色规则。
六、UI 设计
6.1 开始界面
开始界面是一个垂直居中的介绍页,包含:
- 64sp 📝 图标(大号 emoji,营造轻松氛围)
- "趣味问答"标题
- 两句说明文字(“10 道题,测试你的知识储备” + “涵盖编程、科学、常识等多个领域”)
- "开始答题"按钮(紫色
#667eea)
界面极简、无干扰——用户只有一个操作:点击"开始答题"。这是一个经典的"单按钮启动页"设计,减少初次用户的选择成本。
6.2 答题界面
答题界面从上到下依次为:
第 3/10 题 01:23 ✓ 2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 进度条
光年是什么的单位?
A 距离 ← 四个选项
B 时间
C 速度
D 质量
问题文字使用 18sp 粗体,独占一行,与选项之间有 24vp 的间距——确保问题和选项在视觉上明确分开。选项使用全宽卡片布局,点击区域覆盖整个卡片(而非仅文字),减少误触。
6.3 结果界面
结果界面以垂直居中方式展示:
- 表情图标(🎉 8+ 分 / 👍 5-7 分 / 💪 0-4 分)
- "答题完成"标题
- 答对数字(“答对 X / 10 题”)
- 48sp 彩色评级字母
- 用时统计
- 百分比进度条
- "再来一次"按钮
表情图标的选择是一个细节:高分用 🎉(庆祝)、中等用 👍(鼓励)、低分用 💪(加油),不使用任何带有贬低或负面含义的图标。这种"总是鼓励"的反馈风格让用户即使得分不高也愿意再试一次。
七、完整代码结构
QuizPage (~270 行)
├── 数据定义
│ ├── Question — 题目接口(question / options / answer)
│ ├── Q_BANK[10] — 题库
│ └── LABELS[4] — 选项标签 ['A','B','C','D']
├── 状态变量
│ ├── @State questions — 当前局的题目(已洗牌)
│ ├── @State currentIdx / score / elapsedSec — 答题进度
│ ├── @State quizStarted / quizFinished — 阶段控制
│ └── @State selectedOption — 反馈状态(-1=未选)
├── 游戏逻辑
│ ├── startQuiz() — 洗牌 + 初始化 + 启动计时
│ ├── selectOption() — 选题 → 计分 → 反馈 → 推进
│ └── shuffle() — Fisher-Yates 洗牌
├── 视觉辅助
│ ├── optionBg() — 选项背景色(灰/绿/红)
│ ├── optionTextColor() — 选项文字色
│ ├── grade() — A-D 等级
│ └── gradeColor() — 等级颜色
├── 视图(三阶段条件渲染)
│ ├── 开始界面 — 图标 + 说明 + 开始按钮
│ ├── 答题界面 — 进度条 + 问题 + 四选项(ForEach)
│ └── 结果界面 — 等级 + 分数 + 进度条 + 再来一次
└── 生命周期
└── aboutToDisappear() — 清理计时器和反馈定时器
八、总结
本文从零构建了一个趣味问答应用。与前两篇游戏类文章(记忆翻牌、井字棋)不同,趣味问答不是一款游戏——它是教育测试工具,每道题的反馈都有"学习"的目的。从技术角度看,它也是三阶段条件渲染的典型示例——两个布尔变量切换三个完整界面,每个界面的布局、交互和信息密度都截然不同。
核心要点回顾:
-
三阶段状态机:用
quizStarted和quizFinished两个@State布尔变量控制"开始 → 答题 → 结果"三阶段的if/else if/else条件渲染。两个变量产生 2²=4 种组合,其中 3 种有效(第四种防御性兜底)。 -
即时三色反馈:选中答案后,正确选项绿底绿字、错误选择红底红字、其余灰色——三色系统同时传达"正确答案是什么"和"你选了什么"两个信息。1.5s 反馈时长给用户充足的学习时间,又不打断答题节奏。
-
自动推进:
setInterval(fn, 1500)在反馈期结束后自动调用下一题或结束。selectedOption从 -1 变为选择索引再变回 -1,驱动 UI 在"可交互"和"反馈展示"之间切换。重复点击守卫(if (this.selectedOption !== -1) return)防止反馈期间改答案。 -
Fisher-Yates 洗题:每次开始前打乱题目顺序,但保持选项内部顺序不变——洗题目增加重玩性,不洗选项避免混淆。这是与密码生成器和记忆翻牌中同一算法的第三次应用。
-
A-D 评级 + 进度条:4 级评级(90%→A, 70%→B, 50%→C, 其余→D),四种等级颜色(绿/蓝/黄/红)。水平进度条用百分比宽度可视化成绩。C 级的黄色仅用于文字和进度条,不给白字做背景。
-
总是鼓励的结果设计:高分 🎉、中等 👍、低分 💪——三个表情符号覆盖全部等级,没有一个暗示"失败"。这种"正向引导"的结果展示降低了用户放弃的可能性,增加了"再来一次"的点击率。
趣味问答是三道选择题和一段说明文字构成的最小知识测试单元,但在这个 270 行的 ArkUI 实现中,它包含了题库管理、随机化、即时反馈、自动推进、计时计分和评级展示的完整流水线。它是教育类应用的微型原型,也是三阶段条件渲染的集中示范。
更多推荐


所有评论(0)