AIUI 焦点高亮切换失效排查与规避:为什么按钮不变换形态呢?

适用平台:Rokid AIUI(YodaOS-Sprite)AR 眼镜 阅读对象:入门级开发者 范围说明:只讲"焦点高亮 / 选中态切换"这一条链路,不涉及语音交互和页面跳转


一、首页的焦点坏了

事情是这样的。

我们开发的AIUI-《健康操》应用首页并排摆着两张入口卡片,一张"颈椎训练",一张"视力舒缓"。当前焦点在哪张上,哪张就亮一点,另一张暗下去;触控板上下滑,亮度跟着焦点走。这是最基础的效果,也是用户最直接的感受。

开发过程中测试发现,在PC模拟机和真机上:"按钮焦点亮度不替换。"意思是------不管怎么滑,上面那张永远亮着,下面那张一直暗着。

我第一反应是自己的锅。把页面文件翻出来一看,git diff 是空的,这个文件从仓库建好那天起就没动过。又怕真机跑的是旧代码,把设备上那个 HeathyCare.aix 解包,取出里面的 pages/index/index.ink,跟本地这份比了一遍:字节数一样,内容也一样,11813 字节,分毫不差。

日志也干净。按键事件的日志、焦点切换的日志,一条不少,全在正常打印。

这就有点邪门了------代码没动,日志不报错,偏偏PC模拟机和真机上效果不对。

后来查明白了:问题不在我们写的代码里,在眼镜上那个负责画界面的运行时。它在某个版本升级之后,把一条本来好好的渲染指令给丢了。


二、先搞懂:焦点是怎么"亮"起来的

想查一个问题,得先知道这条链路上有哪几环。焦点切换这件事,说白了就三步:

text 复制代码
数据变了  →  setData 通知一声  →  运行时按样式把画面重画一遍

拆开说:

运行时。 AIUI 里画界面这件事,由 @yodaos-pkg/ink 这个包来做。它把我们写的 .ink 页面(模板、样式、数据)画成屏幕上的画面。要注意的是,它不是我们写的 JS,而是一份提前编译好的引擎------所以它换个版本,行为就可能跟着变。这次的问题就出在这儿。

data / setData。 data 是页面自己的状态表;setData 相当于按一下刷新键,把改过的字段递给运行时,让它重画。有一点要记牢:日志里看到 setData 被调用了,不代表画面一定按你想的那么画。这两者之间,隔着运行时。

样式。 决定每个元素长什么样。改明暗,本质就是改样式。

再往下就是我这次栽跟头的地方------改样式其实有两条完全不同的路,很多人(包括我)平时不太区分:

一条是 class 。元素上写 class="card card-focused",样式表里提前给 .card-focused 写好一套样式;切换焦点,就是给一个元素加上这个类、把另一个元素身上的这个类去掉。这是"加标签、撕标签"。

另一条是 inline style 。直接写 style="opacity: 0.5",相当于在元素上贴张便签写个数;要改,就改便签上的数字。这是"改数值"。

这两条路在引擎内部是分开走的。

我们原来的写法,是把焦点的表达全压在 class 这条路上:.card-focused 一加,边框、标题、图标的透明度就从 0.4 跳到 0.8,亮一倍;一去掉,回到 0.4。切换焦点就是给 A 去类、给 B 加类,亮度自然互换。逻辑上一点毛病没有,问题在于------它依赖的那条路断了。

顺便补一句:那两张图标是纯色的 #40ff5e,明暗完全不看图标内容,全靠这层 CSS 透明度撑着。记住"亮是暗的两倍"这个数就行,后面用像素去量的时候,靠的就是它。


三、动手前,先分清是哪一种"不切换"

"焦点不对"这四个字太笼统了。实际会遇到的情况有好几种,处理方式完全不同,先花一分钟归类,能省掉大把无用功。

一进页面就不对:该亮的不亮,或者亮反了。这基本是自己代码的问题,初始 data 写错了,跟引擎没什么关系。

进来是对的、一切换就出问题:比如两张都变亮、焦点转不过去。这次我们碰到的就是这个。

按了没反应:亮度看着正常,但滑了没动静,或者单击确认没反应。这多半是按键事件根本没进来,跟样式没关系。

PC 模拟机和真机上不对:同一份代码,换个引擎版本现象就变了。这是环境或版本差异。

怎么快速分辨后两种?看画面表现------如果是"该暗的暗不下来""亮加上去了去不掉",那是第二种;再拿同一份代码换个环境试------如果换个引擎版本现象就变,那是第四种。这两类经常凑在一起出现。像我们这次,表面上是第二种(加成功、去失败),根子上是第四种(引擎版本换掉之后才这样)。


四、我实际是怎么查出来的

把我当时的顺序捋一遍,基本就是这么走下来的。

先做的事,是往代码里埋日志。把一次焦点切换拆成几段,每段打一条带统一前缀的日志:初始化时的焦点状态、按下的键、焦点索引的前后值、这次 setData 改了什么。模板我放在附录 B,可以直接抄。

埋好之后点一下 ArrowDown,控制台打出来两行:

text 复制代码
[健康操-首页] 按键事件: code=ArrowDown
[健康操-首页] 焦点下移到卡片1

这两行说明事件层是通的:onKeyUp 执行了,setData 也调了,焦点索引也更新了。那问题只能出在后半段------运行时把这些变更画到屏幕上的时候。这一步的价值就在这儿,它把"事件层"和"渲染层"一刀切开,方向一下就明朗了。

接下来是自证清白。既然怀疑渲染,那先得排除自己的代码。git diff 是空的,之前已经看过;真机包也解包比过了,逐字节一致。源码这条线,可以放下了。

然后是排除"老版本不支持这个能力"。我去翻了官方的 wxss 文档,opacity 和 rgba() 本来就是支持的一等能力,官方 samples 里就有专门讲 opacity、讲 colors 的例子。所以"透明度是新版本才支持的"这个方向也站不住。

剩下就只剩一条路了:换引擎版本试。这一步靠 aix preview。它妙就妙在用真的 ink 引擎,把 app 直接画到 canvas 上,跟真机是同一条渲染路径。我用 esm.sh 把引擎按版本 pin 住------https://esm.sh/@yodaos-pkg/ink@版本号 这样写------给每个待测版本各生成一份预览页,本地起个静态服务,用 Playwright 打开,点预览面板上的 ArrowDown,再从 canvas 上采像素。

肉眼说"好像都亮"是不算数的,得量出来。用 canvas 的 getImageData 读指定区域,取绿色通道的最大值:卡片亮的时候透明度是 0.8,峰值绿是 204;暗的时候 0.4,峰值 102。另外还有个 153,那是副标题和底部文案的 0.6,别拿它当依据。采样的时候按"卡片带"取峰值,不要按屏幕半区取------半区会被那堆 0.6 的文字污染。

以我们这张 480×352 的画布为例,采样带是这么切的:

y 区间 峰值绿 这里有什么
24--96 204 标题"健康操"和颈卡标题
96--144 153 副标题"职场健康伴侣"
140--290 204 颈卡
288--312 0 卡片之间的空隙
310--460 102 眼卡
504--624 153 底部文案

先看初始状态,六个版本全试一遍:0.1.0、0.12.3、0.13.0、0.14.0、0.16.1、0.17.1,初始全是"颈卡 204、眼卡 102"。这说明初始这个亮度差根本不是 bug,它就是焦点高亮本来该有的样子。这一步排掉的是一个更大的猜测------透明度这个功能整体没坏。

再看切换。点一下 ArrowDown 重新量:

引擎版本 初始(颈/眼) 点 ArrowDown 后(颈/眼) 焦点有没有互换
0.12.3 204 / 102 102 / 204 互换了,正常
0.13.0 204 / 102 204 / 204 两张都亮,坏了
0.14.0 204 / 102 204 / 204 坏了
0.16.1 204 / 102 204 / 204 坏了
0.17.1 204 / 102 204 / 204 坏了

结论就很清楚了:引擎从 0.13.0 往后,给眼卡"加上" card-focused 是生效的(102 变成 204),但把颈卡上的 card-focused"去掉"不生效(还是 204)。所以两张卡都停在亮态。最后一次正常是 0.12.3,第一次出问题是 0.13.0。

最后再最小验证一下。我写了个极简页面,左边一个元素用 class 切、右边一个元素用 style 数值切,分别试。结果还是那句老话:class 加得上去、去不掉;style 数值替换一切正常。到这儿,根因就钉死了。


五、根因,以及怎么修

根因。 @yodaos-pkg/ink 从 v0.13.0 开始有个渲染回退:用 setData 改 class 字符串的时候,往视图上"加"class 能正常重画,从视图上"移除"class 样式就不再重算。而焦点高亮恰恰靠"一张加、一张去",这个回退就把焦点转移整个废掉了。

引擎是提前编译好的 WASM,源码看不到,没法直接从代码里取证。但"同一份 app、同一套预览、只换引擎版本"就能复现差异,这就足够定性了。

先说一个我试过、但没用的修法。 我当时想,那把 class 字符串换成独立布尔加三元拼接,不就行了?

html 复制代码
<view class="card card-neck{{neckFocused ? ' card-focused' : ''}}" ...>

试了,还是颈卡 204、眼卡 204。没用。因为不管写成字符串互换还是三元拼接,切的还是 class,走的还是"加/去"这条路。

真正管用的修法,是干脆不走 class 了:改用 inline style 的数字表达明暗。子元素统一固定成亮态(0.8),卡片根节点只留一个 opacity 数字随焦点变------聚焦 1.0,未聚焦 0.5。style 的值是"数值替换",走的是另一条渲染路径,不受 class 删除那个回退影响。

模板:

html 复制代码
<view class="card card-neck" style="opacity: {{neckFocused ? 1 : 0.5}}" bindtap="goNeckTraining">
  ...
</view>
<view class="card card-eye" style="opacity: {{eyeFocused ? 1 : 0.5}}" bindtap="goEyeTraining">
  ...
</view>

JS 里,真正变动的就一句 setData:

javascript 复制代码
data: { neckFocused: true, eyeFocused: false },

onShow() {
  // 每次页面显示都把焦点重置回第一张,免得返回后焦点错乱
  this._focusedIndex = 0;
  this.setData({ neckFocused: true, eyeFocused: false });
},

onKeyUp(event) {
  var code = event.code || '';
  switch (code) {
    case 'ArrowUp':
      if (this._focusedIndex > 0) {
        var upIdx = this._focusedIndex - 1;
        this._focusedIndex = upIdx;
        this.setData({ neckFocused: upIdx === 0, eyeFocused: upIdx === 1 });
      }
      break;
    case 'ArrowDown':
      if (this._focusedIndex < CARD_COUNT - 1) {
        var downIdx = this._focusedIndex + 1;
        this._focusedIndex = downIdx;
        this.setData({ neckFocused: downIdx === 0, eyeFocused: downIdx === 1 });
      }
      break;
    case 'Enter':
      // 确认键照旧按 _focusedIndex 走
      if (this._focusedIndex === 0) { this.goNeckTraining(); }
      else { this.goEyeTraining(); }
      break;
  }
}

CSS 那边,就是把子元素全固定成亮态,把原来所有 .card-focused 的规则删掉:

css 复制代码
.card       { border: 2px solid rgba(64, 255, 94, 0.8); }
.icon-svg   { opacity: 0.8; }
.card-title { color: rgba(64, 255, 94, 0.8); }
.card-desc  { color: rgba(64, 255, 94, 0.8); }
/* :active 的单击瞬态保留,不要动 */

为什么效果一样?聚焦时是 1.0 × 0.8 = 0.8,未聚焦时是 0.5 × 0.8 = 0.4,原来那个"亮是暗的两倍"一点没变。

改完在 0.17.1 上验了一遍:初始颈卡 204、眼卡 102;点 ArrowDown 变成颈卡 102、眼卡 204;点 ArrowUp 又回到原样;控制台零报错。

如果暂时不想动代码,还有个备选:把引擎版本锁在 0.13.0 以前,比如用 aix runtime select 固定到 0.12.3,也能躲开这个坑。我们最后选了改写法,因为不想被版本绑住。

顺带说两个也常被误当成"焦点问题"的原因。

一个是按键事件写错了地方。AIUI 的按键分 onKeyDown 和 onKeyUp 两个回调,单击确认(Enter)只在 onKeyUp 里触发,这是框架定死的,跟浏览器的习惯不一样。你要是全写在 onKeyDown 里,就会"滑动能用、单击没反应"。另外取按键码要用 event.code,event.key 和 event.keyCode 在 AIUI 里都拿不到值。

另一个是改了没部署。AIUI 是"本地改 → 上传开发者工作室 → 设备拉取",本地改完设备不会自动跟着变,得重新打包上传、设备端再拉一次。好多人改完发现真机没变化,就以为修了个寂寞,其实是旧包还在跑。


六、以后怎么避免再踩

写代码的时候,焦点、选中这类状态,我现在的习惯是优先用 inline style 的数字去表达,别把它全拴在 class 增删这一条路上------多留一条路,真出问题的时候才有退路。另外 data 里只放真正要在模板里绑定的字段,像焦点索引这种纯内部状态,用 this._xxx 私有属性就行,不然运行时会嫌你没绑定、一直报警告。按键统一放 onKeyUp、统一用 event.code,这些都是顺手的事。

测试的时候,凡是动过焦点或者渲染的改动,别只看 PC 预览,一定上真机过一遍:初始对不对、切换能不能互换、返回后焦点有没有错乱。还有个好习惯是把手上那台设备的引擎版本号记下来,升级之前把焦点相关的用例跑一遍------我们这次就是靠"同一份包换引擎版本对比"才揪出来的,平时顺手做一次,成本很低。

排查的时候,日志按"初始化 / 按键 / 状态变更 / 数据下发 / 渲染校验"分段写,前缀统一。详细日志加个开关(平时关着,要用再开),报错的日志别关。一旦看到"数据都下发对了、画面却不动",就马上把注意力从"按键处理"转到"渲染引擎"上,别在事件那层瞎转。

最后,把踩过的坑记下来。我们项目之前吃过一次亏:新人上来没看经验文档就动手改,结果跟之前的修复撞了车,白折腾一轮。文档这东西平时看着没用,真出事的时候能省一整天。

**最终改善效果:


七、最后

这次"灯不灭",回头看其实就是三句老话:把链路拆开一段一段查,别整体瞎猜;让日志和数据说话,别凭感觉;改动以真机为准,PC 预览不算数。

最坑的是这回的情况------代码没动、日志没报错,画面就是不对。根子藏在你平时看不见的运行时里,它不崩、不报错,只是安安静静地画错。这种问题,光盯着自己的代码是想不出来的。

要是你现在正被焦点问题缠着,可以照这个路子来一遍:先归类是哪一种不切换;再翻日志,把事件层和渲染层分开;然后对一下源码和真机包是不是一回事,有条件就换个引擎版本比一比;最后写个最小页面复现、再上真机确认。多数问题其实走不到第五步,前三步就差不多了。


附录 A:排查速查表

你看到的 先查什么 多半是 怎么弄
一进页面该亮的没亮 / 亮反了 初始 data 的值 初始态写错了 改初始 data
进来正常,一切换两张都亮 / 该暗的暗不下来 切换走的是 class 还是 style 引擎"移除 class"回退 改用 inline style 数值
滑了没反应 / 单击确认没反应 日志里 [焦点-2.按键] 有没有打印 按键事件没进来 处理挪到 onKeyUp,用 event.code
PC 正常、真机不对 本地包和真机包是不是一回事 版本差异 / 没部署 逐字节比包、重新部署、换版本对比
换个引擎版本现象就变 先把版本号记下来 引擎版本回退 改写法绕开,或锁到 0.13.0 以前
改用 class 三元绑定还是不行 切换是不是还在切 class 修错了方向(伪修复) 改走 style 数值这条路

附录 B:焦点链路日志模板

javascript 复制代码
// 1) 初始化:页面打开/显示时,先把焦点状态记下来
console.log(LOG_TAG + ' [焦点-1.初始化] 焦点=卡片' + this._focusedIndex
  + ' 颈亮=' + this.data.neckFocused + ' 眼亮=' + this.data.eyeFocused);

// 2) 按键:每次按键先记 event.code ------ 这条是"事件层"的哨兵
var code = event.code || '';
console.log(LOG_TAG + ' [焦点-2.按键] code=' + code);

// 3) 状态变更:以 ArrowDown 为例,记下切换前后的索引
var newIdx = this._focusedIndex + 1;
console.log(LOG_TAG + ' [焦点-3.状态变更] 焦点 ' + this._focusedIndex + ' -> ' + newIdx);
this._focusedIndex = newIdx;

// 4) 数据下发:记下这次到底 setData 了什么
this.setData({ neckFocused: newIdx === 0, eyeFocused: newIdx === 1 });
console.log(LOG_TAG + ' [焦点-4.数据下发] neckFocused=' + (newIdx === 0)
  + ' eyeFocused=' + (newIdx === 1));

// 5) 渲染校验:运行时不提供"样式有没有重画"的日志,
//    只能靠肉眼或画布采样确认:是不是一明一暗、并且随焦点互换
//    采样判据:绿通道峰值 204=亮(0.8),102=暗(0.4),153=干扰带(0.6)

怎么读这些日志,三句话:

  • [焦点-2] 一条都没有,事件层没通,去看按键处理。
  • [焦点-2] 有、[焦点-3]/[焦点-4] 也有,画面却不动,渲染层有问题,重点怀疑 class 增删。
  • 三段都有、画面也变了,那就是好了。

附录 C:像素采样判别法

用真 ink 引擎在浏览器里渲染,再从画布上采像素,就能不靠肉眼判断焦点有没有正确互换:

text 复制代码
1. 给每个待测版本生成一份预览 HTML,inkSdkUrl 指向 esm.sh 对应版本
   https://esm.sh/@yodaos-pkg/ink@<版本号>
2. 本地起个静态服务,用 Playwright 打开
3. 点预览面板上的 ArrowDown
4. 用 canvas 的 getImageData 采样:
   颈卡带 x=中线 y∈[140,290]
   眼卡带 x=中线 y∈[310,460]
5. 取每条带的"峰值绿":
   正确互换 → 颈 102、眼 204
   异常     → 颈 204、眼 204

两个要点,别踩:

  • 按"卡片带"取峰值,别按屏幕半区取。半区会被副标题、底部文案那堆 rgba(,0.6)(绿 153)污染,量出来不准。
  • 这几个数记熟:204 是亮(0.8),102 是暗(0.4),153 是干扰带(0.6)。
相关推荐
派大鑫wink1 小时前
纯前端实现软考中级·软件设计师备考学习网站
前端·学习
kyriewen1 小时前
我扒了 npm 三年数据:AI 没把依赖变胖,是把包变多了
前端·人工智能·程序员
liqingdi4372 小时前
[SWPUCTF 2023 秋季新生赛]Easy SMC
服务器·前端·javascript
用户87359865635872 小时前
为什么 greedy 也可能出不同 token,MoE 为什么更严重
前端
Gizzap_Tech2 小时前
多语言官网如何兼顾统一与本地化?从页面生成到语言切换
前端
IT_陈寒2 小时前
Java缓存踩坑记:明明设置了过期时间,怎么还是读到脏数据?
前端·人工智能·后端
靠coding维持生计的potato2 小时前
使用 unified diff 为 pnpm 依赖编写可复现补丁
前端
Initialize-le2 小时前
systemd 服务起不来?五步排查法
前端·chrome
OpenTiny社区2 小时前
多会话 + 流式输出怎么落地?TinyRobot Kit 对话数据层深度解析
前端·vue.js