灯光模拟HarmonyOS应用实战-64-请选择答案为何显示成功色-用FeedbackState表达中性过程与结果

“请选择答案”是一条等待用户操作的中性提示,它既不代表成功,也不代表失败。当前 The_kemusan 页面却用 answerCorrect=true 初始化这条文案,渲染时又把 true 映射到 app.color.success。于是,从源码关系看,答题前的提示会使用成功语义的颜色资源。

这个现象的根源不在某个颜色值,而在状态模型只能表达真假两种结果。答题前、正在提交、已经答对、已经答错至少是四种不同阶段。一个名为 answerCorrect 的布尔值适合描述作答结果,却不适合同时承担“当前反馈应该如何呈现”的职责。

本文只讨论反馈提示,不扩展到选项锁定、答案揭示或完整页面状态机。方案用 FeedbackState 把中性、过程、成功和错误分开,再把业务语义映射到颜色、图标、文本和播报策略。当前颜色的实际 RGB、真机对比度与辅助功能效果均未在本文中验证。

FeedbackState 四种反馈语义

一、源码中待答提示和正确结果共用 true

Index.ets 的初始状态把 answerMessage 设为“请选择答案”,同时把 answerCorrect 设为 true。开始新的理论练习和切换下一题时也会再次写入这组值。只有用户选择答案后,answerCorrect 才真正表示本次答案是否正确。

@State selectedAnswer: string = '';
@State answerMessage: string = '请选择答案';
@State answerCorrect: boolean = true;

private startTheoryPractice(): void {
  this.selectedAnswer = '';
  this.answerMessage = '请选择答案';
  this.answerCorrect = true;
}

private nextTheoryQuestion(): 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=trueanswerCorrect=false 等组合会制造互相矛盾的状态。

三、FeedbackState 同时表达阶段和语义

最小模型可以用枚举表达四种反馈语义,并把稳定原因码、文案键和是否需要播报放进同一个值对象。下面是方案示例,尚未写入当前工程。

export enum FeedbackTone {
  NEUTRAL = 'neutral',
  INFO = 'info',
  SUCCESS = 'success',
  ERROR = 'error'
}

export enum FeedbackPhase {
  IDLE = 'idle',
  IN_PROGRESS = 'in_progress',
  SETTLED = 'settled'
}

export interface FeedbackState {
  tone: FeedbackTone;
  phase: FeedbackPhase;
  code: string;
  messageKey: string;
  announce: boolean;
}

tone 回答“这条反馈的语义是什么”,phase 回答“业务是否已经得到终态”,code 用于测试和诊断,messageKey 用于查找文案,announce 则表达是否应主动提示辅助技术。四个字段不是为了堆配置,而是让过去隐藏在布尔值中的决定显式化。

可以先定义几个不可变常量:

const WAITING_FOR_ANSWER: FeedbackState = {
  tone: FeedbackTone.NEUTRAL,
  phase: FeedbackPhase.IDLE,
  code: 'WAITING_FOR_ANSWER',
  messageKey: 'theory.choose_answer',
  announce: false
};

const ANSWER_CORRECT: FeedbackState = {
  tone: FeedbackTone.SUCCESS,
  phase: FeedbackPhase.SETTLED,
  code: 'ANSWER_CORRECT',
  messageKey: 'theory.answer_correct',
  announce: true
};

const ANSWER_WRONG: FeedbackState = {
  tone: FeedbackTone.ERROR,
  phase: FeedbackPhase.SETTLED,
  code: 'ANSWER_WRONG',
  messageKey: 'theory.answer_wrong',
  announce: true
};

待答状态明确为 NEUTRAL + IDLE,因此不再伪装成成功结果。正在进行的状态可以使用 INFO + IN_PROGRESS,但只有当页面确实存在异步提交或数据加载时才创建,不能为了凑齐四种颜色虚构一个用户看不到的流程。

四、业务事件先归约成唯一反馈状态

页面不应在多个函数里分别改文案、布尔值、颜色和播报开关。更稳的方式是把业务事件交给一个纯归约器,产出完整的 FeedbackState。同一个事件只生成一个状态,渲染器不再推测答案是否已经提交。

export enum TheoryFeedbackEvent {
  QUESTION_PRESENTED = 'question_presented',
  SUBMIT_STARTED = 'submit_started',
  ANSWER_ACCEPTED = 'answer_accepted',
  ANSWER_REJECTED = 'answer_rejected'
}

export function reduceTheoryFeedback(
  event: TheoryFeedbackEvent
): FeedbackState {
  if (event === TheoryFeedbackEvent.QUESTION_PRESENTED) {
    return WAITING_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) {
    return ANSWER_CORRECT;
  }
  return ANSWER_WRONG;
}

这个函数只决定语义,不引用 $r 资源,不操作 ArkUI 组件,也不写历史。纯函数可以用小型输入表覆盖,不需要启动页面。若以后错误结果需要携带正确选项,可把参数放进独立的 messageArgs,不要把最终拼接文本当作状态身份。

页面状态可以从三项收敛为一项:

@State feedback: FeedbackState = WAITING_FOR_ANSWER;

private presentQuestion(): void {
  this.selectedAnswer = '';
  this.feedback = reduceTheoryFeedback(
    TheoryFeedbackEvent.QUESTION_PRESENTED
  );
}

private applyAnswerResult(correct: boolean): void {
  this.feedback = reduceTheoryFeedback(
    correct
      ? TheoryFeedbackEvent.ANSWER_ACCEPTED
      : TheoryFeedbackEvent.ANSWER_REJECTED
  );
}

answerCorrect 仍可作为领域结果保留在记录对象中,但不再承担提示样式。一次作答事实和当前反馈视图可以来源相同,却不应是同一个变量。

五、颜色、图标和文案分别从语义映射

FeedbackState 反馈流程

有了语义状态后,视觉层再做资源映射。映射函数应返回资源引用或页面可消费的展示对象,而不是让每个 Text 自行写四段条件。

interface FeedbackVisual {
  color: ResourceColor;
  icon: Resource;
  message: string;
}

private toFeedbackVisual(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 文档核对,本文不编造调用名称。

可以先把策略写成与平台无关的决策:

interface AnnouncementDecision {
  shouldAnnounce: boolean;
  messageKey: string;
}

function decideAnnouncement(
  previous: FeedbackState,
  next: FeedbackState
): AnnouncementDecision {
  const changed = previous.code !== next.code;
  const terminal = next.phase === FeedbackPhase.SETTLED;
  return {
    shouldAnnounce: changed && terminal && next.announce,
    messageKey: next.messageKey
  };
}

这个策略把“状态变化”“已经形成结果”“该状态允许主动播报”三个条件同时检查,避免页面重渲染时重复提示。同一结果如果因父组件刷新被再次赋值,code 没变就不重复触发。

反馈状态颜色角色图标角色默认主动播报原因
NEUTRAL次级文字无或中性提示尚未产生结果
INFO强调信息进度或信息视流程而定只在真实等待较长时提示
SUCCESS成功资源勾选用户需要知道作答已接受
ERROR错误资源警示用户需要知道结果与后续信息

实际可访问性结论必须通过屏幕阅读、焦点顺序、动态内容提示与颜色对比度核对。源码中的资源名不能替代这些设备证据。

七、理论与实操可以共用外壳但保留领域码

反馈语义职责结构

当前页面还有 messageisSuccessMessage,用于灯光演示和考试反馈,它们也呈现真假二分。可以复用 FeedbackState 这一外壳,但不要把理论和实操所有原因码混成一组模糊字符串。

建议用领域前缀区分:

  • 理论:THEORY_WAITINGTHEORY_CORRECTTHEORY_WRONG
  • 普通灯光:LIGHT_DEMOLIGHT_ACTION_CORRECTLIGHT_TIMEOUT
  • 科三实操:PRACTICAL_DEMOPRACTICAL_ACTION_CORRECTPRACTICAL_TIMEOUT
  • 仓储或加载:若确有页面反馈,再使用 HISTORY_LOADINGHISTORY_READ_ERROR

统一的是 tone/phase/code/messageKey/announce 结构和呈现适配器,领域判定仍由各自业务函数完成。这样既减少颜色条件复制,又不会让“答题错误”和“灯光超时”失去各自诊断信息。

一个适配函数可以要求领域码唯一:

function createSettledFeedback(
  code: string,
  success: boolean,
  messageKey: string
): FeedbackState {
  if (code.length === 0 || messageKey.length === 0) {
    throw new Error('feedback identity is required');
  }
  return {
    tone: success ? FeedbackTone.SUCCESS : FeedbackTone.ERROR,
    phase: FeedbackPhase.SETTLED,
    code,
    messageKey,
    announce: true
  };
}

这不是让所有调用点继续传任意布尔值。调用方仍应先得到明确的领域结果,再选择对应原因码;该函数只减少重复构造。

八、防止旧异步结果覆盖新题的中性状态

如果答题流程将来变成异步,旧题提交结果可能在用户已经进入下一题后返回。仅有 FeedbackState 仍不足以阻止过期回调覆盖新题的 WAITING_FOR_ANSWER。状态应绑定稳定题目编号或会话令牌。

interface ScopedFeedback {
  questionId: string;
  revision: number;
  state: FeedbackState;
}

private applyScopedFeedback(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() 的资源角色,第三层再测页面从待答到结果再到下一题的序列。不要只截图比较颜色,因为状态码与阶段同样重要。

const expectations: 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 (const item of expectations) {
  const state = 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
  • 正确与错误分别对应 SUCCESSERROR
  • 业务函数不直接选择颜色资源。
  • 视觉映射不反推答案是否正确。
  • 文案、图标和颜色来源于同一个 FeedbackState
  • 中性、信息、成功、错误都具有稳定原因码。
  • 主动播报只在状态真正变化且策略允许时触发。
  • 颜色不是结果的唯一表达线索。
  • 下一题重置不会残留上一题反馈。
  • 旧异步回调不能覆盖当前题反馈。
  • 理论与实操复用结构,但原因码保持领域区分。
  • 目标 ArkTS 写法、资源引用与辅助功能接口经过文档和编译核对。
  • 深色模式、字体放大、屏幕阅读与真机视觉分别检查。
症状优先核对常见原因修复方向
“请选择答案”仍使用成功色初始 feedback.toneanswerCorrect 仍参与渲染让提示组件只消费 FeedbackState
下一题仍显示“回答正确”新题重置入口只清空了选项通过统一事件写入 WAITING_FOR_ANSWER
文案正确但图标错误视觉映射表组件分别维护条件返回完整 FeedbackVisual
页面刷新重复播报前后状态比较每次重渲染都触发比较 code 与终态阶段
等待状态一直转圈SUBMIT_STARTED 结束路径失败或成功未归约为每条异步出口写终态事件
实操错误显示理论原因码领域事件映射复用了固定字符串为理论、灯光和实操保留前缀
真机颜色含义不清资源和辅助线索只依靠颜色区分增加准确文案、图标并做设备检查
旧题结果覆盖新题题目身份与修订号延迟回调无作用域加入 questionId/revision 守卫

当前源码能够确认的是:answerMessage 初始为“请选择答案”,answerCorrect 初始为 true;提示文字根据该布尔值选择 successdanger 资源;作答后该布尔值才表示答案是否正确。选项文字在未作答时另走中性色逻辑,因此本文没有把问题扩大成整个题卡都使用成功色。

FeedbackState、归约器、展示对象、播报决策、领域原因码和异步作用域都属于改进方案,尚未写入 The_kemusan。本次没有运行项目构建,没有生成新的 HAP,没有启动模拟器,也没有在真机验证资源颜色、主题切换、屏幕阅读或动态内容提示。最终资源名称、ArkTS 类型约束和辅助功能调用必须以目标 SDK 文档、编译结果和设备行为为准。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐