返回文章列表

从一次视觉错觉到状态机边界修复

记录语音按钮问题从“看起来像视觉偏差”到最终定位为状态机边界问题的过程,以及为什么 UI 问题不能只靠调样式。

一开始它看起来只是“不对劲”

语音按钮的问题最初并不像一个严重 bug。

页面能打开,按钮能点,录音状态也能出现。但视觉上总觉得有些不对:按钮状态切换后,反馈不像预期那样稳定,某些瞬间像是背景、边界或状态样式没有对齐。

这种问题很容易被归类成 UI 细节:

  • 是不是颜色不对;
  • 是不是圆角没还原;
  • 是不是 pressed 状态写错;
  • 是不是某个背景 drawable 覆盖了;
  • 是不是动画时机太快。

如果沿着这个方向修,很可能会不断微调样式,却始终没有抓到根因。

视觉问题有时是状态问题

后面继续追查时,我逐渐意识到:这个问题不是单纯的视觉还原偏差,而是状态边界不够清楚。

语音按钮不是一个静态控件。它至少承载这些状态:

idle
recording
recognizing
success
error
disabled

每个状态对应不同的文案、背景、动效、可点击性和后续操作。如果状态之间的切换没有被清楚建模,UI 表现就会变得像“视觉错觉”。

比如按钮刚从录音中切到识别中,如果旧状态的视觉残留没有被清理,用户看到的就不是一个明确状态,而是一段混合反馈。

问题藏在状态过渡,而不是某个样式值

这类问题最麻烦的地方在于,单独看每个状态都可能是对的。

idle 状态看起来正常。
recording 状态看起来也正常。
recognizing 状态单独触发时也不一定有问题。

真正的问题出现在过渡路径里:

  • 从空闲到录音;
  • 从录音到识别;
  • 从识别成功到确认;
  • 从识别失败回到空闲;
  • 从录音中取消回到空闲;
  • 从页面退出中断回到安全状态。

UI bug 经常不是“状态 A 错了”,而是“从状态 A 到状态 B 的路上漏了一步”。

用状态机重新描述按钮

修正这类问题时,我不想继续堆条件判断。

更稳的方式是把按钮当成一个小状态机,而不是一个有很多布尔值的 View。

Idle
  -> Recording
  -> Disabled

Recording
  -> Recognizing
  -> Idle

Recognizing
  -> Success
  -> Error
  -> Idle

Success
  -> Idle

Error
  -> Idle

这样做的好处是,每一次状态切换都能被明确讨论:

  • 进入状态时要设置哪些 UI;
  • 离开状态时要清理哪些 UI;
  • 当前状态是否允许点击;
  • 是否允许重复触发;
  • 异常发生时退回哪里。

当状态机清楚之后,视觉问题就不再靠猜。

AI 在这里适合做路径枚举

这次修复过程中,AI 最有用的地方不是直接给一个样式答案,而是帮我把状态路径枚举完整。

我可以让它围绕按钮行为追问:

  • 有哪些入口会改变状态;
  • 是否存在重复点击;
  • 异步回调回来时页面是否还存在;
  • 失败后是否恢复可点击;
  • 成功后是否等待用户确认;
  • 取消时是否回到一致的空闲态。

这些问题比“把颜色改浅一点”更接近根因。

对 UI 状态复杂的控件来说,AI 适合做路径穷举,人负责判断这些路径是否符合产品体验。

这次修复带来的提醒

表面上,这是一次语音按钮的视觉修复。

但真正沉淀下来的经验是:当一个 UI 问题反复调样式仍然不稳定时,应该暂停一下,检查它背后的状态模型。

尤其是这类控件:

  • 会响应用户操作;
  • 会等待异步结果;
  • 会出现成功和失败;
  • 会涉及权限或网络;
  • 会在页面生命周期中被中断。

它们都不应该只被当成“按钮样式”。

UI 的稳定感来自状态的稳定。视觉错觉很多时候不是眼睛的问题,而是状态边界没有被工程化地表达出来。