灯光模拟HarmonyOS应用实战-64-请选择答案为何显示成功色-用FeedbackState表达中性过程与结果
“请选择答案”是一条等待用户操作的中性提示,它既不代表成功,也不代表失败。当前The_kemusan页面却用answerCorrect=true初始化这条文案,渲染时又把true映射到app.color.success。于是,从源码关系看,答题前的提示会使用成功语义的颜色资源。
这个现象的根源不在某个颜色值,而在状态模型只能表达真假两种结果。答题前、正在提交、已经答对、已经答错至少是四种不同阶段。一个名为answerCorrect的布尔值适合描述作答结果,却不适合同时承担“当前反馈应该如何呈现”的职责。
本文只讨论反馈提示,不扩展到选项锁定、答案揭示或完整页面状态机。方案用FeedbackState把中性、过程、成功和错误分开,再把业务语义映射到颜色、图标、文本和播报策略。当前颜色的实际 RGB、真机对比度与辅助功能效果均未在本文中验证。
一、源码中待答提示和正确结果共用 true
Index.ets的初始状态把answerMessage设为“请选择答案”,同时把answerCorrect设为true。开始新的理论练习和切换下一题时也会再次写入这组值。只有用户选择答案后,answerCorrect才真正表示本次答案是否正确。
@StateselectedAnswer:string='';@StateanswerMessage:string='请选择答案';@StateanswerCorrect:boolean=true;privatestartTheoryPractice():void{this.selectedAnswer='';this.answerMessage='请选择答案';this.answerCorrect=true;}privatenextTheoryQuestion():void{this.selectedAnswer='';this.answerMessage='请选择答案';this.answerCorrect=true;}这段代码说明true有两个含义:答题前表示“没有错误,先用正向样式”,答题后表示“答案正确”。两个含义的生命周期不同,却占用同一个变量。变量名让读者以为它始终是作答事实,初始化逻辑却把它当成呈现开关。
渲染位置进一步固定了这种复用:
Text(this.answerMessage).fontSize(14).fontWeight(FontWeight.Medium).fontColor(this.answerCorrect?$r('app.color.success'):$r('app.color.danger'))能够确认的是资源名映射关系,不能从这里只读断言success在设备上一定呈现为某个具体绿色,也不能断言对比度不足。资源值、主题切换、显示模式和设备渲染都需要另行查看与实测。
二、真假布尔无法覆盖反馈阶段
把反馈压成布尔值时,所有非错误状态都会被迫挤进true,或所有未完成状态都被迫挤进false。无论选哪边,中性和过程态都会借用结果态的视觉含义。
| 业务时刻 | 文案示例 | 是否已有结果 | 合理语义 | 用answerCorrect表达的问题 |
|---|---|---|---|---|
| 题目刚出现 | 请选择答案 | 否 | 中性 | 只能借用true |
| 正在处理提交 | 正在提交 | 否 | 过程/信息 | 没有对应取值 |
| 答案正确 | 回答正确 | 是 | 成功 | true含义清晰 |
| 答案错误 | 回答错误 | 是 | 错误 | false含义清晰 |
| 新题重置 | 请选择答案 | 否 | 中性 | 再次把结果字段改为true |
问题链很直接:页面需要四种语义,状态只有两种取值;重置函数为了获得正向颜色写入true;渲染器再把true当作成功;最终文案与视觉含义不一致。
修复时不应把初始值简单改成false。那只会让“请选择答案”使用错误色,把中性从成功一侧搬到失败一侧。也不应添加第二个isNeutral布尔值,因为isNeutral=true与answerCorrect=false等组合会制造互相矛盾的状态。
三、FeedbackState 同时表达阶段和语义
最小模型可以用枚举表达四种反馈语义,并把稳定原因码、文案键和是否需要播报放进同一个值对象。下面是方案示例,尚未写入当前工程。
exportenumFeedbackTone{NEUTRAL='neutral',INFO='info',SUCCESS='success',ERROR='error'}exportenumFeedbackPhase{IDLE='idle',IN_PROGRESS='in_progress',SETTLED='settled'}exportinterfaceFeedbackState{tone:FeedbackTone;phase:FeedbackPhase;code:string;messageKey:string;announce:boolean;}tone回答“这条反馈的语义是什么”,phase回答“业务是否已经得到终态”,code用于测试和诊断,messageKey用于查找文案,announce则表达是否应主动提示辅助技术。四个字段不是为了堆配置,而是让过去隐藏在布尔值中的决定显式化。
可以先定义几个不可变常量:
constWAITING_FOR_ANSWER:FeedbackState={tone:FeedbackTone.NEUTRAL,phase:FeedbackPhase.IDLE,code:'WAITING_FOR_ANSWER',messageKey:'theory.choose_answer',announce:false};constANSWER_CORRECT:FeedbackState={tone:FeedbackTone.SUCCESS,phase:FeedbackPhase.SETTLED,code:'ANSWER_CORRECT',messageKey:'theory.answer_correct',announce:true};constANSWER_WRONG:FeedbackState={tone:FeedbackTone.ERROR,phase:FeedbackPhase.SETTLED,code:'ANSWER_WRONG',messageKey:'theory.answer_wrong',announce:true};待答状态明确为NEUTRAL + IDLE,因此不再伪装成成功结果。正在进行的状态可以使用INFO + IN_PROGRESS,但只有当页面确实存在异步提交或数据加载时才创建,不能为了凑齐四种颜色虚构一个用户看不到的流程。
四、业务事件先归约成唯一反馈状态
页面不应在多个函数里分别改文案、布尔值、颜色和播报开关。更稳的方式是把业务事件交给一个纯归约器,产出完整的FeedbackState。同一个事件只生成一个状态,渲染器不再推测答案是否已经提交。
exportenumTheoryFeedbackEvent{QUESTION_PRESENTED='question_presented',SUBMIT_STARTED='submit_started',ANSWER_ACCEPTED='answer_accepted',ANSWER_REJECTED='answer_rejected'}exportfunctionreduceTheoryFeedback(event:TheoryFeedbackEvent):FeedbackState{if(event===TheoryFeedbackEvent.QUESTION_PRESENTED){returnWAITING_FOR_ANSWER;}if(event===TheoryFeedbackEvent.SUBMIT_STARTED){return{tone:FeedbackTone.INFO,phase:FeedbackPhase.IN_PROGRESS,code:'ANSWER_SUBMITTING',messageKey:'theory.answer_submitting',announce:false};}if(event===TheoryFeedbackEvent.ANSWER_ACCEPTED){returnANSWER_CORRECT;}returnANSWER_WRONG;}这个函数只决定语义,不引用$r资源,不操作 ArkUI 组件,也不写历史。纯函数可以用小型输入表覆盖,不需要启动页面。若以后错误结果需要携带正确选项,可把参数放进独立的messageArgs,不要把最终拼接文本当作状态身份。
页面状态可以从三项收敛为一项:
@Statefeedback:FeedbackState=WAITING_FOR_ANSWER;privatepresentQuestion():void{this.selectedAnswer='';this.feedback=reduceTheoryFeedback(TheoryFeedbackEvent.QUESTION_PRESENTED);}privateapplyAnswerResult(correct:boolean):void{this.feedback=reduceTheoryFeedback(correct?TheoryFeedbackEvent.ANSWER_ACCEPTED:TheoryFeedbackEvent.ANSWER_REJECTED);}answerCorrect仍可作为领域结果保留在记录对象中,但不再承担提示样式。一次作答事实和当前反馈视图可以来源相同,却不应是同一个变量。
五、颜色、图标和文案分别从语义映射
有了语义状态后,视觉层再做资源映射。映射函数应返回资源引用或页面可消费的展示对象,而不是让每个Text自行写四段条件。
interfaceFeedbackVisual{color:ResourceColor;icon:Resource;message:string;}privatetoFeedbackVisual(state:FeedbackState):FeedbackVisual{if(state.tone===FeedbackTone.NEUTRAL){return{color:$r('app.color.text_secondary'),icon:$r('app.media.feedback_neutral'),message:this.resolveMessage(state.messageKey)};}if(state.tone===FeedbackTone.INFO){return{color:$r('app.color.accent'),icon:$r('app.media.feedback_info'),message:this.resolveMessage(state.messageKey)};}if(state.tone===FeedbackTone.SUCCESS){return{color:$r('app.color.success'),icon:$r('app.media.feedback_success'),message:this.resolveMessage(state.messageKey)};}return{color:$r('app.color.danger'),icon:$r('app.media.feedback_error'),message:this.resolveMessage(state.messageKey)};}资源名称是方案中的占位示例,当前工程未必已经有这些媒体资源。实际实现可以先只调整文字颜色,避免为了状态改造同时引入大量图标资产。关键是映射入口唯一,后续主题或深色模式调整只改资源层,不改变业务事件。
颜色不能成为唯一线索。成功和错误应同时有文案或图标;中性提示不应使用勾号;进行中状态如果没有真实等待,也不应显示旋转动画。视觉层负责清楚表达已有语义,不能反过来决定业务是否成功。
六、播报策略与颜色映射相互独立
announce不等于“只要状态变化就朗读”。题目刚出现时,页面可能已经通过标题让辅助技术获得上下文,再主动播报“请选择答案”可能重复;回答结果通常更值得及时提示。具体 ArkUI 辅助功能接口与参数必须按目标 SDK 文档核对,本文不编造调用名称。
可以先把策略写成与平台无关的决策:
interfaceAnnouncementDecision{shouldAnnounce:boolean;messageKey:string;}functiondecideAnnouncement(previous:FeedbackState,next:FeedbackState):AnnouncementDecision{constchanged=previous.code!==next.code;constterminal=next.phase===FeedbackPhase.SETTLED;return{shouldAnnounce:changed&&terminal&&next.announce,messageKey:next.messageKey};}这个策略把“状态变化”“已经形成结果”“该状态允许主动播报”三个条件同时检查,避免页面重渲染时重复提示。同一结果如果因父组件刷新被再次赋值,code没变就不重复触发。
| 反馈状态 | 颜色角色 | 图标角色 | 默认主动播报 | 原因 |
|---|---|---|---|---|
NEUTRAL | 次级文字 | 无或中性提示 | 否 | 尚未产生结果 |
INFO | 强调信息 | 进度或信息 | 视流程而定 | 只在真实等待较长时提示 |
SUCCESS | 成功资源 | 勾选 | 是 | 用户需要知道作答已接受 |
ERROR | 错误资源 | 警示 | 是 | 用户需要知道结果与后续信息 |
实际可访问性结论必须通过屏幕阅读、焦点顺序、动态内容提示与颜色对比度核对。源码中的资源名不能替代这些设备证据。
七、理论与实操可以共用外壳但保留领域码
当前页面还有message与isSuccessMessage,用于灯光演示和考试反馈,它们也呈现真假二分。可以复用FeedbackState这一外壳,但不要把理论和实操所有原因码混成一组模糊字符串。
建议用领域前缀区分:
- 理论:
THEORY_WAITING、THEORY_CORRECT、THEORY_WRONG。 - 普通灯光:
LIGHT_DEMO、LIGHT_ACTION_CORRECT、LIGHT_TIMEOUT。 - 科三实操:
PRACTICAL_DEMO、PRACTICAL_ACTION_CORRECT、PRACTICAL_TIMEOUT。 - 仓储或加载:若确有页面反馈,再使用
HISTORY_LOADING、HISTORY_READ_ERROR。
统一的是tone/phase/code/messageKey/announce结构和呈现适配器,领域判定仍由各自业务函数完成。这样既减少颜色条件复制,又不会让“答题错误”和“灯光超时”失去各自诊断信息。
一个适配函数可以要求领域码唯一:
functioncreateSettledFeedback(code:string,success:boolean,messageKey:string):FeedbackState{if(code.length===0||messageKey.length===0){thrownewError('feedback identity is required');}return{tone:success?FeedbackTone.SUCCESS:FeedbackTone.ERROR,phase:FeedbackPhase.SETTLED,code,messageKey,announce:true};}这不是让所有调用点继续传任意布尔值。调用方仍应先得到明确的领域结果,再选择对应原因码;该函数只减少重复构造。
八、防止旧异步结果覆盖新题的中性状态
如果答题流程将来变成异步,旧题提交结果可能在用户已经进入下一题后返回。仅有FeedbackState仍不足以阻止过期回调覆盖新题的WAITING_FOR_ANSWER。状态应绑定稳定题目编号或会话令牌。
interfaceScopedFeedback{questionId:string;revision:number;state:FeedbackState;}privateapplyScopedFeedback(next:ScopedFeedback):void{if(next.questionId!==this.getCurrentQuestion().id){return;}if(next.revision<this.feedbackRevision){return;}this.feedbackRevision=next.revision;this.feedback=next.state;}当前answerTheoryQuestion()是同步比较答案,本次只读源码没有证明已经存在这种竞态。这里把它列为演进边界:一旦接入网络判题、异步存储确认或延时动画,就要避免旧反馈越过题目切换。
重置新题时应通过一个入口同时更新selectedAnswer、题目身份和feedback。若先重置反馈、后更新题目,异步回调的身份核对可能短暂使用旧题;具体顺序应由单向状态更新函数固定。
九、测试矩阵要覆盖语义、呈现和重复播报
第一层只测reduceTheoryFeedback(),第二层测toFeedbackVisual()的资源角色,第三层再测页面从待答到结果再到下一题的序列。不要只截图比较颜色,因为状态码与阶段同样重要。
constexpectations:Array<[TheoryFeedbackEvent,FeedbackTone,FeedbackPhase]>=[[TheoryFeedbackEvent.QUESTION_PRESENTED,FeedbackTone.NEUTRAL,FeedbackPhase.IDLE],[TheoryFeedbackEvent.SUBMIT_STARTED,FeedbackTone.INFO,FeedbackPhase.IN_PROGRESS],[TheoryFeedbackEvent.ANSWER_ACCEPTED,FeedbackTone.SUCCESS,FeedbackPhase.SETTLED],[TheoryFeedbackEvent.ANSWER_REJECTED,FeedbackTone.ERROR,FeedbackPhase.SETTLED]];for(constitemofexpectations){conststate=reduceTheoryFeedback(item[0]);expect(state.tone).toBe(item[1]);expect(state.phase).toBe(item[2]);}| 用例 | 操作序列 | 预期语义 | 重点核对 |
|---|---|---|---|
| F01 | 页面首次进入 | NEUTRAL/IDLE | “请选择答案”不用成功资源 |
| F02 | 选择正确答案 | SUCCESS/SETTLED | 文案、颜色、原因码一致 |
| F03 | 选择错误答案 | ERROR/SETTLED | 正确答案补充不改变错误语义 |
| F04 | 点击下一题 | 回到NEUTRAL/IDLE | 不残留上一题成功或错误 |
| F05 | 同一结果重复赋值 | 状态不变 | 不重复主动播报 |
| F06 | 旧题延迟回调 | 被身份守卫忽略 | 新题中性反馈不被覆盖 |
| F07 | 切换主题 | 语义不变 | 仅资源映射变化 |
| F08 | 理论与实操连续操作 | 领域码不同 | 不把两类失败混成同一记录 |
这些是实现后的用例设计,本文没有运行它们。页面截图只能证明某一次渲染,不能证明状态序列和重复回调;纯函数测试、ArkUI 交互与真机辅助功能检查应分别保留结果。
十、验证清单、排障表与事实边界
实现后可逐项核对:
- “请选择答案”对应
NEUTRAL/IDLE。 - “正在提交”只在真实异步阶段使用
INFO/IN_PROGRESS。 - 正确与错误分别对应
SUCCESS和ERROR。 - 业务函数不直接选择颜色资源。
- 视觉映射不反推答案是否正确。
- 文案、图标和颜色来源于同一个
FeedbackState。 - 中性、信息、成功、错误都具有稳定原因码。
- 主动播报只在状态真正变化且策略允许时触发。
- 颜色不是结果的唯一表达线索。
- 下一题重置不会残留上一题反馈。
- 旧异步回调不能覆盖当前题反馈。
- 理论与实操复用结构,但原因码保持领域区分。
- 目标 ArkTS 写法、资源引用与辅助功能接口经过文档和编译核对。
- 深色模式、字体放大、屏幕阅读与真机视觉分别检查。
| 症状 | 优先核对 | 常见原因 | 修复方向 |
|---|---|---|---|
| “请选择答案”仍使用成功色 | 初始feedback.tone | 旧answerCorrect仍参与渲染 | 让提示组件只消费FeedbackState |
| 下一题仍显示“回答正确” | 新题重置入口 | 只清空了选项 | 通过统一事件写入WAITING_FOR_ANSWER |
| 文案正确但图标错误 | 视觉映射表 | 组件分别维护条件 | 返回完整FeedbackVisual |
| 页面刷新重复播报 | 前后状态比较 | 每次重渲染都触发 | 比较code与终态阶段 |
| 等待状态一直转圈 | SUBMIT_STARTED结束路径 | 失败或成功未归约 | 为每条异步出口写终态事件 |
| 实操错误显示理论原因码 | 领域事件映射 | 复用了固定字符串 | 为理论、灯光和实操保留前缀 |
| 真机颜色含义不清 | 资源和辅助线索 | 只依靠颜色区分 | 增加准确文案、图标并做设备检查 |
| 旧题结果覆盖新题 | 题目身份与修订号 | 延迟回调无作用域 | 加入questionId/revision守卫 |
当前源码能够确认的是:answerMessage初始为“请选择答案”,answerCorrect初始为true;提示文字根据该布尔值选择success或danger资源;作答后该布尔值才表示答案是否正确。选项文字在未作答时另走中性色逻辑,因此本文没有把问题扩大成整个题卡都使用成功色。
FeedbackState、归约器、展示对象、播报决策、领域原因码和异步作用域都属于改进方案,尚未写入The_kemusan。本次没有运行项目构建,没有生成新的 HAP,没有启动模拟器,也没有在真机验证资源颜色、主题切换、屏幕阅读或动态内容提示。最终资源名称、ArkTS 类型约束和辅助功能调用必须以目标 SDK 文档、编译结果和设备行为为准。