《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 包括 RUNNINGCYCLINGDRIVING 等。
  • 适合场景:运动记录、驾驶模式切换、健康类应用。
  • 不适合:高频、低延迟的动作检测(比如抬手亮屏)。
对比项 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.json5requestPermissions 字段添加 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 负责生命周期挂载,避免页面切后台后回调空转。
  • 实际提示可以用 vibratorpromptAction.showToast 实现,这里只给出了日志。

踩坑记录:高频问题

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

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

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

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

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

现象 :将折叠屏折叠后,motionisMoving 一直为 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 操作放在 onPageShowonAppear 中。

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

Q:motionuserStatus 的回调是线程安全的吗?

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


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

相关推荐
G_dou_4 小时前
Flutter+OpenHarmony 实战:stopwatch 秒表应用
flutter·harmonyos
亚信安全官方账号4 小时前
AISTrustOne鸿蒙版安全方案 让终端防护“内生”力量觉醒
安全·华为·harmonyos
夜勤月5 小时前
HarmonyOS 6.0 ArkWeb实战:PDF背景色自定义功能全解析(附完整代码+避坑指南)
华为·pdf·harmonyos
想你依然心痛6 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“药界智脑“——PC端AI智能体沉浸式药物研发与分子模拟工作台
人工智能·华为·ar·harmonyos·智能体
G_dou_6 小时前
Flutter +OpenHarmony 实战:clock 时钟应用
flutter·harmonyos
G_dou_7 小时前
Flutter+OpenHarmony 实战:weather 天气查询应用
flutter·harmonyos
yuegu7777 小时前
HarmonyOS应用<节气通>开发第1篇:启动页开发——留下第一印象的2秒
harmonyos
川石课堂软件测试7 小时前
零基础小白如何学习自动化测试
python·功能测试·学习·测试工具·jmeter·压力测试·harmonyos