8、姿态判断:如何检测手机是平放、竖放还是横放?

好,咱们继续往下走。上一章我们聊了怎么用加速度传感器算角度,这一章我们聊聊更实际的问题——怎么判断手机当前是平放、竖放还是横放?

说实话,这个需求在项目中太常见了。我做过一个拍照应用,用户横着拿手机时自动切换到横屏取景,竖着拿就切回竖屏。还有一次做导航软件,需要检测手机是不是平放在车里,然后自动切换到HUD模式。嗯,这些场景都离不开姿态判断。

8.1 姿态判断的核心思路

说白了,姿态判断就是根据加速度传感器三轴的数据,推断手机在空间中的朝向。你想想看,手机平放时,Z轴基本指向重力方向;竖放时,Y轴指向重力方向;横放时,X轴指向重力方向。

所以核心就一句话:看哪个轴的重力分量最大

我个人习惯把这个问题拆成两步:

  1. 计算各轴与重力方向的夹角(上一章的内容)
  2. 根据夹角大小判断姿态(本章重点)

关键点:姿态判断不需要精确的角度值,只需要知道哪个轴最接近重力方向即可。所以我们可以用阈值法,简单又高效。

8.2 三种基本姿态的定义

我们先明确一下三种基本姿态:

姿态 特征 典型场景
平放 Z轴接近重力方向(±g) 手机放在桌上、车载支架
竖放 Y轴接近重力方向(±g) 正常手持打电话、刷抖音
横放 X轴接近重力方向(±g) 横屏玩游戏、看视频

注意,这里说的「接近」不是完全等于,因为用户不可能拿得那么精准。所以我们需要设定一个阈值范围。

8.3 阈值怎么定?

我在项目中踩过这个坑。一开始我设的阈值太严格,比如要求Z轴分量大于9.5 m/s²才算平放。结果用户稍微倾斜一点,系统就判断不出来了。后来我放宽到7.0 m/s²,效果好了很多。

这里给出一组我常用的经验值:

  • 平放:|Z| > 7.0 m/s²,且 |X| < 4.0,|Y| < 4.0
  • 竖放:|Y| > 7.0 m/s²,且 |X| < 4.0,|Z| < 4.0
  • 横放:|X| > 7.0 m/s²,且 |Y| < 4.0,|Z| < 4.0

小技巧:阈值可以根据你的应用场景调整。比如做游戏的话,可以设松一点,让切换更灵敏;做测量工具的话,设紧一点,避免误判。

8.4 代码实现

下面是我写的一个简单判断函数。嗯,代码不长,但很实用:

public enum PhoneOrientation {
    FLAT,       // 平放
    PORTRAIT,   // 竖放
    LANDSCAPE,  // 横放
    UNKNOWN     // 不确定
}

public PhoneOrientation detectOrientation(float[] gravity) {
    float x = gravity[0];
    float y = gravity[1];
    float z = gravity[2];

    float absX = Math.abs(x);
    float absY = Math.abs(y);
    float absZ = Math.abs(z);

    // 阈值:主轴分量 > 7.0,其他轴 < 4.0
    final float MAIN_THRESHOLD = 7.0f;
    final float SIDE_THRESHOLD = 4.0f;

    if (absZ > MAIN_THRESHOLD && absX < SIDE_THRESHOLD && absY < SIDE_THRESHOLD) {
        return PhoneOrientation.FLAT;
    } else if (absY > MAIN_THRESHOLD && absX < SIDE_THRESHOLD && absZ < SIDE_THRESHOLD) {
        return PhoneOrientation.PORTRAIT;
    } else if (absX > MAIN_THRESHOLD && absY < SIDE_THRESHOLD && absZ < SIDE_THRESHOLD) {
        return PhoneOrientation.LANDSCAPE;
    } else {
        return PhoneOrientation.UNKNOWN;
    }
}

这段代码的逻辑很直白:先取三个轴的绝对值,然后比较哪个轴最大。如果最大的那个超过阈值,且另外两个轴都很小,就判定为对应的姿态。

注意:这里用的是重力数据(经过低通滤波后的值),不是原始传感器数据。原始数据噪声太大,直接判断会频繁误触发。

8.5 更精细的判断:正放还是倒放?

有时候我们还需要知道手机是正着放还是倒着放。比如竖放时,是屏幕朝上(正常手持)还是屏幕朝下(扣在桌上)?

这个其实很简单——看主轴的符号:

  • 竖放时,如果Y > 0,说明手机正放(屏幕朝上);Y < 0 则是倒放(屏幕朝下)
  • 横放时,如果X > 0,说明右侧朝上;X < 0 则是左侧朝上
  • 平放时,如果Z > 0,说明屏幕朝上;Z < 0 则是屏幕朝下

我曾经做过一个计步器应用,需要检测用户是不是把手机放在裤兜里(通常是竖放且屏幕朝向身体)。当时就用这个符号判断来过滤掉一些误触发。

8.6 姿态判断的流程图

下面我用一张SVG图把整个判断流程串起来,方便你理解:

姿态判断流程图 获取重力数据 计算三轴绝对值:|X|, |Y|, |Z| 哪个轴分量最大且超过阈值? |Z|最大 平放 (FLAT) |Y|最大 竖放 (PORTRAIT) |X|最大 横放 (LANDSCAPE) 输出姿态结果

8.7 避坑指南

最后分享几个我踩过的坑:

  • 不要直接用原始数据:传感器数据抖动很大,一定要先做低通滤波。我之前偷懒没做,结果手机放桌上不动,姿态却在平放和竖放之间来回跳。
  • 注意坐标系方向:不同手机厂商的坐标系可能不一样。虽然Android有标准定义,但有些定制ROM会改。建议在真机上测试一下各轴的正负方向。
  • 阈值要留余量:用户不可能把手机拿得绝对水平或垂直。阈值设得太死,用户体验会很差。我一般留30%的余量。
  • 考虑过渡状态:手机从平放到竖放的过程中,会有一段「未知」状态。不要急着切换,加个延时或者滞回逻辑会更平滑。

我的习惯:在实际项目中,我会再加一个「接近平放」的状态(比如|Z| > 5.0),这样在用户从桌上拿起手机的过程中,系统能提前准备,切换更流畅。

好了,关于姿态判断就聊这么多。核心就是抓住「哪个轴最接近重力方向」这个关键点,配合合理的阈值,就能做出稳定可靠的判断。下一章我们聊聊更高级的话题——如何用加速度传感器做步数检测。


公众号:蓝海资料掘金营,微信deep3321