HarmonyOS 大屏键鼠适配实战:从焦点移动到快捷键、多窗口和体验验收
大屏适配不是把手机页面放大。真正的问题出现在用户开始用键盘、鼠标、触控板和多窗口时:方向键不知道焦点去哪了,Tab 顺序混乱,快捷键和系统冲突,窗口变窄后布局挤压,鼠标悬停没有反馈,弹窗打开后焦点还停在背景按钮上。
这篇文章只解决一个工程问题:HarmonyOS 应用如何把大屏键鼠体验做成一套可维护的焦点、快捷键和多窗口适配方案。

本文会落到四个结果:
一、先定边界:大屏适配重点是输入方式变化
手机页面主要依赖触控,而平板、电脑、大屏设备会同时出现键盘、鼠标、触控板和触摸。输入方式变多后,页面必须能回答:
| 当前焦点在哪里 | 用户不知道键盘会操作哪个控件 |
| Tab 下一步去哪 | 表单、列表、弹窗顺序混乱 |
| 快捷键是否冲突 | 输入框里按键触发页面动作 |
| 鼠标悬停是否有反馈 | 大屏上缺少可点击感 |
| 窗口变窄怎么办 | 布局挤压、按钮跑出屏幕 |
大屏体验不是“页面更大”,而是“用户不用手指也能完成任务”。
二、资料与版本边界:本文围绕 ArkUI 焦点、按键和窗口尺寸
本文示例面向 HarmonyOS NEXT / ArkUI / ArkTS 工程,重点放在焦点控制、按键事件、快捷键注册、响应式布局和多窗口尺寸变化。具体组件属性和窗口能力以当前 SDK 文档为准。

| 焦点层 | Tab 顺序、方向键、弹窗焦点 | 底层输入驱动 |
| 快捷键层 | 页面级动作、输入框冲突 | 系统全局快捷键 |
| 鼠标层 | hover、右键、滚轮 | 高级手势算法 |
| 窗口层 | 宽度断点、多窗口布局 | 系统窗口管理内部实现 |

三、先给页面做焦点地图
焦点不要靠组件自然顺序碰运气。先把页面的焦点节点建模。
export type FocusNodeType = 'button' | 'input' | 'listItem' | 'tab' | 'dialogAction';
export interface FocusNode {
id: string;
type: FocusNodeType;
group: string;
order: number;
enabled: boolean;
}
export function sortFocusNodes(nodes: FocusNode[]): FocusNode[] {
return nodes
.filter((item) => item.enabled)
.sort((a, b) => a.group === b.group ? a.order – b.order : a.group.localeCompare(b.group));
}
这段模型解决三个问题:
如果页面复杂,焦点地图比临时调 focusable 更容易维护。
四、Tab 顺序:表单页面必须能闭环
表单和设置页最需要稳定 Tab 顺序。下面是一个焦点游标。
export class FocusCursor {
private nodes: FocusNode[] = [];
private currentIndex: number = 0;
reset(nodes: FocusNode[]): void {
this.nodes = sortFocusNodes(nodes);
this.currentIndex = 0;
}
current(): FocusNode | undefined {
return this.nodes[this.currentIndex];
}
next(): FocusNode | undefined {
if (this.nodes.length === 0) {
return undefined;
}
this.currentIndex = (this.currentIndex + 1) % this.nodes.length;
return this.current();
}
previous(): FocusNode | undefined {
if (this.nodes.length === 0) {
return undefined;
}
this.currentIndex = (this.currentIndex – 1 + this.nodes.length) % this.nodes.length;
return this.current();
}
}
焦点闭环对弹窗尤其重要。弹窗打开后,Tab 应该在弹窗内部循环,而不是跳到背景页面。
五、方向键移动:列表和网格不要乱跳
大屏上用户经常用方向键浏览卡片。列表和网格要有明确移动规则。
export interface GridFocusPosition {
row: number;
column: number;
}
export function moveGridFocus(
current: GridFocusPosition,
direction: 'up' | 'down' | 'left' | 'right',
rowCount: number,
columnCount: number
): GridFocusPosition {
if (direction === 'up') {
return { row: Math.max(0, current.row – 1), column: current.column };
}
if (direction === 'down') {
return { row: Math.min(rowCount – 1, current.row + 1), column: current.column };
}
if (direction === 'left') {
return { row: current.row, column: Math.max(0, current.column – 1) };
}
return { row: current.row, column: Math.min(columnCount – 1, current.column + 1) };
}
这个函数看起来简单,但它把方向键移动变成可测试逻辑。否则大屏列表一多,用户会感觉焦点“飘”。
六、快捷键注册:输入框优先,页面动作其次
快捷键要集中管理,尤其要避开输入框。
export type ShortcutAction = 'save' | 'search' | 'close' | 'refresh' | 'openCommand';
export interface ShortcutRule {
key: string;
ctrl: boolean;
shift: boolean;
action: ShortcutAction;
disabledWhenTyping: boolean;
}
export function matchShortcut(
eventKey: string,
ctrl: boolean,
shift: boolean,
typing: boolean,
rules: ShortcutRule[]
): ShortcutAction | undefined {
const rule = rules.find((item) =>
item.key === eventKey && item.ctrl === ctrl && item.shift === shift
);
if (!rule) {
return undefined;
}
if (typing && rule.disabledWhenTyping) {
return undefined;
}
return rule.action;
}
规则解释:
七、鼠标反馈:hover 不是装饰,是可点击提示
大屏鼠标用户需要 hover、按下、禁用状态。
export interface PointerVisualState {
hovered: boolean;
pressed: boolean;
disabled: boolean;
}
export function resolvePointerStyle(state: PointerVisualState): string {
if (state.disabled) {
return '#E5E7EB';
}
if (state.pressed) {
return '#D6E4FF';
}
if (state.hovered) {
return '#EEF5FF';
}
return '#FFFFFF';
}
hover 状态要适度。不要让所有卡片都大幅缩放,滚动列表会显得跳。更稳的做法是轻微背景变化、边框增强或阴影变化。
八、多窗口断点:宽度变了,布局要重排
大屏和多窗口场景下,窗口宽度会不断变化。页面应该按断点切换布局。
export type WindowSizeClass = 'compact' | 'medium' | 'expanded';
export function resolveWindowSizeClass(widthVp: number): WindowSizeClass {
if (widthVp < 600) {
return 'compact';
}
if (widthVp < 900) {
return 'medium';
}
return 'expanded';
}
export interface PageLayoutPlan {
sizeClass: WindowSizeClass;
showSideBar: boolean;
columnCount: number;
}
export function buildLayoutPlan(widthVp: number): PageLayoutPlan {
const sizeClass = resolveWindowSizeClass(widthVp);
return {
sizeClass,
showSideBar: sizeClass === 'expanded',
columnCount: sizeClass === 'expanded' ? 3 : sizeClass === 'medium' ? 2 : 1
};
}
断点策略要和设计对齐。不要让同一个页面在窄窗口里仍然展示三列卡片,那会让键鼠操作和阅读都很差。
九、弹窗焦点:打开后先进入弹窗,关闭后回到原控件
弹窗焦点是大屏体验的高频问题。
export interface DialogFocusSnapshot {
triggerNodeId: string;
dialogFirstNodeId: string;
}
export class DialogFocusManager {
private stack: DialogFocusSnapshot[] = [];
open(snapshot: DialogFocusSnapshot): string {
this.stack.push(snapshot);
return snapshot.dialogFirstNodeId;
}
close(): string | undefined {
const snapshot = this.stack.pop();
return snapshot?.triggerNodeId;
}
}
弹窗打开后,焦点应该进入第一个可操作项;弹窗关闭后,焦点回到触发按钮。否则用户用键盘关闭弹窗后,会失去当前位置。
十、大屏键鼠问题排查表
| Tab 顺序混乱 | 没有焦点地图 | 查 FocusNode 顺序 | 按 group/order 管理 |
| 方向键乱跳 | 网格移动规则不明确 | 查行列计算 | 用 moveGridFocus() |
| 输入时触发快捷键 | 未判断 typing | 查快捷键规则 | 输入框聚焦时屏蔽页面动作 |
| 弹窗关闭后焦点丢失 | 没保存触发节点 | 查弹窗栈 | 关闭后恢复 trigger |
| 窄窗口布局挤压 | 没有断点 | 查窗口宽度 | 设计 compact/medium/expanded |
| hover 过于夸张 | 动效过重 | 真机鼠标测试 | 改成轻量反馈 |
大屏排查必须拿键盘和鼠标真测,只靠触控预览看不出来。
十一、大屏验收表
| 全页面可键盘操作 | 不用鼠标也能完成主流程 |
| 焦点可见 | 当前焦点有明显样式 |
| Tab 顺序符合阅读路径 | 不跳、不漏、不进入隐藏元素 |
| 快捷键不干扰输入 | 输入框聚焦时不误触页面动作 |
| 多窗口可重排 | 三种断点都可用 |
| 弹窗焦点闭环 | 打开、Tab、Esc、关闭都稳定 |
| 鼠标 hover 清晰 | 可点击区域有反馈 |
这张表可以直接变成测试用例。大屏适配不是看截图,而是看用户能不能连续操作。
十二、推荐落地顺序
这个顺序能避免“一边改样式一边猜焦点”的混乱。
十三、大屏键鼠相关官方资料
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/introduction-to-one-time-development
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-responsive-layout
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-common-events
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/window-overview
十四、把大屏适配做成输入系统
大屏适配的核心不是“更大的 UI”,而是“更多输入方式”。只要焦点、快捷键、鼠标反馈和多窗口断点有统一规则,页面就能从手机体验自然扩展到平板、电脑和大屏。
最后用这张表复盘:
| 用户看得见焦点吗 | 每个可操作元素都有焦点样式 |
| 用户能只用键盘完成吗 | 主流程不依赖鼠标 |
| 窗口变窄怎么办 | 断点布局自动重排 |
| 弹窗会不会抢错焦点 | 弹窗内部闭环,关闭回到触发项 |
| 快捷键谁维护 | 集中配置,不散落页面 |





