

前言
上一篇我们用 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(); })
}
核心问题:
onHover何时触发?和onTouch的「按下」有什么不同?isHover: boolean怎么判定进入还是离开?- TV 端遥控器、PC 端鼠标、手机端手指,三种输入分别怎么触发?
- 手机端需要 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 鼠标场景:
- 鼠标移入「暂停」按钮 →
onHover(true)→hoverBtn = 'pause'→ 按钮变深蓝色高亮。 - 鼠标点击 →
onClick→pauseGame()。 - 鼠标移出「暂停」按钮 →
onHover(false)→hoverBtn = ''→ 按钮变回灰色。
TV 遥控场景:
- 方向键选中「暂停」按钮 →
onHover(true)→ 高亮。 - 确认键按下 →
onClick→pauseGame()。 - 方向键移到「重新开始」→ 「暂停」
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();
}
})
八、调试技巧
console.info打 isHover:追触发时机,验证是否只在边界进出。- PC 端用 DevEco 预览器鼠标测试:移入移出看 onHover 日志。
- TV 端用遥控器模拟器:方向键选中看 onHover 日志。
- 手机端不触发排查:确认是正常行为(手机无悬停),不是 bug;手机端高亮改用 onTouch。
九、性能与最佳实践
- PC/TV 跨设备应用加 onHover------手机端自动忽略,不冲突。
- onHover 只触发进入/离开------指针在组件内移动不重复触发,不像 onTouch Move。
- 进入高亮,离开复位------忘复位按钮一直高亮。
- 悬停展开菜单/Tooltip 在 PC 端常见------TV 端要配 focusable 让方向键能进入。
- 手机端高亮用 onTouch Down------手机无悬停,触摸按下时高亮。
- 回调用箭头函数保留 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------键盘/遥控器按键事件。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - ArkUI onHover 悬停事件官方指南
- HarmonyOS TV/PC 跨设备适配最佳实践
- ArkUI 鼠标与遥控器事件官方指南
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md