ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

HarmonyOS 7 ArkUI:折叠屏鼠标悬停态与键盘焦点交互 ReviewBoard 在折叠屏连接鼠标后看起来很顺手指针移到任务卡上编辑、归档、更多操作会自然浮现。可一旦拔掉鼠标改用触控这三个按钮就像从页面上消失了接入键盘后焦点虽然能移动视觉高亮又和悬停态打架。这个问题不是加一个onHover就结束而是要让鼠标、键盘和触控共享同一套操作语义。一、鼠标用户看得到手指用户找不到ReviewBoard 是一张审核任务看板。展开态窗口宽度为912 vp任务卡RV-1027右上角有编辑、归档和更多三个轻量操作。设计稿为了减少干扰默认隐藏按钮只有鼠标悬停时显示。大屏演示很漂亮真机测试却马上暴露问题半折叠后用户改用触摸没有 Hover操作入口一直隐藏用 Tab 键进入卡片时焦点落在卡片上但按钮仍不可见。第一次修复很直接触摸按下时也把hoveredtrue。结果手指离开后悬停态没有可靠的退出时机卡片长期保持高亮滚动列表时经过的卡片还会一个个“点亮”。这让我意识到Hover 只是输入信号不应该直接充当业务状态。最终我把交互拆成四个瞬时事实hovered、focused、pressed、menuOpened再由它们推导actionsVisible。触控模式还提供常驻“更多”按钮不依赖任何悬停动作。本文固定的验收记录为PARITY-1002页面ReviewBoard任务RV-1027覆盖POINTER、KEYBOARD、TOUCH三种模式共 18 个用例最终不可达操作为 0状态为PARITY_PASSED。二、输入来源和页面形态是两条轴窗口变宽不等于一定连接鼠标连接鼠标也不等于用户不会触摸。旧代码用width 840推断“桌面模式”把输入能力和布局尺寸绑在一起折叠屏恰恰会打破这个假设。当前问题是分别记录布局和最近有效输入再计算展示策略。下面的状态模型不把 Hover 写进业务对象也不在build()中修改状态。exporttypeLayoutModeCOMPACT|EXPANDEDexporttypeInputModePOINTER|KEYBOARD|TOUCHObservedV2exportclassInteractionContext{Tracelayout:LayoutModeCOMPACTTraceinput:InputModeTOUCHTracewidthVp:number0updateWidth(widthVp:number):void{this.widthVpwidthVpconstnext:LayoutModewidthVp840?EXPANDED:COMPACTif(next!this.layout)this.layoutnext}markInput(input:InputMode):void{this.inputinput}shouldKeepMoreVisible():boolean{returnthis.inputTOUCH||this.layoutCOMPACT}}updateWidth()只负责布局markInput()只记录最近一次明确输入。展开到912 vp后页面可以使用横向任务卡但如果最近输入仍是触控“更多”入口照样常驻。鼠标移动或键盘按键事件到来时输入模式再切换不需要重建页面。正式项目可通过当前 SDK 支持的输入事件来源进一步区分鼠标、触控板和触摸屏Demo 为了可复测用统一的页面事件适配器上报。窗口监听和输入监听应在页面出现时注册、退场时注销避免旧页面继续改变全局模式。宽度阈值还要加迟滞防止自由窗口停在 839/840 vp 附近反复重排。三、操作可见性必须是推导值每张任务卡最初维护一个showActionsHover、Focus、点击菜单都可以改它。多个事件交错后谁最后写值谁获胜导致菜单已经打开鼠标移出却把按钮和菜单锚点一起隐藏。当前问题是把事实状态保留下来由一个只读规则计算最终可见性。这样每个事件只修改自己负责的字段。ObservedV2exportclassCardInteractionState{Tracehovered:booleanfalseTracefocused:booleanfalseTracepressed:booleanfalseTracemenuOpened:booleanfalseactionsVisible(context:InteractionContext):boolean{returnthis.hovered||this.focused||this.menuOpened||context.shouldKeepMoreVisible()}visualRaised():boolean{returnthis.hovered||this.focused||this.pressed}resetTransient():void{this.hoveredfalsethis.focusedfalsethis.pressedfalse}}当鼠标移出时只把hovered设为 false如果菜单仍然打开actionsVisible()依旧为 true。键盘焦点进入时focusedtrue按钮立即出现触控模式即使四个事实都是 falseshouldKeepMoreVisible()仍保证一个可达入口。状态跟随卡片组件实例列表复用时要按稳定任务 ID 重置不能把上一张卡的 Hover 带到下一张。页面形态从展开变紧凑时调用resetTransient()防止已经不存在的指针位置继续抬高卡片menuOpened不在其中因为菜单是否关闭应由弹层生命周期决定而不是布局变化偷偷决定。四、同一个动作必须有三条等价路径“按钮出现了”并不代表等价交互已经完成。鼠标可以点击图标键盘用户需要 Enter/Space触控用户需要足够大的命中区。更关键的是三个入口必须调用同一个业务命令否则埋点、确认弹窗和权限判断会分叉。当前问题是把动作收敛到executeAction()输入事件只负责把意图翻译为命令。exporttypeReviewActionEDIT|ARCHIVE|MOREexportclassReviewActionCoordinator{privaterunning:SetstringnewSet()asyncexecuteAction(taskId:string,action:ReviewAction):Promisevoid{constkey${taskId}:${action}if(this.running.has(key))returnthis.running.add(key)try{if(actionARCHIVE){awaitthis.confirmAndArchive(taskId)}elseif(actionEDIT){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:stringParamtitle:stringParamcontext:InteractionContextnewInteractionContext()Localstate:CardInteractionStatenewCardInteractionState()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.hoveredvalue}).onFocus((){this.context.markInput(KEYBOARD)this.state.focusedtrue}).onBlur((){this.state.focusedfalse}).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 width912vp layoutEXPANDED、Input POINTER - KEYBOARD - TOUCH、RV-1027 actionsVisibletrue、Cases18 unreachable0 statePARITY_PASSED。这几行比“Hover 动画正常”更接近验收目标。DevEco Studio 图里右侧模拟器显示RV-1027的操作条中间代码停在actionsVisible()底部日志显示三种输入模式都通过。红色标注只指向常驻“更多”入口和unreachable0。七、折回触控后入口仍然在最终手机图模拟窗口从912 vp折回684 vp布局变为COMPACT最近输入为TOUCH。Hover 与 Focus 都已经清零但任务卡右上角仍有“更多”状态卡显示PARITY_PASSED18 个用例全部完成。这次修复没有让触控界面永久铺开三个按钮而是保留一个清晰入口键盘用户获得焦点描边鼠标用户继续享受悬停快捷操作。三条路径视觉可以不同业务动作和可达性不能不同。八、结语多形态适配不只是窗口宽度变化也包括输入方式随时变化。把 Hover 当成装饰没问题把它当成唯一入口就会制造功能缺口。ReviewBoard 最终留下的规则很简单布局和输入分轴记录瞬时事实不互相覆盖操作命令只保留一个执行入口。实际项目还要把屏幕阅读、快捷键提示和触控命中区纳入同一套验收矩阵。只有鼠标、键盘、手指都能完成同一件事折叠屏上的“大屏体验”才不是只对演示设备成立。
返回列表