10、抬手亮屏:检测抬手动作的算法思路

抬手亮屏这个功能,大家应该都不陌生。手机从桌上拿起来,屏幕自动就亮了。这个体验很自然,但背后的算法其实挺有意思的。我当年第一次做这个功能时,以为很简单——不就是检测一个向上的加速度嘛。结果踩了不少坑,今天跟你聊聊我的经验。

抬手动作的物理特征

先想想看,抬手这个动作到底发生了什么?

你把手机从桌面拿起来,大致经历这么几个阶段:

  1. 静止状态:手机平放在桌上,Z轴加速度约等于 -9.8 m/s²(重力向下)
  2. 拿起瞬间:手给手机一个向上的力,Z轴加速度突然变大(正值)
  3. 翻转/倾斜:手机从水平变为竖直,X轴和Y轴的角度发生变化
  4. 稳定状态:手机被举到眼前,基本静止,但姿态变了

说白了,抬手动作的核心特征就是:先有一个向上的加速脉冲,然后伴随角度变化

关键点:抬手亮屏不是检测「手抬起来了」,而是检测「手机从水平放置变成了竖直握持」这个状态变化。

算法思路一:基于加速度阈值

最简单的做法,就是盯着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. 记录上一帧的手机姿态(水平/竖直)
  2. 检测当前帧是否有向上的加速度脉冲
  3. 检测手机是否从水平变为竖直
  4. 两个条件同时满足,才判定为抬手

我的经验:阈值不要写死。不同用户抬手习惯不一样,有人喜欢从桌上拿起来看,有人喜欢从床上翻个身看。我建议把阈值做成可配置的,或者根据用户使用习惯动态调整。

算法思路三:状态机模型

如果你想让检测更鲁棒,可以用状态机。把抬手过程拆成几个阶段,每个阶段有明确的进入和退出条件。

我画了一张状态机图,你看看就明白了:

静止 水平放置 加速中 Z轴 > 阈值 翻转中 角度变化 亮屏 触发成功 Z轴突增 角度变化 稳定在竖直 超时/放下 误触回退

状态机的优势在于:你可以精确控制每个阶段的超时时间。比如「加速中」这个状态,如果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