在鸿蒙(HarmonyOS)PC 端和平板端应用中,自定义鼠标光标(Cursor)是提供精准操作反馈和打造专业级桌面体验的重要一环。开发者可以通过系统内置样式(setCursor)和自定义图像资源(setCustomCursor)两种方式来控制光标形状。
以下是实现光标样式自定义的核心策略与代码示例:
一、 使用系统内置光标样式(setCursor)
对于常见的交互场景,鸿蒙提供了丰富的内置光标样式(如手型、十字准星、文本输入、方向箭头等)。官方推荐通过 getUIContext().getCursorController() 获取实例来控制光标,以避免 UI 上下文不明确的问题。
核心代码示例:
javascript
import { pointer } from '@kit.InputKit';
Row()
.width(200)
.height(200)
.backgroundColor(Color.Green)
.onHover((flag: boolean) => {
if (flag) {
// 鼠标悬停时,更改为系统内置的手型光标
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.HAND);
} else {
// 鼠标离开时,恢复为默认的箭头光标
this.getUIContext().getCursorController().restoreDefault();
}
})
二、 自定义图像光标(setCustomCursor)
当系统内置样式无法满足需求时(例如:设计软件中的画笔、取色器、缩放手柄等),可以使用 pointer.setCustomCursor 加载自定义的图像资源。此功能从 API version 15 开始支持。
核心代码示例:
javascript
import { image } from '@kit.ImageKit';
import { pointer } from '@kit.InputKit';
import { window } from '@kit.ArkUI';
// 1. 加载自定义光标资源(如 SVG 或 PNG)
getContext().resourceManager.getMediaContent($r("app.media.custom_cursor")).then((buffer) => {
const svgBuffer: ArrayBuffer = buffer.slice(0);
let imgSource: image.ImageSource = image.createImageSource(svgBuffer);
// 2. 设置光标的解码尺寸(最大限制为 256 x 256px)
let decodingOptions: image.DecodingOptions = { desiredSize: { width: 32, height: 32 } };
imgSource.createPixelMap(decodingOptions).then((pixelMap) => {
window.getLastWindow(getContext(), (error, win) => {
if (error) return;
let windowId = win.getWindowProperties().id;
// 3. 配置自定义光标(设置焦点坐标,即点击触发的精准位置)
let customCursor: pointer.CustomCursor = {
pixelMap: pixelMap,
focusX: 0, // 焦点水平坐标
focusY: 0 // 焦点垂直坐标
};
let config: pointer.CursorConfig = {
followSystem: false // 是否跟随系统设置调整大小
};
// 4. 应用自定义光标
pointer.setCustomCursor(windowId, customCursor, config).then(() => {
console.log('自定义光标设置成功');
});
});
});
});
三、 全局光标的显示与隐藏(setPointerVisible)
在某些沉浸式场景下(如全屏视频播放、3D 游戏或演示模式),需要完全隐藏鼠标光标。可以通过 setPointerVisible 接口进行全局控制。
核心代码示例:
javascript
import { pointer } from '@kit.InputKit';
// 进入全屏播放时隐藏光标
pointer.setPointerVisible(false, (error) => {
if (error) {
console.error(`隐藏光标失败: ${JSON.stringify(error)}`);
return;
}
console.log('光标已隐藏');
});
// 退出全屏时恢复显示
pointer.setPointerVisible(true, (error) => {
if (error) {
console.error(`显示光标失败: ${JSON.stringify(error)}`);
}
});
桌面级光标交互开发建议
- 及时恢复默认状态 :自定义光标通常具有极强的场景指向性。务必在组件的
onHover(false)回调中调用restoreDefault(),防止光标移出区域后仍保持特殊样式。 - 精准设置焦点(FocusX/FocusY) :在自定义图像光标时,务必根据图像内容合理设置
focusX和focusY。例如,准星光标的焦点应在正中心,而画笔光标的焦点应在笔尖处。 - 注意性能与资源释放 :自定义光标依赖
PixelMap,在页面销毁或光标不再使用时,应主动调用pixelMap.release()释放内存,避免内存泄漏。 - 状态变更时的重设机制:当应用窗口布局改变、页面跳转或光标移出再回到窗口时,系统可能会将光标切换回默认样式。开发者需要在相关生命周期或回调中重新设置自定义光标样式。
四、 多区域光标状态隔离与嵌套处理
在复杂的嵌套布局中(例如:地图应用中的地图区域、可拖拽的侧边栏、普通文本区),鼠标在不同区域移动时会频繁触发光标切换。为了防止状态混乱,建议将光标控制逻辑与组件的 onHover 深度绑定,并确保内层组件离开时能正确恢复外层状态。
核心代码示例:
javascript
Column() {
// 外层普通区域
Text('普通文本区域')
.onHover((isHover) => {
if (isHover) {
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.TEXT);
} else {
this.getUIContext().getCursorController().restoreDefault();
}
})
// 内层拖拽调整区域
Divider()
.height(4)
.onHover((isHover) => {
if (isHover) {
// 内层组件悬停时,覆盖外层光标
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.RESIZE_VERTICAL);
} else {
// 离开内层组件时,恢复默认或交由外层处理
this.getUIContext().getCursorController().restoreDefault();
}
})
}
五、 沉浸式场景下的光标自动隐藏与显现
在全屏视频播放、3D 渲染或演示模式下,光标长时间静止会遮挡视线。鸿蒙支持通过 setPointerVisible 结合定时器,实现"移动时显示,静止 N 秒后自动隐藏"的沉浸式体验。
核心代码示例:
javascript
import { pointer } from '@kit.InputKit';
@State private hideCursorTimer: number = -1;
// 在鼠标移动或悬停时触发
.onHover((isHover) => {
if (isHover && this.isFullScreen) {
// 1. 显示光标
pointer.setPointerVisible(true);
// 2. 清除上一次的隐藏定时器
if (this.hideCursorTimer !== -1) clearTimeout(this.hideCursorTimer);
// 3. 设置新的隐藏定时器(例如 3 秒无操作后隐藏)
this.hideCursorTimer = setTimeout(() => {
pointer.setPointerVisible(false);
}, 3000);
}
})
六、 动态调整自定义光标焦点(FocusX/FocusY)
在某些特殊交互中(如:根据缩放级别改变画笔粗细,或切换不同的取色器模式),光标的图像可能发生变化,此时必须同步更新 focusX 和 focusY,否则会导致"点击位置"与"视觉光标位置"发生偏移。
核心代码示例:
javascript
// 假设用户切换了画笔大小,需要重新加载光标
private async updateBrushCursor(size: number) {
const pixelMap = await this.generateBrushPixelMap(size); // 动态生成不同大小的画笔图像
const customCursor: pointer.CustomCursor = {
pixelMap: pixelMap,
focusX: size / 2, // 【关键】焦点始终保持在画笔正中心
focusY: size / 2
};
const config: pointer.CursorConfig = { followSystem: false };
const windowId = this.getWindowId();
await pointer.setCustomCursor(windowId, customCursor, config);
}
七、 构建全局光标状态机(CursorStateManager)
在大型 PC 应用中,如果在各个页面分散调用 setCursor 和 restoreDefault,极易导致光标状态残留(例如:从拖拽页面跳转到新页面,光标依然是拖拽手型)。建议封装一个全局的光标状态管理类,统一管理当前应用的光标上下文。
核心代码示例:
javascript
export class CursorStateManager {
private static currentStyle: pointer.PointerStyle = pointer.PointerStyle.DEFAULT;
private static uiContext: UIContext;
static init(context: UIContext) {
this.uiContext = context;
}
// 安全地设置光标
static set(style: pointer.PointerStyle) {
if (this.currentStyle !== style) {
this.currentStyle = style;
this.uiContext.getCursorController().setCursor(style);
}
}
// 安全地恢复默认光标
static restore() {
if (this.currentStyle !== pointer.PointerStyle.DEFAULT) {
this.currentStyle = pointer.PointerStyle.DEFAULT;
this.uiContext.getCursorController().restoreDefault();
}
}
}
// 在组件中使用,避免重复调用和状态错乱
Row()
.onHover((isHover) => {
if (isHover) CursorStateManager.set(pointer.PointerStyle.HAND);
else CursorStateManager.restore();
})
八、 结合鼠标事件与修饰键的联动控制
在 PC 端,光标样式往往需要与鼠标的按键状态(如按下左键)或键盘修饰键(如按住 Shift 或 Ctrl)联动。通过 onMouse 事件,可以捕获这些复合状态,从而动态切换光标。
核心代码示例:
javascript
import { MouseEvent, Action, Button } from '@kit.InputKit';
Rectangle()
.width(300).height(200)
.onMouse((event: MouseEvent) => {
// 当按下左键且按住 Shift 键时,切换为十字准星(常用于框选)
if (event.action === Action.BUTTON_DOWN && event.button === Button.LEFT) {
if (event.shiftKey) {
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.CROSS);
}
}
// 释放按键时恢复默认
else if (event.action === Action.BUTTON_UP) {
this.getUIContext().getCursorController().restoreDefault();
}
})
九、 拖拽交互的光标反馈(onDrag)
在文件管理器或看板应用中,当用户拖拽元素时,系统默认的拖拽反馈可能不够直观。可以在 onDragStart 时将光标切换为"抓取/禁止"状态,并在 onDragEnd 时恢复。
核心代码示例:
javascript
Column()
.onDragStart(() => {
// 拖拽开始时,切换为"移动/抓取"光标
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.MOVE);
return new DragItem('拖拽的数据');
})
.onDragEnd(() => {
// 拖拽结束时,务必恢复默认光标
this.getUIContext().getCursorController().restoreDefault();
})
十、 文本编辑区的光标与底层控制器联动
在富文本编辑器或代码编辑器中,除了视觉上的光标(Caret),还需要控制底层的输入光标(Pointer)。结合 API 23 新增的底层能力,可以实现更精细的文本交互。
核心代码示例:
javascript
// 当进入文本编辑模式时
TextInput({ controller: this.textController })
.onFocus(() => {
// 视觉指针切换为文本输入样式
this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.TEXT);
})
.onBlur(() => {
this.getUIContext().getCursorController().restoreDefault();
})
// 在自定义工具栏中,通过控制器直接操作底层光标(如模拟退格删除)
Button('删除前一字符')
.onClick(() => {
// 直接调用底层控制器,无需手动截取字符串,避免光标抖动
this.textController.deleteBackward();
})
十一、 游戏与 3D 场景的光标锁定(Pointer Lock)
在 3D 渲染、全景视频或第一人称游戏中,需要鼠标无限移动且不被屏幕边界限制。鸿蒙支持通过 setPointerLock 锁定光标,使其在物理鼠标移动时仅触发相对坐标变化,而不影响系统级绝对坐标。
核心代码示例:
javascript
// 进入 3D 场景时锁定光标
Button('进入沉浸模式')
.onClick(async () => {
try {
const windowClass = await window.getLastWindow(getContext(this));
// 锁定光标
await windowClass.setPointerLock(true);
console.info('光标已锁定,可自由旋转视角');
} catch (error) {
console.error('锁定光标失败', error);
}
})
// 退出沉浸模式时解锁
Button('退出沉浸模式')
.onClick(async () => {
const windowClass = await window.getLastWindow(getContext(this));
await windowClass.setPointerLock(false);
this.getUIContext().getCursorController().restoreDefault();
})
补充建议
- 光标与焦点的解耦:视觉光标(Pointer)代表"鼠标在哪",而焦点(Focus)代表"键盘输入给谁"。在 PC 开发中,不要将两者混淆。例如,鼠标悬停在按钮上改变了光标,但不应该自动让按钮获取键盘焦点(除非用户明确点击)。
- 全局状态兜底 :在 3D 场景或全屏应用中,如果应用意外崩溃或失去焦点(如
Alt+Tab切出),系统通常会自动解锁光标。但在应用重新激活(onForeground)时,务必检查并重置光标状态,防止出现"光标不可见"或"光标被锁死"的极端 Bug。 - 多窗口光标隔离 :在多窗口(如分屏、画中画)场景下,每个窗口拥有独立的
Window实例。自定义光标或锁定光标时,必须获取当前活动窗口的 ID 进行操作,避免误改其他后台窗口的光标状态。 - 性能与内存监控 :频繁切换自定义光标(如在列表中快速滑动时)会产生大量的
PixelMap创建与销毁开销。务必复用PixelMap对象,并在组件销毁(aboutToDisappear)时统一释放资源,防止内存泄漏。