手机上的任务中心,用户习惯用手指点开任务;到了平板或者电脑,同样的页面还会接收鼠标点击、外接键盘的 Tab 键、方向键和快捷键。布局断点解决的是“内容放在哪里”,输入适配却要回答“操作从哪里开始、焦点接下来去哪里”。两件事混为一谈,就容易出现列表能点却不能用键盘选、详情打开后焦点仍留在旧控件、关闭弹窗后找不到上一个操作位置等问题。

多设备输入交互与焦点管理封面

上一篇把任务列表从窄屏单列扩展到宽屏主从两栏。这一次沿用“工作任务中心”的业务关系,但把关注点放在鼠标与键盘。一个可交付的多设备页面不只是屏幕尺寸变化后的排版,还应该让同一任务在不同输入设备上都能被发现、聚焦、打开、编辑和返回。

从触摸操作迁移到键鼠操作,丢失的是什么

设想用户在任务列表中筛选“需求评审”,然后按 Tab 跳到结果列表,选中第一项,继续进入详情中的“编辑”。触屏只需要看得见的点击区域;键盘则需要一条连续且可预测的焦点路径。鼠标介于两者之间:指针可以悬停在卡片上,但悬停高亮并不等于元素已经获焦,键盘后续动作也不应该依赖刚才的鼠标位置。

建议把业务状态和交互状态分开。业务状态保存 selectedId、筛选关键字和编辑内容;交互状态记录当前获焦组件、上次焦点来源和是否正在编辑。只保存 selectedId 还不够,因为任务选中以后,用户可能停留在搜索框,也可能已经进入编辑按钮。反过来,切换焦点时也不能顺便更改任务的业务选择,避免出现“按一次 Tab 就换了一条任务”的意外。

输入方式用户预期页面需要维护的机制
手指点击点击任务即可选中,点击操作按钮可编辑明确点击区域,选中态独立于焦点
鼠标点击指针悬停有反馈,点击后操作有效onHover 提示与 onClick 行为区分
外接键盘Tab 沿合理顺序走焦,Enter 激活控件可获焦组件和自然的走焦顺序
快捷键搜索入口快速定位,不改写系统快捷键审核组合键冲突并设置明确动作

对大型表格尤其如此。表格中如果每个单元格都挤进 Tab 顺序,用户要经过几十次按键才能到“保存”。可把行级按钮、搜索框、工具栏作为主要焦点节点,行内次要操作借助菜单展开,这样能缩短输入路径。

给焦点设置真实的“落脚点”

ArkUI 的 focusable 决定组件能否获焦,focusOnTouch 支持点击获焦,tabIndex 参与 Tab 走焦管理。TextInput 与 Button 等带有内建交互的组件本身具备获焦能力。defaultFocus(true) 只作用于对应页面初次进入时的默认焦点,不应当被当作每次切换详情的焦点恢复器。对于需要主动转移焦点的流程,可通过 getUIContext().getFocusController().requestFocus() 按组件标识申请焦点。

以下页面把焦点展示与任务选中分离。搜索框拥有固定 id,任务按钮拥有稳定的业务标识。筛选以后,焦点不会因为列表顺序发生变化而错误地指向另一条记录。Ctrl+K 只是应用自定义的搜索入口,正式产品仍应结合系统快捷键与输入法操作习惯核对冲突。

interface WorkItem {
  id: string;
  title: string;
}

@Entry
@Component
struct TaskWorkspace {
  @State query: string = '';
  @State selectedId: string = 'review';
  @State focusLabel: string = '搜索任务';
  private items: WorkItem[] = [
    { id: 'review', title: '需求评审' },
    { id: 'design', title: '交互设计' },
    { id: 'publish', title: '发布清单' }
  ];

  private focusSearch(): void {
    this.getUIContext().getFocusController()
      .requestFocus('taskSearch');
  }

  build() {
    Column({ space: 14 }) {
      Text('工作任务中心').fontSize(24).fontWeight(FontWeight.Bold)
      TextInput({ text: this.query, placeholder: '搜索任务' })
        .id('taskSearch')
        .focusable(true)
        .focusOnTouch(true)
        .onChange((value: string) => { this.query = value; })
        .onFocus(() => { this.focusLabel = '搜索任务'; })
        .keyboardShortcut('k', [ModifierKey.CTRL],
          () => { this.focusSearch(); })
      ForEach(this.items.filter((item: WorkItem) =>
        item.title.includes(this.query)), (item: WorkItem) => {
        Button(item.title)
          .id('task-' + item.id)
          .focusable(true)
          .focusOnTouch(true)
          .onFocus(() => { this.focusLabel = item.title; })
          .onClick(() => { this.selectedId = item.id; })
          .backgroundColor(this.selectedId === item.id ?
            '#236AF2' : '#DDE9FF')
      }, (item: WorkItem) => item.id)
      Text('当前焦点:' + this.focusLabel).fontSize(14)
    }
    .padding(20)
    .width('100%')
  }
}

白色主题开发环境中的搜索框焦点与任务选择

这里有两处容易被忽略的细节。第一,ForEach 的键使用业务 id,而不是数组下标。排序、筛选、异步增量更新都可能改变数组下标,用它当组件身份会增加焦点落错项目的风险。第二,requestFocus 申请的组件必须存在并允许获焦;如果某个详情编辑控件只在宽屏条件分支中创建,窄屏切换时就需要先完成导航和渲染,再申请焦点,不宜对尚未生成的组件直接发起请求。

焦点路径与业务选中态必须各有边界

在窄屏页面,列表与详情通常按导航层级依次出现;在宽屏页面,二者可能同时可见。如果“进入详情”的行为自动触发焦点切换,就应该定义清楚是落在详情标题、首个输入框,还是保存按钮。对于只读详情,优先让用户继续用 Tab 浏览可操作控件;对于明确进入编辑态的操作,将焦点交给首个可编辑字段更符合预期。

下面的代码把业务导航和焦点决策分离。它不直接访问 UI,因而适合写成独立的可测试函数。这里使用枚举式字符串作为目标,避免在不同宽度下散落硬编码组件名称。

export type FocusTarget = 'search' | 'list' | 'detail' | 'save';

export class FocusRoute {
  static afterOpenDetail(wideWindow: boolean): FocusTarget {
    return wideWindow ? 'detail' : 'save';
  }

  static afterCloseEditor(wideWindow: boolean): FocusTarget {
    return wideWindow ? 'detail' : 'list';
  }

  static targetId(target: FocusTarget, selectedId: string): string {
    switch (target) {
      case 'search': return 'taskSearch';
      case 'list': return 'task-' + selectedId;
      case 'detail': return 'detailTitle';
      case 'save': return 'saveButton';
    }
  }
}

这个函数只给出“应该回到哪里”,真正转移焦点的入口仍由页面控制器负责。页面在触发导航以后,应先检查目标控件是否已装载,再通过 UIContext 申请焦点,必要时使用默认焦点策略作为后备。这样即使用户从双栏拖回单栏,业务选中的任务仍然是同一个,交互目标却可以根据实际布局改变。

鼠标悬停与快捷键不是两套业务逻辑

鼠标进入任务卡片可以改变边框或背景,但不应该自动打开详情。onHover 更适合表达“这里可以操作”;onClick 才提交选中。键盘的 Enter 则应尽可能利用 Button 等组件现成的激活行为,而不是同时写一个 onKeyEvent 再调用一次相同动作,否则一次按键可能重复执行提交。

需要自定义组合键时,ArkUI 的 keyboardShortcut 能绑定字符与修饰键。需要拦截原始按键时,onKeyEvent 能收到 KeyEvent,但必须明确处理的键、事件阶段及传播方式;不可把所有按键都拦截,系统已有的 Tab 走焦和文本输入就会受到破坏。对于搜索框,尤其不要覆盖输入法的 Enter 行为或文本编辑快捷键。

下面是鼠标悬停只改变视觉提示、点击才改变选中的局部代码。项目列表项仍可使用按钮作为交互主体。

@Component
struct TaskAction {
  @Prop title: string;
  @Link selectedId: string;
  @Prop itemId: string;
  @State hovering: boolean = false;

  build() {
    Button(this.title)
      .id('action-' + this.itemId)
      .focusable(true)
      .focusOnTouch(true)
      .onHover((value: boolean) => { this.hovering = value; })
      .onClick(() => { this.selectedId = this.itemId; })
      .border({ width: this.hovering ? 2 : 1,
        color: this.hovering ? '#236AF2' : '#C5D3E8' })
  }
}

在设备切换时保住用户的操作上下文

多设备适配最难发现的问题不在初始页面,而在“操作到一半”。例如用户在平板上编辑任务,连接键盘后改变窗口宽度,详情从右栏变为新页面;此时若因 UI 重建将 draft 重新初始化,输入内容就丢失了。业务数据应当留在状态管理层,容器的分栏或单栏切换只改变视图结构;焦点可以重新计算,但编辑草稿必须继续存在。

对于带搜索条件的列表,要同时保存筛选词与已选任务标识。如果筛选后被选项从结果列表消失,详情区应给出“当前任务不在筛选结果中”的明确反馈,而不是悄悄指向列表首条。对于弹窗,打开前记录触发控件标识;关闭后优先回到触发入口。如果入口因为断点切换被销毁,则回到同层级可见的安全目标,如工具栏按钮或列表首项。

手机任务输入界面与焦点反馈

检查多端输入路径时,可用一条完整用户任务覆盖多个交互节点:打开任务中心、筛选任务、用 Tab 走焦、进入详情、修改文本、通过鼠标保存、缩放窗口、再返回原任务。每一步分别记录选中项、草稿内容、可见焦点和当前窗口布局。比起单独点几个按钮,这条链路更容易发现焦点丢失、重复提交、选中态错位及搜索词消失。

最后把视觉焦点也纳入无障碍体验:焦点框与背景保持足够区分度,不能只靠颜色区分;按钮要具有明确文本,禁用状态应具有可见反馈。焦点顺序遵循阅读顺序,界面缩放后不把内容藏在无法通过键盘到达的区域。这样,上一节的断点布局才能真正成为可交互的大屏应用。

相关文档:ArkUI 焦点控制、组件快捷键事件、焦点事件、HarmonyOS 多设备开发。

多设备交互的成果不是把手机的按钮原样放大,而是让用户在触摸、鼠标与键盘之间切换时,操作对象保持一致,焦点去向清楚,输入内容不会丢失。布局可以因窗口而改变,任务的连续性不该因此中断。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐