焦点管理:使用Tab键控制UI组件的焦点切换逻辑(74)

在鸿蒙(HarmonyOS)PC 端和平板端开发中,焦点(Focus)不仅仅是"高亮显示",其本质是**"输入路由权"**,决定了键盘事件发给谁、快捷键是否生效以及输入法是否激活。

构建符合桌面级体验的焦点切换逻辑,需要结合系统的走焦算法与自定义属性。以下是实现 Tab 键控制焦点切换的核心策略与代码示例:

一、 基础 Tab 键走焦(tabIndex 属性)

鸿蒙系统默认支持 Tab 键遵循 Z 字型遍历逻辑。开发者可以通过 tabIndex 属性显式指定组件的获焦顺序,使焦点按照业务逻辑而非单纯的 UI 挂载顺序进行跳转。

核心代码示例:

javascript 复制代码
Column({ space: 20 }) {
  TextInput({ placeholder: '用户名' })
    .tabIndex(1) // 按 Tab 键时第一个获焦

  TextInput({ placeholder: '密码', type: InputType.Password })
    .tabIndex(2) // 按 Tab 键时第二个获焦

  Button('登录')
    .tabIndex(3) // 按 Tab 键时第三个获焦
}

二、 焦点组与区域级快速跳转(tabIndex + groupDefaultFocus)

在复杂的 PC 界面中(如包含侧边栏、内容区、设置面板),如果逐个遍历所有元素效率极低。可以通过将容器配置 tabIndex,并结合内部子组件的 groupDefaultFocus,实现按 Tab 键在"区域"间快速切换,同时自动聚焦到该区域内的默认核心控件。

核心代码示例:

javascript 复制代码
Row({ space: 20 }) {
  // 侧边导航区
  Column({ space: 10 }) {
    Button('首页')
    Button('设置').groupDefaultFocus(true) // 当焦点进入此区域时,默认获焦
    Button('关于')
  }
  .tabIndex(1) // 作为焦点组1,按 Tab 键时整体参与遍历

  // 内容操作区
  Column({ space: 10 }) {
    TextInput({ placeholder: '搜索内容' }).groupDefaultFocus(true) // 内容区默认获焦
    Button('提交')
  }
  .tabIndex(2) // 作为焦点组2
}

三、 页面初始化默认焦点(defaultFocus)

当页面首次加载或从其他层级页面返回时,系统默认焦点通常位于根容器上。为了提升操作效率,应使用 defaultFocus 将初始焦点直接指定到用户最可能需要操作的组件上(如搜索框或主按钮)。

核心代码示例:

javascript 复制代码
Column() {
  Text('欢迎使用鸿蒙PC应用')
  
  TextInput({ placeholder: '请输入搜索关键词' })
    .defaultFocus(true) // 页面加载时自动获取焦点并唤起输入法
}

四、 鼠标与键盘的焦点状态隔离(focusOnTouch)

PC 端用户常在鼠标和键盘之间切换。为了避免鼠标点击后屏幕上残留难看的焦点框,鸿蒙提供了 focusOnTouch 属性。启用后,鼠标点击组件会使其获焦(触发内部逻辑),但不会显示焦点框;只有当用户再次按下 Tab 键或方向键时,焦点框才会重新显现。

核心代码示例:

javascript 复制代码
Button('操作按钮')
  .focusOnTouch(true) // 允许鼠标点击获焦,但隐藏焦点框
  .onFocus(() => {
    // 无论是鼠标点击还是键盘 Tab 切换,都会触发此回调
    console.info('组件已获取输入路由权');
  })

五、 主动请求焦点(focusControl.requestFocus)

在某些复杂的交互场景下(例如:弹窗关闭后焦点需要回到触发按钮,或者表单校验失败后焦点需自动跳转到错误输入框),需要通过代码主动控制焦点。建议使用 getUIContext().getFocusController() 获取绑定实例的焦点控制器,避免实例不明确的问题。

核心代码示例:

javascript 复制代码
@Entry
@Component
struct FocusControlExample {
  build() {
    Column({ space: 20 }) {
      TextInput({ placeholder: '目标输入框' })
        .id('targetInput') // 必须设置唯一 ID

      Button('将焦点移至输入框')
        .onClick(() => {
          // 主动将焦点转移到指定 ID 的组件上
          UIContext.getCurrentUIContext().getFocusController().requestFocus('targetInput');
        })
    }
  }
}

PC 端焦点管理架构建议

  1. 焦点集中建模 :在大型 PC 应用中,切忌让各个组件在生命周期中随意调用 requestFocus() 抢夺焦点。建议定义一个明确的 FocusModel,由统一的 Controller 调度焦点切换,组件仅声明"我能不能被聚焦"。
  2. 遵循十字走焦规范:Tab 键负责 Z 字型的线性遍历,而方向键(上、下、左、右)应遵循十字型移动策略。对于复杂布局,系统默认采用中心点距离优先算法来确定下一个目标。
  3. 位置记忆机制:当用户通过鼠标或触控板交互导致焦点隐藏后,再次使用键盘触发焦点时,系统/程序应能记住上次焦点操作的位置,避免每次都要从头开始按 Tab 键。
  4. 可交互才可获焦:纯展示类内容(如普通文本、分割线、数据图表)不可获焦。不要为了让焦点框经过某个位置而给纯展示控件强行绑定点击事件。

六、 动态列表焦点保持(nextFocus 属性)

在长列表或瀑布流场景中,当用户通过方向键(↑/↓)进行走焦时,系统默认的投影走焦算法可能会因为组件大小不一而导致焦点"乱跳"。开发者可以通过 nextFocus 属性,显式指定组件在四个方向上的下一个获焦目标,确保焦点移动的绝对可控。

核心代码示例:

javascript 复制代码
Column({ space: 10 }) {
  ForEach(this.cardList, (item: CardItem, index: number) => {
    CardComponent({ item: item })
      .nextFocus({ 
        // 显式指定向下走焦的目标组件 ID
        down: `card_${index + 1}`, 
        // 显式指定向上走焦的目标组件 ID
        up: `card_${index - 1}` 
      })
      .id(`card_${index}`)
  })
}

七、 自定义焦点样式反馈(stateStyles 多态样式)

PC 端用户对焦点的视觉反馈极其敏感。除了系统默认的焦点框,开发者可以通过 stateStyles 属性,为组件配置专属的获焦态(.focused())样式,例如改变边框颜色、添加阴影或轻微放大,从而提供清晰的视觉指引。

核心代码示例:

javascript 复制代码
Button('提交表单')
  .width(120)
  .height(40)
  .stateStyles({
    // 获焦态:高亮边框与阴影
    focused: {
      borderWidth: 2,
      borderColor: '#007DFF',
      shadow: { radius: 8, color: '#33007DFF', offsetX: 0, offsetY: 2 }
    },
    // 按压态:颜色加深
    pressed: {
      backgroundColor: '#005BB5'
    }
  })

八、 监听焦点状态变化(onFocus / onBlur)

在复杂的业务逻辑中,组件可能需要根据自身的焦点状态来触发特定的行为(例如:输入框获焦时清空占位符提示,或者列表项获焦时自动滚动到可视区域)。通过成对使用 onFocusonBlur 事件,可以精准捕获焦点的生命周期。

核心代码示例:

javascript 复制代码
TextInput({ placeholder: '请输入内容' })
  .onFocus(() => {
    console.info('输入框已获焦,准备接收键盘输入');
    // 可在此处触发软键盘或高亮相关提示
  })
  .onBlur(() => {
    console.info('输入框已失焦,执行数据校验');
    // 可在此处执行表单验证逻辑
  })

九、 被动走焦的异常处理与边界控制

当处于焦点状态的组件被删除、隐藏(visibility 设为 Hidden/None)或其 focusable 属性被动态置为 false 时,系统会触发"被动走焦"。为了防止焦点丢失导致键盘操作失效,开发者应在删除组件前,主动将焦点转移到安全的备用组件上。

核心代码示例:

javascript 复制代码
Button('删除当前卡片')
  .onClick(() => {
    // 【关键】在删除当前获焦组件前,先将焦点转移给相邻的安全组件
    if (this.currentFocusedId === this.currentCardId) {
      UIContext.getCurrentUIContext().getFocusController().requestFocus('safe_backup_button');
    }
    // 执行删除逻辑
    this.removeCurrentCard();
  })

十、 焦点作用域隔离(focusScopeId)

在包含多个独立交互区域的复杂 PC 界面(如左侧导航栏、右侧多标签页编辑器)中,为了防止 Tab 键在切换区域时产生混乱,可以使用 focusScopeId 将焦点限制在特定的作用域内。当焦点进入该作用域时,Tab 键只会在该区域内的可获焦组件间循环,直到通过特定的快捷键或方向键跳出。

核心代码示例:

javascript 复制代码
Row() {
  // 左侧导航作用域
  Column() {
    Button('导航1')
    Button('导航2')
  }
  .focusScopeId('nav_scope')

  // 右侧编辑器作用域
  Column() {
    TextInput({ placeholder: '编辑区' })
    Button('保存')
  }
  .focusScopeId('editor_scope')
}