HarmonyOS应用开发实战:猫猫大作战-onHover 触发时机、HoverType 类型判定、与 onTouch 的差异、TV/PC 场景应用四

前言

上一篇我们用 onTouch 处理了手势全过程------按下/移动/抬起。但有种交互不靠「接触」而靠「接近」------鼠标悬停、遥控器焦点停留、电视遥控方向选中 。在 TV/Pad 遥控器和桌面鼠标场景下,用户不点击就「悬停」在某控件上,要给反馈(高亮/提示/展开子菜单)。HarmonyOS 的 onHover 事件捕获「进入/离开」两种状态,是 TV/PC 应用的必备。

本篇以「猫猫大作战」底部按钮悬停高亮为预演场景,把 onHover 触发时机HoverType 类型判定与 onTouch 的差异TV/PC 场景应用四大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--51 篇。本篇是阶段三第二篇。

一、场景拆解:底部按钮悬停反馈

「猫猫大作战」底部控制栏(第 11、32 篇)有「暂停/重新开始」两个按钮。手机端用户点击就触发------但 TV 端用户用遥控器方向键选中时,焦点停留但未点击,要给「这个按钮被选中了」的视觉反馈:

ts 复制代码
// 预演:底部按钮加 onHover 悬停高亮
Row() {
  Button('暂停')
    .backgroundColor(this.hoverBtn === 'pause' ? '#3498DB' : '#95A5A6')
    .onHover((isHover: boolean) => {
      if (isHover) {
        this.hoverBtn = 'pause';     // 焦点进入:高亮
      } else {
        this.hoverBtn = '';          // 焦点离开:复位
      }
    })
    .onClick(() => { this.pauseGame(); })

  Spacer()

  Button('重新开始')
    .backgroundColor(this.hoverBtn === 'restart' ? '#E67E22' : '#95A5A6')
    .onHover((isHover: boolean) => {
      if (isHover) {
        this.hoverBtn = 'restart';
      } else {
        this.hoverBtn = '';
      }
    })
    .onClick(() => { this.clearTimers(); this.startGame(); })
}

核心问题

  1. onHover 何时触发?和 onTouch 的「按下」有什么不同?
  2. isHover: boolean 怎么判定进入还是离开?
  3. TV 端遥控器、PC 端鼠标、手机端手指,三种输入分别怎么触发?
  4. 手机端需要 onHover 吗?

二、onHover 触发时机

2.1 两种触发

复制代码
指针进入组件边界 → onHover(true)
       ↓
指针在组件内移动(不重复触发,与 onTouch 不同)
       ↓
指针离开组件边界 → onHover(false)
事件 触发时机 参数
onHover(true) 指针进入组件边界 isHover = true
onHover(false) 指针离开组件边界 isHover = false

关键经验onHover 只触发「进入/离开」两次------指针在组件内移动不重复触发(不像 onTouch 的 Move 高频)。

2.2 事件回调签名

ts 复制代码
.onHover((isHover: boolean) => {
  // isHover: true 进入,false 离开
})

2.3 与 onTouch 的关键差异

维度 onTouch onHover
触发条件 必须接触(手指/鼠标按下) 不接触(鼠标移动/遥控焦点)
阶段 Down/Move/Up 三阶段 进入/离开两阶段
移动时 高频 Move 触发 不触发(只边界进出)
手机端 手指触摸触发 ❌ 手机无悬停概念
TV 端 ❌ 遥控器无接触 ✅ 焦点停留触发
PC 端 鼠标按下触发 ✅ 鼠标移过触发

关键经验onTouch 要「接触」,onHover 要「接近」------手机端用 onTouch,TV/PC 端补 onHover。

三、HoverType 类型判定(进阶)

3.1 基本用法:isHover 布尔

最简形式就是 isHover: boolean

ts 复制代码
.onHover((isHover: boolean) => {
  if (isHover) {
    this.hoverBtn = 'pause';
  } else {
    this.hoverBtn = '';
  }
})

3.2 进阶:HoverEventObject 拿坐标

某些版本 ArkUI 的 onHover 也能传 HoverEventObject 拿坐标:

ts 复制代码
.onHover((event: HoverEventObject) => {
  // event.type: HoverType(Enter/Exit/Move 在某些版本)
  // event.x, event.y: 指针相对组件的局部坐标
  // event.isHover: boolean
})

判定进入/离开

ts 复制代码
.onHover((event: HoverEventObject) => {
  switch (event.type) {
    case HoverType.Enter:    // 进入
      this.hoverBtn = 'pause';
      break;
    case HoverType.Exit:     // 离开
      this.hoverBtn = '';
      break;
  }
})

提示:不同 ArkUI 版本的 onHover 签名可能不同------有的传 boolean,有的传 HoverEventObject。以实际 SDK 为准,最简 boolean 形式兼容性最好。

3.3 坐标信息

ts 复制代码
.onHover((event: HoverEventObject) => {
  console.info(`悬停在 (${event.x}, ${event.y})`);
})

坐标系 :和 onTouch 一样,x/y相对组件左上角的局部坐标。

实战经验消除类游戏通常只用 boolean 形式------不关心具体悬停位置,只关心「是否悬停」。

四、三种输入设备的触发差异

4.1 鼠标(PC 端)

复制代码
鼠标移入组件边界 → onHover(true)
鼠标在组件内移动 → 不触发(只边界进出)
鼠标移出组件边界 → onHover(false)
鼠标按下 → onTouch(Down)
鼠标抬起 → onTouch(Up) + onClick

PC 端典型交互:鼠标移入高亮(onHover),点击触发(onClick)。

4.2 遥控器(TV 端)

复制代码
方向键选中某控件 → 控件获得焦点 → onHover(true)
方向键移到别控件 → 原控件失焦 → onHover(false),新控件 onHover(true)
确认键按下 → onClick

TV 端典型交互:方向键选中高亮(onHover),确认键触发(onClick)。

4.3 手指(手机端)

复制代码
手指悬停?→ ❌ 手机无悬停概念
手指触摸 → onTouch(Down)
手指抬起 → onTouch(Up) + onClick

手机端典型交互:手指触摸高亮(onTouch Down),抬起触发(onClick)。

4.4 三设备对照

设备 悬停事件 接触事件 点击事件
PC 鼠标 ✅ onHover onTouch onClick
TV 遥控 ✅ onHover(焦点) onClick(确认键)
手机手指 onTouch onClick

关键经验手机端不需要 onHover------但写上无害(手机不触发)。跨设备应用统一加 onHover,手机端自动忽略。

五、实战:底部按钮悬停高亮

5.1 改造底部控制栏

ts 复制代码
// 预演:Index 底部控制栏加 onHover(第 11、32 篇的底部栏改造)
@Entry
@Component
struct Index {
  @State gameState: GameState = GameState.IDLE;
  @State hoverBtn: string = '';        // ← onHover 状态:哪按钮悬停中
  /* ... 其他 state */

  /* startGame / pauseGame / resumeGame / endGame / clearTimers / formatTime / aboutToDisappear 筑略 */

  @Builder
  GameView() {
    Column() {
      this.GameHUD()
      Column() {
        Row() { /* 预告区 */ }
        Stack() {
          /* 棋盘背景、猫咪渲染、列点击层 */
        }
        .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE)
        .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
        .borderRadius(12).clip(true).backgroundColor('#D6EEF5')
      }.alignItems(HorizontalAlign.Center)

      Spacer()

      // 底部控制栏:onHover 悬停高亮(本篇预演)
      Row() {
        Button('暂停')
          .width(120).height(44)
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')
          // 悬停高亮:hoverBtn === 'pause' 时变深色
          .backgroundColor(this.hoverBtn === 'pause' ? '#3498DB' : '#95A5A6')
          .borderRadius(22)
          .onHover((isHover: boolean) => {       // ← onHover
            this.hoverBtn = isHover ? 'pause' : '';
          })
          .onClick(() => { this.pauseGame(); })

        Spacer()

        Button('重新开始')
          .width(120).height(44)
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')
          .backgroundColor(this.hoverBtn === 'restart' ? '#E67E22' : '#95A5A6')
          .borderRadius(22)
          .onHover((isHover: boolean) => {       // ← onHover
            this.hoverBtn = isHover ? 'restart' : '';
          })
          .onClick(() => {
            this.clearTimers();
            this.startGame();
          })
      }
      .width('100%')
      .padding({ left: 24, right: 24, bottom: 24, top: 12 })
    }
    .width('100%').height('100%')
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
    })
    .alignItems(HorizontalAlign.Center)
  }

  /* GameHUD / MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */
}

5.2 触发流程

PC 鼠标场景

  1. 鼠标移入「暂停」按钮 → onHover(true)hoverBtn = 'pause' → 按钮变深蓝色高亮。
  2. 鼠标点击 → onClickpauseGame()
  3. 鼠标移出「暂停」按钮 → onHover(false)hoverBtn = '' → 按钮变回灰色。

TV 遥控场景

  1. 方向键选中「暂停」按钮 → onHover(true) → 高亮。
  2. 确认键按下 → onClickpauseGame()
  3. 方向键移到「重新开始」→ 「暂停」onHover(false) 复位,「重新开始」onHover(true) 高亮。

手机手指场景

  • onHover 不触发,hoverBtn 始终为 '',按钮始终灰色------但用户点击时按钮有 onClick 反馈,不影响功能

六、进阶:悬停展开子菜单

6.1 悬停展开下拉菜单

ts 复制代码
@State hoverMenu: boolean = false;

Column() {
  Button('设置')
    .onHover((isHover: boolean) => {
      this.hoverMenu = isHover;     // 悬停时展开菜单
    })

  if (this.hoverMenu) {
    Column() {
      Text('音效设置').onClick(() => { /* ... */ })
      Text('震动设置').onClick(() => { /* ... */ })
      Text('语言设置').onClick(() => { /* ... */ })
    }
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .padding(8)
  }
}

实战经验悬停展开菜单在 PC 端常见------但 TV 端遥控器方向键要能进入子菜单,要配 focusable tabindex(第 53 篇会专讲按键)。

6.2 悬停 Tooltip 提示

ts 复制代码
@State hoverTip: string = '';

Stack() {
  Button('暂停')
    .onHover((isHover: boolean) => {
      this.hoverTip = isHover ? '点击暂停游戏' : '';
    })

  if (this.hoverTip !== '') {
    Text(this.hoverTip)
      .fontSize(12).fontColor('#FFFFFF')
      .backgroundColor('rgba(0,0,0,0.8)')
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .borderRadius(4)
      .position({ x: 0, y: -30 })    // 显示在按钮上方
  }
}

关键经验Tooltip 悬停提示是 PC 端惯例------手机端用长按出 Tooltip(onTouch + setTimeout)。

七、踩坑提示

7.1 onHover 用普通函数丢 this

ts 复制代码
// ❌ 错误:普通函数 this 不指向组件
.onHover(function (isHover) { this.hoverBtn = 'pause'; })

// ✅ 正确:箭头函数保留 this(第 38 篇讲过)
.onHover((isHover: boolean) => { this.hoverBtn = isHover ? 'pause' : ''; })

7.2 忘复位离开状态

ts 复制代码
// ❌ 错误:进入高亮,离开忘复位,按钮一直高亮
.onHover((isHover: boolean) => {
  if (isHover) { this.hoverBtn = 'pause'; }
  // 忘了 else { this.hoverBtn = ''; }
})

// ✅ 正确:进入/离开都处理
.onHover((isHover: boolean) => {
  this.hoverBtn = isHover ? 'pause' : '';
})

7.3 手机端期望 onHover 触发

ts 复制代码
// ❌ 错误:手机端测试发现 onHover 不触发,以为 bug
// 实际手机无悬停概念,onHover 只在 PC/TV 触发

// ✅ 正确:跨设备统一加 onHover,手机端自动忽略;手机端高亮用 onTouch Down
Column()
  .onHover((isHover) => { this.hoverBtn = isHover ? 'pause' : ''; })    // PC/TV
  .onTouch((event) => {                                                  // 手机
    if (event.type === TouchType.Down) { this.hoverBtn = 'pause'; }
    else if (event.type === TouchType.Up) { this.hoverBtn = ''; }
  })
  .onClick(() => { this.pauseGame(); })

7.4 期望悬停时持续触发

ts 复制代码
// ❌ 错误:期望指针在组件内移动时持续触发 onHover
.onHover((isHover) => {
  this.updatePosition();     // 以为会持续调,实际只进出两次
})

// ✅ 正确:悬停要持续响应用 onTouch(如果接触)或鼠标移动事件
.onMouse((event) => {        // 鼠标移动事件(PC 专)
  if (event.action === MouseAction.Move) {
    this.updatePosition();
  }
})

八、调试技巧

  1. console.info 打 isHover:追触发时机,验证是否只在边界进出。
  2. PC 端用 DevEco 预览器鼠标测试:移入移出看 onHover 日志。
  3. TV 端用遥控器模拟器:方向键选中看 onHover 日志。
  4. 手机端不触发排查:确认是正常行为(手机无悬停),不是 bug;手机端高亮改用 onTouch。

九、性能与最佳实践

  1. PC/TV 跨设备应用加 onHover------手机端自动忽略,不冲突。
  2. onHover 只触发进入/离开------指针在组件内移动不重复触发,不像 onTouch Move。
  3. 进入高亮,离开复位------忘复位按钮一直高亮。
  4. 悬停展开菜单/Tooltip 在 PC 端常见------TV 端要配 focusable 让方向键能进入。
  5. 手机端高亮用 onTouch Down------手机无悬停,触摸按下时高亮。
  6. 回调用箭头函数保留 this------普通函数 this 丢失。

十、阶段三进度(51--55)

本篇是阶段三「交互与动画」第 2 篇:

主题 核心要点
51 onTouch 手势三阶段 Down/Move/Up
52(本篇) onHover 悬停进入/离开,TV/PC 场景
53 onKeyEvent 键盘/遥控器按键
54 bindContextMenu 右键/长按上下文菜单
55 animateTo 显式动画触发

总结

本篇我们从 onHover 悬停切入,掌握了触发时机(进入/离开两次)isHover 布尔判定与 onTouch 的差异(接近 vs 接触) 、**三设备输入差异(PC/TV 触发,手机不触发)**四大要点,并给出了底部按钮悬停高亮的完整代码。核心要点:onHover 只触发进出两次;PC/TV 跨设备加;手机端自动忽略改用 onTouch;进入高亮离开复位

下一篇我们将拆解 onKeyEvent------键盘/遥控器按键事件。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
程序员黑豆1 小时前
鸿蒙应用开发之@State 装饰器详解:从基本类型到 @Observed/@ObjectLink/@Track 嵌套监听
前端·harmonyos
慧海灵舟1 小时前
鸿蒙南向开发教程 Day 11:GPIO 按键中断与 LED 状态机
华为·harmonyos
LEO111102 小时前
HarmonyOS应用开发实战:猫猫大作战-HUD 拆成独立子组件、用 `@Prop` 接收父组件得分/时间为锚点,把 @Prop 声明与传递、单向只
harmonyos·鸿蒙
yaoyaoxingzhe2 小时前
HarmonyOS应用开发实战:猫猫大作战-`ForEach` 的双层嵌套、`keyGenerator` 密钥、`width` 百分比响应式格子
harmonyos·鸿蒙
echohelloworld113 小时前
HarmonyOS应用开发实战:猫猫大作战-Row 横向布局、Spacer 占位、Button 双态样式(暂停灰 / 重新开始红)、底部 paddin
harmonyos·鸿蒙
爱写代码的森4 小时前
鸿蒙三方库 | harmony-utils之ObjectUtil对象拷贝与合并详解
华为·harmonyos·鸿蒙·huawei
fiona20264 小时前
HarmonyOS应用开发实战:猫猫大作战-Scroll 容器、Scroller 控制器、滚动监听与性能
harmonyos·鸿蒙
GitCode官方5 小时前
开源鸿蒙跨平台直播| 快手KRN鸿蒙适配与性能优化
华为·开源·harmonyos·atomgit
爱写代码的森5 小时前
鸿蒙三方库 | harmony-utils之TypeUtil类型检查工具详解
华为·harmonyos·鸿蒙·huawei