news 2026/10/8 8:01:50

HarmonyOS 7 ArkUI:折叠屏鼠标悬停态与键盘焦点交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS 7 ArkUI:折叠屏鼠标悬停态与键盘焦点交互

ReviewBoard 在折叠屏连接鼠标后看起来很顺手:指针移到任务卡上,编辑、归档、更多操作会自然浮现。可一旦拔掉鼠标改用触控,这三个按钮就像从页面上消失了;接入键盘后,焦点虽然能移动,视觉高亮又和悬停态打架。这个问题不是加一个onHover就结束,而是要让鼠标、键盘和触控共享同一套操作语义。

一、鼠标用户看得到,手指用户找不到

ReviewBoard 是一张审核任务看板。展开态窗口宽度为912 vp,任务卡RV-1027右上角有编辑、归档和更多三个轻量操作。设计稿为了减少干扰,默认隐藏按钮,只有鼠标悬停时显示。大屏演示很漂亮,真机测试却马上暴露问题:半折叠后用户改用触摸,没有 Hover,操作入口一直隐藏;用 Tab 键进入卡片时,焦点落在卡片上,但按钮仍不可见。

第一次修复很直接:触摸按下时也把hovered=true。结果手指离开后悬停态没有可靠的退出时机,卡片长期保持高亮;滚动列表时,经过的卡片还会一个个“点亮”。这让我意识到,Hover 只是输入信号,不应该直接充当业务状态。

最终我把交互拆成四个瞬时事实:hovered、focused、pressed、menuOpened,再由它们推导actionsVisible。触控模式还提供常驻“更多”按钮,不依赖任何悬停动作。本文固定的验收记录为PARITY-1002,页面ReviewBoard,任务RV-1027,覆盖POINTER、KEYBOARD、TOUCH三种模式,共 18 个用例,最终不可达操作为 0,状态为PARITY_PASSED。

二、输入来源和页面形态是两条轴

窗口变宽不等于一定连接鼠标,连接鼠标也不等于用户不会触摸。旧代码用width >= 840推断“桌面模式”,把输入能力和布局尺寸绑在一起;折叠屏恰恰会打破这个假设。

当前问题是分别记录布局和最近有效输入,再计算展示策略。下面的状态模型不把 Hover 写进业务对象,也不在build()中修改状态。

exporttypeLayoutMode='COMPACT'|'EXPANDED'exporttypeInputMode='POINTER'|'KEYBOARD'|'TOUCH'@ObservedV2exportclassInteractionContext{@Tracelayout:LayoutMode='COMPACT'@Traceinput:InputMode='TOUCH'@TracewidthVp:number=0updateWidth(widthVp:number):void{this.widthVp=widthVpconstnext:LayoutMode=widthVp>=840?'EXPANDED':'COMPACT'if(next!==this.layout)this.layout=next}markInput(input:InputMode):void{this.input=input}shouldKeepMoreVisible():boolean{returnthis.input==='TOUCH'||this.layout==='COMPACT'}}

updateWidth()只负责布局,markInput()只记录最近一次明确输入。展开到912 vp后,页面可以使用横向任务卡,但如果最近输入仍是触控,“更多”入口照样常驻。鼠标移动或键盘按键事件到来时,输入模式再切换,不需要重建页面。

正式项目可通过当前 SDK 支持的输入事件来源进一步区分鼠标、触控板和触摸屏;Demo 为了可复测,用统一的页面事件适配器上报。窗口监听和输入监听应在页面出现时注册、退场时注销,避免旧页面继续改变全局模式。宽度阈值还要加迟滞,防止自由窗口停在 839/840 vp 附近反复重排。

三、操作可见性必须是推导值

每张任务卡最初维护一个showActions,Hover、Focus、点击菜单都可以改它。多个事件交错后,谁最后写值谁获胜,导致菜单已经打开,鼠标移出却把按钮和菜单锚点一起隐藏。

当前问题是把事实状态保留下来,由一个只读规则计算最终可见性。这样每个事件只修改自己负责的字段。

@ObservedV2exportclassCardInteractionState{@Tracehovered:boolean=false@Tracefocused:boolean=false@Tracepressed:boolean=false@TracemenuOpened:boolean=falseactionsVisible(context:InteractionContext):boolean{returnthis.hovered||this.focused||this.menuOpened||context.shouldKeepMoreVisible()}visualRaised():boolean{returnthis.hovered||this.focused||this.pressed}resetTransient():void{this.hovered=falsethis.focused=falsethis.pressed=false}}

当鼠标移出时只把hovered设为 false;如果菜单仍然打开,actionsVisible()依旧为 true。键盘焦点进入时focused=true,按钮立即出现;触控模式即使四个事实都是 false,shouldKeepMoreVisible()仍保证一个可达入口。

状态跟随卡片组件实例,列表复用时要按稳定任务 ID 重置,不能把上一张卡的 Hover 带到下一张。页面形态从展开变紧凑时调用resetTransient(),防止已经不存在的指针位置继续抬高卡片;menuOpened不在其中,因为菜单是否关闭应由弹层生命周期决定,而不是布局变化偷偷决定。

四、同一个动作必须有三条等价路径

“按钮出现了”并不代表等价交互已经完成。鼠标可以点击图标,键盘用户需要 Enter/Space,触控用户需要足够大的命中区。更关键的是,三个入口必须调用同一个业务命令,否则埋点、确认弹窗和权限判断会分叉。

当前问题是把动作收敛到executeAction(),输入事件只负责把意图翻译为命令。

exporttypeReviewAction='EDIT'|'ARCHIVE'|'MORE'exportclassReviewActionCoordinator{privaterunning:Set<string>=newSet()asyncexecuteAction(taskId:string,action:ReviewAction):Promise<void>{constkey=`${taskId}:${action}`if(this.running.has(key))returnthis.running.add(key)try{if(action==='ARCHIVE'){awaitthis.confirmAndArchive(taskId)}elseif(action==='EDIT'){RouterService.openEditor(taskId)}else{OverlayService.openMore(taskId)}}finally{this.running.delete(key)}}}

鼠标点击、键盘激活和触控点击都调用同一个方法。running以任务和动作组合做门闩,避免鼠标双击或键盘长按重复归档。动作完成后必须在finally中释放,否则异常会让入口永久失效。

Demo 的归档确认使用稳定的任务 ID,不读取列表当前索引。正式项目还要在服务端做幂等,页面门闩只能挡住同一实例内的重复操作。若菜单跨窗口显示,OverlayService应跟随当前 UIContext 创建并在页面离开时关闭,不能作为全局单例长期持有窗口上下文。

五、ArkUI 卡片把 Hover、Focus 和触控分开接线

状态模型和协调器准备好后,组件只负责事件绑定。视觉上,Hover 与 Focus 都抬高卡片,但焦点需要清晰描边;触控按下只提供短暂反馈,不让悬停态残留。

当前问题是让不同事件修改各自事实,并始终保留可访问名称。

@ComponentV2struct ReviewCard{@ParamtaskId:string=''@Paramtitle:string=''@Paramcontext:InteractionContext=newInteractionContext()@Localstate:CardInteractionState=newCardInteractionState()build(){Stack({alignContent:Alignment.TopEnd}){this.CardBody()if(this.state.actionsVisible(this.context)){this.ActionBar()}}.focusable(true).onHover((value:boolean)=>{this.context.markInput('POINTER')this.state.hovered=value}).onFocus(()=>{this.context.markInput('KEYBOARD')this.state.focused=true}).onBlur(()=>{this.state.focused=false}).gesture(TapGesture().onAction(()=>{this.context.markInput('TOUCH')})).accessibilityText(`${this.title},审核任务${this.taskId}`)}}

onHover不再直接显示按钮,它只更新hovered;可见性由规则推导。焦点进入后输入模式切为 KEYBOARD,退出只清理卡片焦点。Tap 只标记触控来源,不冒充 Hover。操作按钮本身还要分别设置可读语义和合理命中尺寸,不能只依靠整卡 accessibilityText。

页面销毁时组件状态自然回收,但若外部 Input Kit 监听注册在 ViewModel 中,仍需显式取消。重复创建监听会让一次移动触发多次模式切换。不同 API 版本的输入来源字段可能变化,应通过适配层封装,不要把系统事件类型散落在每张卡片里。

六、调试不看动画,看可达性矩阵

工程目录分为pages/ReviewBoardPage.ets、components/ReviewCard.ets、model/InteractionContext.ets、model/CardInteractionState.ets、service/ReviewActionCoordinator.ets和utils/InputModeAdapter.ets。我没有把所有逻辑写进卡片,因为输入模式属于页面上下文,动作幂等属于业务协调器。

HiLog 固定输出:PARITY-1002 width=912vp layout=EXPANDED、Input POINTER -> KEYBOARD -> TOUCH、RV-1027 actionsVisible=true、Cases=18 unreachable=0 state=PARITY_PASSED。这几行比“Hover 动画正常”更接近验收目标。

DevEco Studio 图里,右侧模拟器显示RV-1027的操作条,中间代码停在actionsVisible(),底部日志显示三种输入模式都通过。红色标注只指向常驻“更多”入口和unreachable=0。

七、折回触控后,入口仍然在

最终手机图模拟窗口从912 vp折回684 vp,布局变为COMPACT,最近输入为TOUCH。Hover 与 Focus 都已经清零,但任务卡右上角仍有“更多”,状态卡显示PARITY_PASSED,18 个用例全部完成。

这次修复没有让触控界面永久铺开三个按钮,而是保留一个清晰入口;键盘用户获得焦点描边,鼠标用户继续享受悬停快捷操作。三条路径视觉可以不同,业务动作和可达性不能不同。

八、结语

多形态适配不只是窗口宽度变化,也包括输入方式随时变化。把 Hover 当成装饰没问题,把它当成唯一入口就会制造功能缺口。ReviewBoard 最终留下的规则很简单:布局和输入分轴记录,瞬时事实不互相覆盖,操作命令只保留一个执行入口。

实际项目还要把屏幕阅读、快捷键提示和触控命中区纳入同一套验收矩阵。只有鼠标、键盘、手指都能完成同一件事,折叠屏上的“大屏体验”才不是只对演示设备成立。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 8:01:01

Superpowers技能库详解:让Claude Code按工程方法论执行开发

先交代一下背景。最近在折腾 AI 编程助手的过程中&#xff0c;碰到一个叫Superpowers的开源技能库&#xff0c;热度挺高&#xff0c;GitHub 上 star 涨得很快&#xff0c;中文社区里问得最多的几个问题就是&#xff1a;它到底是什么、有哪些 skills、怎么引入这些技能、安装之后…

作者头像 李华
网站建设 2026/10/8 8:00:21

无人机集群分布式估计:事件触发与量化通信的EKF仿真对比

1. 为什么拿集中式EKF当标尺&#xff1a;分布式估计的通信代价从哪来无人机集群协同最核心的瓶颈其实不是算力&#xff0c;而是通信。你想想看&#xff0c;每架无人机都在用机载传感器&#xff08;惯导、GPS、视觉、雷达&#xff09;感知周围环境&#xff0c;但这些传感器数据如…

作者头像 李华
网站建设 2026/10/8 8:00:08

Linux SNMP监控与snmp++精简实践:从协议原理到采集器开发

简介&#xff1a;面向Linux网络管理与嵌入式开发者的SNMP精简实现源码包&#xff0c;主要解决资源受限环境下快速部署、学习或二次开发SNMP协议栈的迫切需求。压缩包共6个C语言源码文件&#xff0c;总大小仅39KB&#xff0c;代码结构紧凑&#xff0c;涵盖ASN.1编解码、MIB信息结…

作者头像 李华
网站建设 2026/10/8 7:57:37

开源工具Ponytail:分块检索技术扩展大模型上下文窗口

分享一个我最近在长文本生成项目里反复用到的工具&#xff1a;Ponytail。如果你平时写小说、做剧本、生成深度长文&#xff0c;或者搞AI辅助创作&#xff0c;一定遇到过这种尴尬——模型上下文窗口不够用&#xff0c;生成到一半忘了前文设定&#xff0c;角色性格漂移&#xff0…

作者头像 李华