
这两个 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('设备已静止');
}
};
最佳实践
-
不要在
build()中注册回调。build()在每次状态变更时都会执行,导致on被反复调用,不仅浪费性能,还会触发重复订阅导致回调重复执行。需要把on操作放在onPageShow或onAppear中。 -
userStatus的取消时机比订阅更重要。 很多开发者只记得on,忘记off。结果页面已经销毁,但系统还在轮询传感器并推送给已销毁的组件,造成内存泄漏和空指针异常。建议在aboutToDisappear中统一清理所有订阅。 -
运动模拟器无法测试
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。
如果你也遇到类似问题,可以重点检查生命周期和状态同步逻辑。官方文档对这个行为描述得比较简单,建议结合实际运行效果一起验证。不同设备上的行为可能存在差异,建议真机测试。