引言

想一想你的 HarmonyOS NEXT 设备——它可能连接了蓝牙键盘、鼠标、触控笔,甚至游戏手柄。你的闹钟 App 想检测用户是否在操作设备来决定是否响铃;你的外设管理工具想列出所有连接的输入设备并显示详情;你的辅助功能应用想感知键盘的插拔来切换输入模式。

这些场景都有一个共同的需求:发现、查询和监听输入设备。HarmonyOS NEXT 的 @ohos.multimodalInput.inputDevice 模块正是为此而生的。

本文将通过"输入设备实验室"Demo,深入讲解输入设备管理的核心 API:设备列表获取、设备详情查询、热插拔事件监听、键盘类型识别、空闲时间检测。值得注意的是,inputDevice 属于 @kit.InputKit,这也是本系列首次离开 @kit.BasicServicesKit 的舒适区,进入输入子系统的领域。

@ohos.multimodalInput.inputDevice 模块概述

@ohos.multimodalInput.inputDevice 是 HarmonyOS NEXT 多模输入子系统中的设备管理模块。它不负责输入事件的传递(那是 @ohos.multimodalInput.inputMonitor 的职责),而是专注于设备元数据查询和生命周期感知——设备有哪些、每个设备的属性是什么、设备何时接入/移除、用户多久没操作了。

导入方式

import inputDevice from '@ohos.multimodalInput.inputDevice';

模块属于 @kit.InputKit,不是 @kit.BasicServicesKit。这是理解鸿蒙 Kit 划分的好例子:窗口、WiFi、电池、温控、电源都属于基础系统服务(BasicServicesKit),而输入设备属于专门的输入 Kit。

API 总览

API返回值类型说明API 版本
getDeviceList()Promise<Array<number>>异步获取所有设备 ID 列表API 9+
getDeviceInfo(deviceId)Promise<InputDeviceData>异步获取指定设备详细信息API 9+
getDeviceInfoSync(deviceId)InputDeviceData同步同步获取设备信息API 10+
on('change', listener)void订阅监听设备热插拔事件API 9+
off('change', listener?)void取消取消监听热插拔事件API 9+
getKeyboardType(deviceId)Promise<KeyboardType>异步获取键盘类型API 9+
getKeyboardTypeSync(deviceId)KeyboardType同步同步获取键盘类型API 10+
supportKeys(deviceId, keys)Promise<Array<boolean>>异步检测按键支持API 9+
supportKeysSync(deviceId, keys)Array<boolean>同步同步检测按键支持API 10+
getIntervalSinceLastInput()Promise<number>异步距上次输入的时间间隔(ms)API 14+

关键数据类型

SourceType — 输入源类型(联合类型字符串),不是枚举:

type SourceType = 'keyboard' | 'mouse' | 'touchpad' |
                  'touchscreen' | 'joystick' | 'trackball';

KeyboardType — 键盘类型枚举:

枚举值数值含义
NONE0无键盘
UNKNOWN1未知类型键盘
ALPHABETIC_KEYBOARD2全键盘(QWERTY 等)
DIGITAL_KEYBOARD3数字小键盘
HANDWRITING_PEN4手写笔
REMOTE_CONTROL5遥控器

InputDeviceData — 设备详细信息的完整接口:

interface InputDeviceData {
  id: number;          // 设备唯一 ID(重连可能变化)
  name: string;        // 设备名称,如 "HUAWEI Mouse"
  sources: Array<SourceType>;  // 支持的输入源(可多个)
  axisRanges: Array<AxisRange>; // 轴信息
  bus: number;         // 总线类型
  product: number;     // 产品 ID
  vendor: number;      // 厂商 ID
  version: number;     // 版本号
  phys: string;        // 物理路径
  uniq: string;        // 唯一标识符
  isVirtual?: boolean; // 是否虚拟设备(API 23+)
  isLocal?: boolean;   // 是否本地设备(API 23+)
}

DeviceListener — 热插拔事件:

interface DeviceListener {
  type: ChangedType;   // 'add' | 'remove'
  deviceId: number;    // 发生变化的设备 ID
}

核心 API 详解

1. getDeviceList() — 枚举所有设备

function getDeviceList(): Promise<Array<number>>;

这是探索输入设备的起点——先获取所有设备的 ID 列表,再逐个查询详情。

inputDevice.getDeviceList()
  .then((ids: Array<number>) => {
    console.log('发现 ' + ids.length + ' 个输入设备');
    // ids = [1, 2, 3, ...]
    // 每个 ID 对应一个物理或虚拟输入设备
  })
  .catch((err: Error) => {
    console.error('获取设备列表失败: ' + err.message);
  });

注意getDeviceList() 返回的是 Promise,这意味着获取设备列表是一个异步操作——系统需要查询底层输入子系统来枚举所有设备。在设备热插拔时,返回的列表会反映当前最新的设备状态。

旧版 API getDeviceIds()(API 8)已被废弃,始终使用 getDeviceList()

2. getDeviceInfo / getDeviceInfoSync — 设备详情查询

function getDeviceInfo(deviceId: number): Promise<InputDeviceData>;
function getDeviceInfoSync(deviceId: number): InputDeviceData;

Promise 版本和同步版本提供相同的数据,区别在于调用方式。对于已经获取到的 ID 列表,用同步版本逐个查询更简洁:

// 推荐:先用 getDeviceList 获取 ID,再用 getDeviceInfoSync 逐个查询
inputDevice.getDeviceList()
  .then((ids: Array<number>) => {
    for (let i = 0; i < ids.length; i++) {
      try {
        const info: inputDevice.InputDeviceData =
          inputDevice.getDeviceInfoSync(ids[i]);
        console.log(info.name);          // "HUAWEI Mouse"
        console.log(info.sources);       // ['mouse']
        console.log(info.vendor);        // 厂商 ID
        console.log(info.product);       // 产品 ID
        console.log(info.phys);          // 物理路径
      } catch (e) {
        console.error('获取设备 ' + ids[i] + ' 信息失败');
      }
    }
  });

InputDeviceData 中几个值得关注的字段:

  • sources:这是一个数组——一个物理设备可以有多个输入源。例如带触控板的键盘会同时报告 ['keyboard', 'touchpad']
  • vendor / product:USB-IF 分配的厂商和产品 ID,可以用来识别具体的外设型号。
  • phys:Linux 内核的设备物理路径,如 "usb-xhci-hcd.0.auto-1/input0"
  • bus:总线类型的数值编码,如 USB=3、Bluetooth=5 等。
  • isVirtual(API 23+):标识是否为虚拟输入设备(如远程桌面注入的输入)。

3. on(‘change’) / off(‘change’) — 热插拔监听

function on(type: 'change', listener: Callback<DeviceListener>): void;
function off(type: 'change', listener?: Callback<DeviceListener>): void;

这是 inputDevice 模块最"活"的 API——它让应用能实时感知外设的接入和移除。当用户插入 USB 键盘、连接蓝牙鼠标、或拔出触控笔时,监听器会立即收到事件。

// 注册监听器
const listener: Callback<inputDevice.DeviceListener> =
  (data: inputDevice.DeviceListener) => {
    if (data.type === 'add') {
      console.log('设备接入: ID=' + data.deviceId);
      // 可以调用 getDeviceInfoSync 查询新设备详情
    } else {
      console.log('设备移除: ID=' + data.deviceId);
    }
  };

inputDevice.on('change', listener);

// 取消监听(页面销毁时必须调用)
inputDevice.off('change', listener);

重要提示

  1. off()listener 参数是可选的——不传则取消该事件的所有监听器。但如果要精确取消某一个监听器,必须传入 on() 时使用的同一个函数引用(不是内容相同的匿名函数)。这是 Callback 模式的核心规则。

  2. 生命周期管理是必须的:在页面的 aboutToDisappear() 中调用 off(),否则页面销毁后监听器仍在后台运行,既浪费资源又可能导致内存泄漏。

  3. 热插拔事件中的 deviceId 是新设备的 ID,可以立即用 getDeviceInfoSync(deviceId) 获取详情。

4. getKeyboardType / getKeyboardTypeSync — 键盘类型识别

function getKeyboardType(deviceId: number): Promise<KeyboardType>;
function getKeyboardTypeSync(deviceId: number): KeyboardType;

对于键盘类设备,可以进一步查询其具体类型:

try {
  const kt: inputDevice.KeyboardType =
    inputDevice.getKeyboardTypeSync(deviceId);
  if (kt === inputDevice.KeyboardType.ALPHABETIC_KEYBOARD) {
    console.log('全键盘 — 可使用快捷键');
  } else if (kt === inputDevice.KeyboardType.DIGITAL_KEYBOARD) {
    console.log('数字键盘 — 仅数字输入');
  }
} catch (e) {
  // 非键盘设备可能抛异常
  console.log('非键盘设备');
}

对于非键盘设备(如纯鼠标),getKeyboardTypeSync() 可能抛出异常。因此在实际使用中应该用 try-catch 包裹,或先用 sources 判断设备是否包含 'keyboard' 源。

5. getIntervalSinceLastInput() — 空闲检测

function getIntervalSinceLastInput(): Promise<number>;

这是 API 14 新增的功能——返回自上次用户输入事件(键盘按键、屏幕触摸、鼠标点击等)以来经过的毫秒数。对于需要根据用户活跃度调整行为的场景非常有用:

inputDevice.getIntervalSinceLastInput()
  .then((ms: number) => {
    const seconds: number = Math.floor(ms / 1000);
    if (seconds < 60) {
      console.log('用户 ' + seconds + ' 秒前有操作');
    } else if (seconds < 3600) {
      console.log('用户已空闲 ' + Math.floor(seconds / 60) + ' 分钟');
    } else {
      console.log('用户已空闲超过 1 小时');
    }
  });

注意,这个 API 计算的是距上次用户输入事件的时间,不是"用户离开屏幕"的时间。它包括了设备休眠时间——如果设备休眠了 10 分钟后被唤醒,返回的值会超过 10 分钟。

6. supportKeys / supportKeysSync — 按键支持检测

function supportKeys(deviceId: number, keys: Array<KeyCode>): Promise<Array<boolean>>;
function supportKeysSync(deviceId: number, keys: Array<KeyCode>): Array<boolean>;

用于查询某个输入设备是否支持特定的按键码。一次最多查询 5 个按键。这个 API 在适配特殊键盘布局时非常有用——例如确认外接键盘是否有媒体控制键、功能键等。
在这里插入图片描述
在这里插入图片描述

实战:输入设备实验室

页面设计

Demo 的整体布局如下:

┌─────────────────────────────────┐
│  < 输入设备实验室  @kit.InputKit  │  ← 标题栏(#EC4899 粉红)
├─────────────────────────────────┤
│  已连接设备                      │
│  ┌─────────────────────────────┐│
│  │ HUAWEI Mouse               ││  ← 可点击展开的卡片
│  │ [鼠标]                  ▼  ││
│  │ ─────────────────────────  ││
│  │ 设备 ID: 1                  ││  ← 展开后的详细信息
│  │ 键盘类型: 非键盘设备         ││
│  │ 轴数量: 3 个                ││
│  │ 总线: 0x3                  ││
│  └─────────────────────────────┘│
│  ┌─────────────────────────────┐│
│  │ HUAWEI Keyboard            ││
│  │ [键盘]                  ▼  ││
│  └─────────────────────────────┘│
├─────────────────────────────────┤
│  [重新扫描] [热插拔监听]          │  ← 操作按钮
├─────────────────────────────────┤
│  空闲检测                        │
│  ┌─────────────────────────────┐│
│  │ 距上次输入事件        42 秒  ││
│  │ [检测空闲时间]              ││
│  └─────────────────────────────┘│
├─────────────────────────────────┤
│  事件日志                        │
│  ┌─────────────────────────────┐│
│  │ 14:32:05  发现 3 个输入设备  ││
│  │ 14:32:01  正在扫描输入设备.. ││
│  └─────────────────────────────┘│
└─────────────────────────────────┘

核心实现

设备数据模型
interface DeviceItem {
  id: number;
  name: string;
  sourceLabels: string[];  // 中文标签数组,如 ['键盘', '触控板']
  bus: number;
  product: number;
  vendor: number;
  version: number;
  phys: string;
  keyboardType: string;    // 键盘类型中文描述
  axisCount: number;       // 轴数量
}

注意 sourceLabelsstring[] 而非 string——因为一个物理设备可以有多个输入源(如带触控板的键盘),用数组存储可以在 UI 中渲染为多个彩色标签。这是从 API 的 Array<SourceType> 到 UI 的 string[] 的映射层。

源类型映射

SourceType 是英文字符串,在 UI 中需要映射为中文标签:

private sourceLabel(s: string): string {
  if (s === 'keyboard') { return '键盘'; }
  if (s === 'mouse') { return '鼠标'; }
  if (s === 'touchpad') { return '触控板'; }
  if (s === 'touchscreen') { return '触摸屏'; }
  if (s === 'joystick') { return '手柄'; }
  if (s === 'trackball') { return '轨迹球'; }
  return s;
}

每种设备类型在 UI 中都有不同的颜色标识——键盘用天蓝 #0EA5E9,鼠标用翠绿 #10B981,触控板用靛蓝 #6366F1,触摸屏用橙色 #F97316,手柄用紫罗兰 #8B5CF6,轨迹球用粉红 #EC4899。这种色彩区分让用户一眼就能识别设备类型。

设备扫描流程
private loadDevices(): void {
  this.addLog('正在扫描输入设备...');
  inputDevice.getDeviceList()
    .then((ids: Array<number>) => {
      const items: DeviceItem[] = [];
      for (let i = 0; i < ids.length; i++) {
        try {
          // 用同步 API 逐个获取设备信息
          const info = inputDevice.getDeviceInfoSync(ids[i]);
          // 映射 SourceType → 中文标签
          const labels: string[] = [];
          for (let j = 0; j < info.sources.length; j++) {
            labels.push(this.sourceLabel(info.sources[j]));
          }
          // 尝试获取键盘类型
          let kbType: string = '';
          try {
            kbType = this.keyboardTypeLabel(
              inputDevice.getKeyboardTypeSync(ids[i]));
          } catch (e) {
            kbType = '非键盘设备';
          }
          items.push({
            id: info.id,
            name: info.name,
            sourceLabels: labels,
            bus: info.bus,
            product: info.product,
            vendor: info.vendor,
            version: info.version,
            phys: info.phys,
            keyboardType: kbType,
            axisCount: info.axisRanges.length
          });
        } catch (e) {
          this.addLog('获取设备 ' + ids[i] + ' 信息失败');
        }
      }
      this.devices = items;
      this.loading = false;
      this.addLog('发现 ' + ids.length + ' 个输入设备');
    });
}

这里的模式值得注意:异步获取列表 + 同步获取详情getDeviceList() 是 Promise,因为它需要查询系统服务;而拿到 ID 后,getDeviceInfoSync() 可以直接从内核接口读取数据,无需再次异步。这种组合方式比"全部 Promise"更简洁,避免了嵌套 then() 的问题。

热插拔监听
private changeListener: Callback<inputDevice.DeviceListener> | null = null;

private startListen(): void {
  this.changeListener = (data: inputDevice.DeviceListener) => {
    const action: string = data.type === 'add' ? '插入' : '移除';
    this.addLog('设备' + action + ': ID=' + data.deviceId);
    this.loadDevices(); // 刷新设备列表
  };
  inputDevice.on('change', this.changeListener);
  this.listening = true;
  this.addLog('开始监听设备热插拔');
}

private stopListen(): void {
  if (this.changeListener !== null) {
    inputDevice.off('change', this.changeListener);
    this.changeListener = null;
  }
  this.listening = false;
}

这里的设计与上一篇温控实验室的 Callback 模式相同:将 listener 引用保存在类属性中,在 aboutToDisappear() 中调用 stopListen() 清理。off() 传入的 listener 必须是 on() 时注册的同一个函数引用。

空闲时间检测
private checkIdle(): void {
  this.addLog('正在查询空闲时间...');
  inputDevice.getIntervalSinceLastInput()
    .then((ms: number) => {
      const sec: number = Math.floor(ms / 1000);
      if (sec < 60) {
        this.idleTime = sec + ' 秒';
      } else if (sec < 3600) {
        this.idleTime = Math.floor(sec / 60) + ' 分 ' +
          (sec % 60) + ' 秒';
      } else {
        this.idleTime = Math.floor(sec / 3600) + ' 时 ' +
          Math.floor((sec % 3600) / 60) + ' 分';
      }
      this.addLog('距上次输入: ' + this.idleTime);
    })
    .catch((err: Error) => {
      this.idleTime = '获取失败';
      this.addLog('查询失败: ' + err.message);
    });
}

getIntervalSinceLastInput() 返回的是距离用户最后一次输入事件的时间。这里的"输入事件"是广义的——包括触摸屏幕、点击鼠标、按下键盘等所有用户主动触发的输入。与 @ohos.powerisActive()(亮屏)和 isStandby()(待机)不同,空闲检测更精细——用户可能在亮屏状态下闲置(例如阅读长文),也可能在待机状态下通过后台音乐保持活跃。

可展开的设备卡片

Demo 中的设备卡片是可展开的——点击卡片会在折叠和展开之间切换。展开后显示 InputDeviceData 的全部字段:

@Builder
deviceCard(item: DeviceItem) {
  Column() {
    Row() {
      Column() {
        Text(item.name)
          .fontSize(14).fontColor('#0F172A')
          .fontWeight(FontWeight.Medium)
        Row() {
          ForEach(item.sourceLabels, (s: string) => {
            Text(s)
              .fontSize(9).fontColor(this.sourceColor(s))
              .backgroundColor(this.sourceBgColor(s))
              .borderRadius(4)
          })
        }
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)

      Text(this.expandedId === item.id ? '▲' : '▼')
        .fontSize(10).fontColor('#94A3B8')
    }
    .width('100%')

    if (this.expandedId === item.id) {
      Divider().height(0.5).color('#E2E8F0')
      this.detailRow('设备 ID', item.id.toString())
      this.detailRow('键盘类型', item.keyboardType)
      this.detailRow('轴数量', item.axisCount + ' 个')
      this.detailRow('总线', '0x' + item.bus.toString(16))
      this.detailRow('产品 ID', '0x' + item.product.toString(16))
      this.detailRow('厂商 ID', '0x' + item.vendor.toString(16))
      this.detailRow('版本', item.version.toString())
      this.detailRow('物理地址', item.phys)
    }
  }
  .width('100%').padding({ top: 8, bottom: 8 })
  .onClick(() => { this.toggleExpand(item.id); })
}

展开/折叠的状态管理很简单——用一个 expandedId 状态变量记录当前展开的设备 ID。点击设备时,如果是同一个则折叠(设为 -1),如果是另一个则切换展开目标。

没有权限要求

inputDevice 模块的一个显著特点是:所有查询和监听 API 都不需要权限getDeviceList()getDeviceInfo()on('change')getKeyboardType()getIntervalSinceLastInput() 全部无需声明任何权限即可使用。

唯一的例外是 setFunctionKeyEnabled()(API 15+),它需要 ohos.permission.INPUT_KEYBOARD_CONTROLLER 权限——但这个 API 仅限输入法应用使用,普通应用不需要关心。

这使得 inputDevice 成为最容易集成到任何应用中的系统 API——不需要申请权限,不需要用户授权弹窗,开箱即用。

@kit.InputKit 与 Kit 体系

本系列前六篇全部使用 @kit.BasicServicesKit 中的模块(window、wifiManager、batteryInfo、thermal、power、runningLock)。inputDevice 是第一个来自 @kit.InputKit 的模块。

HarmonyOS NEXT 将系统能力划分为不同的 Kit,每个 Kit 包含一组相关的能力模块:

Kit包含的模块(示例)领域
@kit.BasicServicesKitwindow, wifiManager, batteryInfo, thermal, power, runningLock, sensor 等基础系统服务
@kit.InputKitinputDevice, inputMonitor, inputConsumer, keyCode, keyEvent 等输入子系统
@kit.ArkUI所有 UI 组件、router、动画用户界面
@kit.TelephonyKitsim, radio, call, sms 等电话通信

理解 Kit 的划分有助于高效查阅文档——当你需要输入相关的能力时,去 @kit.InputKit 而非 @kit.BasicServicesKit 中寻找。这种模块化设计也意味着不同 Kit 有不同的 SystemCapability,应用可以在 module.json5 中精确声明所需的能力。

五种异步模式的回顾

截至目前,本系列已经遇到了五种 API 调用模式:

模式代表模块特征适用场景
同步常量batteryInfo, power直接读属性,零开销状态查询
Promise 链wifiManager, window.then().catch()需系统服务响应
Callback 订阅thermal, inputDeviceon(ev, cb) / off(ev, cb)持续事件流
Promise 创建 + 同步操作runningLockcreate() 返回 Promise,后续同步资源获取后持续操作
Promise + Sync 双轨inputDevice每个查询 API 同时提供 Promise 和 Sync 版本灵活选择调用方式

第五种模式——Promise + Sync 双轨——是 inputDevice 的特色。API 9 引入了 Promise 版本(getDeviceListgetDeviceInfo),API 10 又补充了 Sync 版本(getDeviceInfoSyncgetKeyboardTypeSyncsupportKeysSync)。这种设计给了开发者最大的灵活性:

  • getDeviceList()(Promise)获取动态列表——因为设备数量可能随时变化,Promise 保证拿到最新快照
  • getDeviceInfoSync()(Sync)获取静态详情——因为设备信息在短时间内不变,同步调用更简洁
  • on('change')(Callback)持续监听——因为这是一条事件流,不是一次性查询

三种异步模式在同一个模块中协同工作,体现了 HarmonyOS NEXT API 设计上的成熟。

实际应用场景

场景 1:外设管理工具

在设备的"设置 → 蓝牙"或"设置 → 外接设备"页面中,通常会列出所有已连接的输入设备:

// 获取设备列表用于设置页面
async function loadDeviceList(): Promise<DeviceItem[]> {
  const ids = await inputDevice.getDeviceList();
  const items: DeviceItem[] = [];
  for (const id of ids) {
    const info = inputDevice.getDeviceInfoSync(id);
    items.push({ id: info.id, name: info.name,
      sources: info.sources });
  }
  return items;
}

配合 on('change'),当用户插拔设备时列表可以实时更新。

场景 2:辅助功能 — 输入模式自适应

对于辅助功能应用,当检测到物理键盘接入时自动切换输入模式:

inputDevice.on('change', (data: inputDevice.DeviceListener) => {
  if (data.type === 'add') {
    const info = inputDevice.getDeviceInfoSync(data.deviceId);
    if (info.sources.includes('keyboard')) {
      // 切换到键盘导航模式
      switchToKeyboardMode();
    } else if (info.sources.includes('mouse')) {
      // 启用鼠标指针增强
      enableMousePointerEnhancement();
    }
  } else if (data.type === 'remove') {
    // 检查是否还有键盘连接
    checkAndFallbackToTouchMode();
  }
});

场景 3:防误触与自动锁屏

利用 getIntervalSinceLastInput() 检测用户空闲时长,结合运行锁实现智能锁屏策略:

async function shouldAutoLock(): Promise<boolean> {
  const idleMs = await inputDevice.getIntervalSinceLastInput();
  // 如果用户 5 分钟无操作,且界面不涉及导航/播放等场景
  return idleMs > 5 * 60 * 1000 && !isActiveTaskRunning();
}

这比简单的定时器更精确——它不是从页面打开开始计时,而是从用户最后一次操作开始计时。

场景 4:游戏手柄适配

游戏应用检测手柄的接入,自动切换控制方案:

inputDevice.on('change', (data: inputDevice.DeviceListener) => {
  if (data.type === 'add') {
    const info = inputDevice.getDeviceInfoSync(data.deviceId);
    if (info.sources.includes('joystick')) {
      showGamepadIndicator();
      loadGamepadControlScheme();
    }
  }
});

配合 supportKeys() 可以进一步检测手柄是否支持振动、陀螺仪等高级特性。

注意事项

  1. 设备 ID 不持久:同一物理设备在重新插拔后 ID 可能变化。不要将 deviceId 持久化存储,应该每次查询时重新获取。

  2. 不要假设设备存在:在模拟器上,getDeviceList() 返回的结果很有限(通常只有触摸屏)。getDeviceInfoSync() 对不存在的 deviceId 会抛异常,始终用 try-catch 包裹。

  3. 注册的监听器必须在 aboutToDisappear() 中移除:这是所有 Callback 模式 API 的通用规则。页面销毁时如果不 off(),监听器变成悬空引用,不仅无法触发 UI 更新,还会浪费系统资源。

  4. getIntervalSinceLastInput() 包含休眠时间:如果设备休眠了 2 小时,返回的不是"用户亮屏后的 5 秒",而是 7200000ms+(2 小时+)。这在判断"用户是否刚操作过"时需要特别注意。

  5. Sync 方法虽方便,但要理解适用边界:Sync 方法读取的是调用时刻的快照,不会触发系统服务的实时查询。对于动态变化的设备列表,始终用 getDeviceList() Promise 版本获取最新状态。

  6. getKeyboardTypeSync() 对非键盘设备可能抛异常:不是返回 KeyboardType.NONE,而是直接抛异常。这是因为非键盘设备根本没有"键盘类型"这个概念。在调用前先用 sources 判断设备类型会更安全。

  7. supportKeys() 最多 5 个按键:这是单次查询的限制,需要查询更多按键时可以分批调用。

完整代码

import { router } from '@kit.ArkUI';
import inputDevice from '@ohos.multimodalInput.inputDevice';
import { FontSize, Spacing } from '../common/Constants';

interface DeviceItem {
  id: number;
  name: string;
  sourceLabels: string[];
  bus: number;
  product: number;
  vendor: number;
  version: number;
  phys: string;
  keyboardType: string;
  axisCount: number;
}

@Entry
@Component
struct InputDeviceLabPage {
  @State devices: DeviceItem[] = [];
  @State expandedId: number = -1;
  @State listening: boolean = false;
  @State eventLogs: ChangeEvent[] = [];
  @State idleTime: string = '--';
  @State loading: boolean = true;

  private changeListener: Callback<inputDevice.DeviceListener> | null = null;

  aboutToAppear(): void { this.loadDevices(); }
  aboutToDisappear(): void { this.stopListen(); }

  private loadDevices(): void {
    inputDevice.getDeviceList()
      .then((ids: Array<number>) => {
        const items: DeviceItem[] = [];
        for (let i = 0; i < ids.length; i++) {
          try {
            const info = inputDevice.getDeviceInfoSync(ids[i]);
            // 构建中文标签 + 键盘类型 …
            items.push({ /* … */ });
          } catch (e) { /* … */ }
        }
        this.devices = items;
        this.loading = false;
      });
  }

  // … 完整代码见项目源码
}

总结

本文深入讲解了 HarmonyOS NEXT 中 @ohos.multimodalInput.inputDevice 模块的设计思路和实战应用:

  1. 设备发现getDeviceList() Promise 获取所有设备 ID,getDeviceInfoSync() 同步获取详情——Promise + Sync 组合使用。

  2. 热插拔感知on('change') / off('change') Callback 模式实时感知外设接入和移除,是应用响应设备变化的核心机制。

  3. 类型识别:通过 SourceType 判断设备类型(键盘/鼠标/触控板/触摸屏/手柄/轨迹球),通过 getKeyboardType() 进一步细分键盘类别。

  4. 空闲检测getIntervalSinceLastInput() 精细判断用户活跃度,为自动锁屏、省电策略、辅助功能提供数据支持。

  5. 无权限门槛:所有查询和监听 API 无需权限,降低了集成成本。仅 setFunctionKeyEnabled() 需要 INPUT_KEYBOARD_CONTROLLER 权限。

  6. 首个 @kit.InputKit 模块:本系列首次走出 @kit.BasicServicesKit,进入输入子系统。理解 Kit 划分有助于在正确的范围内查找 API。

  7. 第五种异步模式——Promise + Sync 双轨:同一模块同时提供 Promise 和 Sync 版本的查询 API,让开发者根据场景灵活选择调用方式。

掌握 @ohos.multimodalInput.inputDevice,你就能构建出感知输入设备变化、自适应不同外设、精细化管理用户交互体验的应用——无论是在外设管理页面、辅助功能设置,还是游戏手柄适配等场景中,这层能力都是不可或缺的底座。


Logo

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

更多推荐