《HarmonyOS技术精讲》二:用户动作与状态感知实战

这两个 API 的定位,很多人一开始就搞反了

HarmonyOS NEXT 的多模态融合感知服务(Multimodal Awareness Kit )里,有两组看起来很像的 API:motion(用户动作)和 userStatus(用户状态)。官方文档把它们的应用场景写得比较聚焦,但实际项目里很多开发者会把它们混用。

一个常见的误判是:想监听用户"静止",就用 userStatus;想监听用户"跑步",就用 motion。这其实反了。

motion 判断的是 设备本身的运动状态 ------手机是不是在移动、是不是静止。它不关心人是谁,也不关心运动模式,它只输出"动"或"不动"。而 userStatus 判断的是 用户的活动类型 ------跑步、骑行、开车。它依赖的传感器更多,结果也更抽象,需要常驻或低频轮询。

如果项目只需要知道"用户有没有在走动",用 motion 就够了,功耗低、延迟少。但如果你想知道"用户是不是在跑步",就必须走 userStatus。这两个 API 的分工和用法完全不同,但可以组合起来用。


它俩各自解决了什么问题

Motion:设备级运动检测

  • 能力 :通过 motionDetect 事件订阅设备动作状态,参数 MotionType 包括 MOTION_TYPE_STILL(静止)、MOTION_TYPE_MOVEMENT(移动)。
  • 适合场景:屏幕休眠后设备晃动唤醒、防误触、运动检测辅助定位。
  • 不适合:识别用户具体在做什么(如跑步、骑行)。

UserStatus:用户活动状态识别

  • 能力 :通过 statusChange 事件订阅用户状态变化,参数 UserStatus 包括 RUNNING、CYCLING、DRIVING 等。
  • 适合场景:运动记录、驾驶模式切换、健康类应用。
  • 不适合:高频、低延迟的动作检测(比如抬手亮屏)。
对比项 Motion UserStatus
输入传感器 加速度计为主 加速度计 + 陀螺仪 + GPS
状态粒度 动静二态 多种活动分类
功耗 较低 较高(常驻轮询)
回调频率 较高 较低
依赖权限 无 ohos.permission.ACTIVITY_MOTION

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(建议真机测试,模拟器不支持部分传感器)

核心实现:两套感知模块的订阅与取消

1. Motion:设备动作感知

这部分代码用于订阅设备是否处于移动状态。重点在于 MotionType 的取值和 on/off 的生命周期管理。

typescript 复制代码
import { motion, MotionType } from '@kit.MultimodalAwarenessKit';

let callback: Callback<boolean> = (isMoving: boolean) => {
  console.info(`设备运动状态变更: ${isMoving}`);
  // isMoving = true 表示设备正在移动
};

// 订阅
try {
  motion.on('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, callback);
} catch (err) {
  console.error('motion on error: ' + JSON.stringify(err));
}

// 取消订阅(推荐在页面 onPageHide 或生命周期销毁时调用)
try {
  motion.off('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, callback);
} catch (err) {
  console.error('motion off error: ' + JSON.stringify(err));
}

注意事项:

  • MotionType 还有一个 MOTION_TYPE_STILL,效果与 MOVEMENT 相反。不需要同时订阅两种,isMoving = false 就是静止。
  • 取消订阅时,callback 参数一定要和订阅时传入的是同一个函数引用,否则取消不生效。
  • motion 不涉及权限声明,系统默认开启。

2. UserStatus:用户活动状态感知

这部分代码用于监听用户活动状态的切换,比如从"静止"变为"跑步"。

typescript 复制代码
import { userStatus, UserStatus } from '@kit.MultimodalAwarenessKit';
import { common } from '@kit.AbilityKit';

// 获取上下文,用于动态请求权限
let context: common.UIAbilityContext = getContext(this);

// 动态申请权限(仅首次启动时调用即可)
context.requestPermissionsFromUser(['ohos.permission.ACTIVITY_MOTION']).then(() => {
  console.info('权限授权成功');
}).catch((err: Error) => {
  console.error('权限授权失败: ' + err.message);
});

let statusCallback: Callback<UserStatus> = (newStatus: UserStatus) => {
  switch (newStatus) {
    case UserStatus.RUNNING:
      console.info('用户状态变为:跑步');
      break;
    case UserStatus.CYCLING:
      console.info('用户状态变为:骑行');
      break;
    case UserStatus.DRIVING:
      console.info('用户状态变为:驾驶');
      break;
    default:
      console.info('用户状态变为:其他');
      break;
  }
};

// 订阅
try {
  userStatus.on('statusChange', statusCallback);
} catch (err) {
  console.error('userStatus on error: ' + JSON.stringify(err));
}

// 取消订阅
try {
  userStatus.off('statusChange', statusCallback);
} catch (err) {
  console.error('userStatus off error: ' + JSON.stringify(err));
}

关键点:

  • 务必在 module.json5 的 requestPermissions 字段添加 ohos.permission.ACTIVITY_MOTION,否则动态权限申请会走不通。
  • UserStatus 回调不是实时变化,系统会有一个死区(约5-10秒)防止抖动。测试时不要指望毫秒级响应。
  • 不要在构造函数或 aboutToAppear 里直接传入 this.statusCallback,容易出现回调未绑定的情况。建议在 onPageShow 中注册。

3. 综合场景:检测到用户跑步时触发提示

这个场景把两套 API 组合起来:先通过 motion 判断设备是否在移动,再通过 userStatus 判断具体活动类型。

typescript 复制代码
@Entry
@Component
struct MainPage {
  @State lastStatus: UserStatus = UserStatus.STILL;

  aboutToDisappear(): void {
    // 页面销毁时清理
    motion.off('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, this.motionCallback);
    userStatus.off('statusChange', this.statusCallback);
  }

  // 注意:必须用箭头函数或 bind 确保 this 指向
  private motionCallback: Callback<boolean> = (isMoving: boolean) => {
    if (!isMoving) {
      // 设备静止时,清理 userStatus,节省功耗
      userStatus.off('statusChange', this.statusCallback);
      return;
    }
    // 设备移动时,订阅 userStatus
    try {
      // 先取消旧的,避免重复订阅
      userStatus.off('statusChange', this.statusCallback);
      userStatus.on('statusChange', this.statusCallback);
    } catch (err) {
      console.error('userStatus on failed: ' + JSON.stringify(err));
    }
  };

  private statusCallback: Callback<UserStatus> = (newStatus: UserStatus) => {
    if (newStatus === UserStatus.RUNNING) {
      // 触发提示:可能是震动、弹窗或语音提示
      console.info('检测到用户正在跑步');
      // 实际项目中,可以在这里调用 Vibration 或 promptAction 接口
    }
  };

  build() {
    Column() {
      Text('用户状态监测中...')
        .fontSize(20)
        .align(Alignment.Center)
    }
    .width('100%')
    .height('100%')
    .onPageShow(() => {
      // 页面显示时订阅 motion
      try {
        motion.on('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, this.motionCallback);
      } catch (err) {
        console.error('motion on failed: ' + JSON.stringify(err));
      }
    })
  }
}

设计思路:

  • 用 motion 作为"门控":只有设备移动时,才去订阅高功耗的 userStatus。静止时关闭,节省电量。
  • onPageShow / aboutToDisappear 负责生命周期挂载,避免页面切后台后回调空转。
  • 实际提示可以用 vibrator 或 promptAction.showToast 实现,这里只给出了日志。

踩坑记录:高频问题

问题 1:权限申请成功,但 userStatus 回调从未触发

现象 :动态授权已走通,module.json5 也添加了权限,但回调就是不走。

原因 :HarmonyOS 的 ACTIVITY_MOTION 权限是"用户运动状态"权限,但系统对 userStatus 的生效有一个延迟窗口。首次授权后,需要等待约 30 秒到 1 分钟,系统才会开始推送数据。很多开发者以为授权后立即就有回调,结果等不到就放弃了。

解决方案 :在授权后,延迟一段时间再订阅;或者先 on 一次,然后保持页面不销毁,等阈值时间后再观察。真机测试最好在运动状态下保持 2 分钟。

问题 2:motion 在折叠屏上表现异常

现象 :将折叠屏折叠后,motion 的 isMoving 一直为 true,无法静止。

原因 :折叠屏的铰链动作会导致加速度计数据抖动,系统可能会把折叠动作误判为移动。官方在 API 18 之后对折叠屏设备有特殊处理,但如果 motion 的采样频率设置不当,依然容易误报。

解决方案 :推荐在上层加一个二次过滤------连续 5 次 isMoving = false 才认为静止。可以在回调里累积计数,而不是单次判断。

typescript 复制代码
private stillCount: number = 0;

private motionCallback: Callback<boolean> = (isMoving: boolean) => {
  if (isMoving) {
    stillCount = 0;
    // 确实在动
    return;
  }
  stillCount++;
  if (stillCount >= 5) {
    // 确认静止
    console.info('设备已静止');
  }
};

最佳实践

  1. 不要在 build() 中注册回调。 build() 在每次状态变更时都会执行,导致 on 被反复调用,不仅浪费性能,还会触发重复订阅导致回调重复执行。需要把 on 操作放在 onPageShow 或 onAppear 中。

  2. userStatus 的取消时机比订阅更重要。 很多开发者只记得 on,忘记 off。结果页面已经销毁,但系统还在轮询传感器并推送给已销毁的组件,造成内存泄漏和空指针异常。建议在 aboutToDisappear 中统一清理所有订阅。

  3. 运动模拟器无法测试 userStatus。 即使你在模拟器里模拟"走路",userStatus 也不会生效。它依赖真实的 GPS 和陀螺仪数据,模拟器只能输出假数据。测试时必须使用真机,并且让手机处于运动状态(可以握在手里走路)。


完整入口文件

typescript 复制代码
// pages/Index.ets
import { motion, userStatus, MotionType, UserStatus } from '@kit.MultimodalAwarenessKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct MotionDemo {
  aboutToDisappear(): void {
    motion.off('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, this.motionCallback);
    userStatus.off('statusChange', this.statusCallback);
  }

  private motionCallback: Callback<boolean> = (isMoving: boolean) => {
    if (!isMoving) {
      userStatus.off('statusChange', this.statusCallback);
      return;
    }
    userStatus.off('statusChange', this.statusCallback);
    userStatus.on('statusChange', this.statusCallback);
  };

  private statusCallback: Callback<UserStatus> = (newStatus: UserStatus) => {
    if (newStatus === UserStatus.RUNNING) {
      console.info('用户正跑步');
    }
  };

  build() {
    Column() {
      Text('用户动作与状态监测')
        .fontSize(24)
        .padding(20);
    }
    .width('100%')
    .height('100%')
    .onPageShow(() => {
      // 动态申请权限
      let context: common.UIAbilityContext = getContext(this);
      context.requestPermissionsFromUser(['ohos.permission.ACTIVITY_MOTION']).then(() => {
        try {
          motion.on('motionDetect', MotionType.MOTION_TYPE_MOVEMENT, this.motionCallback);
        } catch (err) {
          console.error('motion on failed: ' + JSON.stringify(err));
        }
      }).catch((err: Error) => {
        console.error('权限授权失败: ' + err.message);
      });
    })
  }
}

FAQ

Q:为什么真机可以,模拟器上 userStatus 一直不生效?

A:模拟器不提供真实的传感器数据,它只模拟加速度计和陀螺仪的基本输出,但不做用户活动分类。UserStatus 依赖的多模态融合算法在模拟器中不存在。 唯一解法是真机测试。

Q:为什么页面返回后,状态检测依然在运行?

A:因为你没有在 aboutToDisappear 或 onPageHide 中调用 off()。页面虽然不可见,但 userStatus 的回调仍然存活,会一直收到数据。如果回调里引用了 @State 变量,还会造成尝试更新已销毁组件的 Warning。

Q:motion 和 userStatus 的回调是线程安全的吗?

A:是的,这两个回调都在系统线程中执行,但你不能在里面直接修改 @State。官方文档没写这点,但实测如果直接更新 @State,会导致 ArkUI 的渲染线程和传感器线程争锁,出现 UI 卡顿。建议在回调里使用 Observable 对象或通过 updateState 方式延迟更新 UI。


如果你也遇到类似问题,可以重点检查生命周期和状态同步逻辑。官方文档对这个行为描述得比较简单,建议结合实际运行效果一起验证。不同设备上的行为可能存在差异,建议真机测试。

相关推荐
传奇开心果编程9 分钟前
【ArkUI进阶练中学】第19课:AI安全与隐私治理
学习·ui·华为·harmonyos
李游Leo42 分钟前
HarmonyOS 7 QuickDock 闪控窗开发实录 06:floatView × 回归验收:25轮场景回归、资源基线与发布前收口【鸿蒙心迹】
回归·kotlin·harmonyos
李游Leo44 分钟前
HarmonyOS 7 DualCart 平行视界适配实录 06:Navigation × 多窗口回归:路由冲突、恢复一致性与性能验收【鸿蒙心迹】
回归·kotlin·harmonyos
传奇开心果编程1 小时前
【ArkUI进阶练中学】第18课:Agent亲和架构与应用智能化改造
学习·ui·华为·harmonyos
李游Leo1 小时前
HarmonyOS 7 QuickDock 闪控窗开发实录 02:floatView × floatingBall:形态切换、位置恢复与单一状态源【鸿蒙心迹】
java·华为·harmonyos
李游Leo1 小时前
HarmonyOS 7 Spatial Recon Kit 开发实录 03:重建进度、暂停恢复与前后台状态机【鸿蒙心迹】
华为·harmonyos
李游Leo1 小时前
HarmonyOS 7 QuickDock 闪控窗开发实录 03:floatView × ArkData:自由拖动、侧边暂存与位置持久化【鸿蒙心迹】
华为·harmonyos
resh_people15 小时前
开源鸿蒙平台 KMP_CMP 三方库「Kermit」适配全流程
华为·开源·harmonyos
李游Leo16 小时前
HarmonyOS 7 EasyGo + ArkUI Navigation:平行视界主副页筛选事务与跨栏撤销一致性【鸿蒙心迹】
华为·harmonyos
HwJack2017 小时前
【共创稿事节】HarmonyOS 7空间应用的可用性测试和体验评估小方法分享
华为·harmonyos·可用性测试