10、抬手亮屏:检测抬手动作的算法思路
抬手亮屏这个功能,大家应该都不陌生。手机从桌上拿起来,屏幕自动就亮了。这个体验很自然,但背后的算法其实挺有意思的。我当年第一次做这个功能时,以为很简单——不就是检测一个向上的加速度嘛。结果踩了不少坑,今天跟你聊聊我的经验。
抬手动作的物理特征
先想想看,抬手这个动作到底发生了什么?
你把手机从桌面拿起来,大致经历这么几个阶段:
- 静止状态:手机平放在桌上,Z轴加速度约等于 -9.8 m/s²(重力向下)
- 拿起瞬间:手给手机一个向上的力,Z轴加速度突然变大(正值)
- 翻转/倾斜:手机从水平变为竖直,X轴和Y轴的角度发生变化
- 稳定状态:手机被举到眼前,基本静止,但姿态变了
说白了,抬手动作的核心特征就是:先有一个向上的加速脉冲,然后伴随角度变化。
关键点:抬手亮屏不是检测「手抬起来了」,而是检测「手机从水平放置变成了竖直握持」这个状态变化。
算法思路一:基于加速度阈值
最简单的做法,就是盯着Z轴加速度看。当Z轴加速度超过某个阈值时,认为发生了抬手动作。
// 伪代码:基于阈值的抬手检测
float threshold = 3.0f; // m/s²,经验值
float zAccel = sensorEvent.values[2];
if (zAccel > threshold) {
// 检测到抬手动作
wakeUpScreen();
}
这个方案的问题很明显——太容易误触了。你走路时手机在口袋里晃一下,Z轴可能就超了。我早期项目里用过这个方案,结果用户反馈说「手机在口袋里自己亮了又灭」,体验很差。
注意:单纯用加速度阈值,误触率极高。不建议在生产环境中直接使用。
算法思路二:结合角度变化
既然只靠加速度不行,那就加上角度判断。抬手时,手机从水平变为竖直,这个角度变化是很有特征的。
我们可以用加速度计的数据计算出手机的倾斜角度:
// 计算俯仰角(Pitch)和横滚角(Roll)
float pitch = (float) Math.atan2(-accelX, Math.sqrt(accelY * accelY + accelZ * accelZ));
float roll = (float) Math.atan2(accelY, accelZ);
// 判断手机是否从水平变为竖直
boolean isHorizontal = Math.abs(pitch) < 0.3f; // 接近水平
boolean isVertical = Math.abs(pitch) > 1.0f; // 接近竖直
完整的抬手检测逻辑可以这样:
- 记录上一帧的手机姿态(水平/竖直)
- 检测当前帧是否有向上的加速度脉冲
- 检测手机是否从水平变为竖直
- 两个条件同时满足,才判定为抬手
我的经验:阈值不要写死。不同用户抬手习惯不一样,有人喜欢从桌上拿起来看,有人喜欢从床上翻个身看。我建议把阈值做成可配置的,或者根据用户使用习惯动态调整。
算法思路三:状态机模型
如果你想让检测更鲁棒,可以用状态机。把抬手过程拆成几个阶段,每个阶段有明确的进入和退出条件。
我画了一张状态机图,你看看就明白了:
状态机的优势在于:你可以精确控制每个阶段的超时时间。比如「加速中」这个状态,如果1秒内没有进入「翻转中」,就自动回到「静止」。这样可以过滤掉很多随机抖动。
算法思路四:结合陀螺仪
如果你手头有陀螺仪数据,那检测精度还能再上一个台阶。陀螺仪测量的是角速度,抬手时手机绕X轴或Y轴旋转的角速度会有一个明显的峰值。
// 结合陀螺仪的抬手检测
float gyroX = sensorEvent.values[0]; // 绕X轴角速度
float gyroY = sensorEvent.values[1]; // 绕Y轴角速度
// 计算合成角速度
float angularSpeed = (float) Math.sqrt(gyroX * gyroX + gyroY * gyroY);
// 抬手时角速度通常在 1.0 ~ 3.0 rad/s 之间
if (angularSpeed > 0.8f && angularSpeed < 5.0f) {
// 可能是抬手动作
}
为什么加上限?因为正常抬手不会转得飞快。如果角速度超过5 rad/s,那大概率是用户甩了一下手机,不是抬手。
推荐方案:加速度计 + 陀螺仪 + 状态机,三者结合。加速度计检测线性运动,陀螺仪检测旋转运动,状态机做决策。这是目前主流手机厂商的做法。
避坑指南
我踩过的坑,列出来给你参考:
- 传感器采样率别设太低:抬手动作也就几百毫秒,采样率低于50Hz基本抓不到特征。我建议用SENSOR_DELAY_GAME(约50Hz)或更高。
- 注意坐标系变化:手机横屏竖屏时,加速度计的坐标系是跟着转的。你写代码时一定要搞清楚当前设备方向。
- 别在锁屏状态下频繁唤醒CPU:抬手检测需要常开传感器,但不要每帧都去算角度。可以用硬件FIFO或者批处理模式,降低功耗。
- 考虑用户正在使用手机的情况:如果屏幕已经亮着,就别再触发抬手检测了。这个逻辑看似简单,但我见过不少App在这里翻车。
代码实现要点
最后给一段实际可用的核心逻辑,你感受一下:
public class RaiseToWakeDetector implements SensorEventListener {
private static final float ACCEL_THRESHOLD = 2.5f; // m/s²
private static final float ANGLE_THRESHOLD = 0.8f; // 弧度
private static final long TIMEOUT_MS = 800; // 毫秒
private boolean isHorizontal = true;
private long lastAccelTime = 0;
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
// 计算俯仰角
float pitch = (float) Math.atan2(-x, Math.sqrt(y*y + z*z));
// 检测向上加速
boolean hasAccel = z > ACCEL_THRESHOLD;
// 检测角度变化
boolean nowHorizontal = Math.abs(pitch) < 0.3f;
boolean nowVertical = Math.abs(pitch) > ANGLE_THRESHOLD;
if (hasAccel && isHorizontal && nowVertical) {
long now = System.currentTimeMillis();
if (now - lastAccelTime > TIMEOUT_MS) {
// 真正的抬手动作
triggerWakeUp();
lastAccelTime = now;
}
}
isHorizontal = nowHorizontal;
}
}
}
这段代码我简化了一些边界处理,但核心逻辑都在了。你实际用的时候,记得加上低通滤波,把传感器的高频噪声滤掉。
嗯,抬手亮屏这个功能,说难不难,说简单也不简单。关键是把物理特征理解透,然后用合适的算法去匹配。希望今天的分享对你有帮助。
公众号:蓝海资料掘金营,微信deep3321