如何在 Android N multi-window 模式下确定正确的设备方向?

How to determine correct device orientation in Android N multi-window mode?

来自Multi-Window documentation

Disabled features in multi-window mode

Certain features are disabled or ignored when a device is in multi-window mode, because they don’t make sense for an activity which may be sharing the device screen with other activities or apps. Such features include:

  • Some System UI customization options are disabled; for example, apps cannot hide the status bar if they are not running in full-screen mode.
  • The system ignores changes to the android:screenOrientation attribute.

我知道对于大多数应用程序来说,区分纵向和横向模式没有意义,但是我正在开发包含相机视图的 SDK,用户可以将其放在他们希望的任何 activity 上 - 包括activity表示支持多window模式。问题是相机视图包含显示相机预览的 SurfaceView/TextureView,为了在所有 activity 方向上正确显示预览,需要有关正确 activity 方向的知识,以便相机预览可以正确旋转。

问题是我的代码通过检查当前配置方向(纵向或横向)和当前屏幕旋转来计算正确的 activity 方向。问题是在 multi-window 模式下,当前配置方向并不反映真正的 activity 方向。这会导致相机预览旋转 90 度,因为 Android 报告的配置与方向不同。

我目前的解决方法是检查请求的 activity 方向并将其用作基础,但这样做有两个问题:

  1. 请求的 activity 方向不必反映实际 activity 方向(即请求可能仍未满足)
  2. 请求的 activity 方向可以是 'behind'、'sensor'、'user' 等,这不会透露有关当前 activity 方向的任何信息。
  3. 根据 documentation,屏幕方向实际上在 multi-window 模式下被忽略,所以 1. 和 2. 将不起作用

即使在多 window 配置中,是否有任何方法可以稳健地计算出正确的 activity 方向?

这是我目前使用的代码(请参阅有问题的部分的评论):

protected int calculateHostScreenOrientation() {
    int hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    int rotation = getDisplayOrientation(wm);

    boolean activityInPortrait;
    if ( !isInMultiWindowMode() ) {
        activityInPortrait = (mConfigurationOrientation == Configuration.ORIENTATION_PORTRAIT);
    } else {
        // in multi-window mode configuration orientation can be landscape even if activity is actually in portrait and vice versa
        // Try determining from requested orientation (not entirely correct, because the requested orientation does not have to
        // be the same as actual orientation (when they differ, this means that OS will soon rotate activity into requested orientation)
        // Also not correct because, according to https://developer.android.com/guide/topics/ui/multi-window.html#running this orientation
        // is actually ignored.
        int requestedOrientation = getHostActivity().getRequestedOrientation();
        if ( requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT ) {
            activityInPortrait = true;
        } else if ( requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE ) {
            activityInPortrait = false;
        } else {
            // what to do when requested orientation is 'behind', 'sensor', 'user', etc. ?!?
            activityInPortrait = true; // just guess
        }
    }

    if ( activityInPortrait ) {
        Log.d(this, "Activity is in portrait");
        if (rotation == Surface.ROTATION_0) {
            Log.d(this, "Screen orientation is 0");
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
        } else if (rotation == Surface.ROTATION_180) {
            Log.d(this, "Screen orientation is 180");
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
        } else if (rotation == Surface.ROTATION_270) {
            Log.d(this, "Screen orientation is 270");
            // natural display rotation is landscape (tablet)
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
        } else {
            Log.d(this, "Screen orientation is 90");
            // natural display rotation is landscape (tablet)
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
        }
    } else {
        Log.d(this, "Activity is in landscape");
        if (rotation == Surface.ROTATION_90) {
            Log.d(this, "Screen orientation is 90");
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
        } else if (rotation == Surface.ROTATION_270) {
            Log.d(this, "Screen orientation is 270");
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
        } else if (rotation == Surface.ROTATION_0) {
            Log.d(this, "Screen orientation is 0");
            // natural display rotation is landscape (tablet)
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
        } else {
            Log.d(this, "Screen orientation is 180");
            // natural display rotation is landscape (tablet)
            hostScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
        }
    }
    return hostScreenOrientation;
}

private int getDisplayOrientation(WindowManager wm) {
    if (DeviceManager.getSdkVersion() < 8) {
        return wm.getDefaultDisplay().getOrientation();
    }

    return wm.getDefaultDisplay().getRotation();
}

private boolean isInMultiWindowMode() {
    return Build.VERSION.SDK_INT >= 24 && getHostActivity().isInMultiWindowMode();
}

protected Activity getHostActivity() {
    Context context = getContext();
    while (context instanceof ContextWrapper) {
        if (context instanceof Activity) {
            return (Activity) context;
        }
        context = ((ContextWrapper) context).getBaseContext();
    }
    return null;
}

编辑:我也向 Android issue tracker 报告了此事。

我认为您可以利用加速度计来检测 "down" 的位置 - 从而检测 phone 的方向。 The Engineer Guy explains 这就是 phone 本身的方式。

我在 SO 上搜索了一种方法并找到了 。基本上,您需要检查 3 个加速度计中的哪一个检测到最重要的引力分量,您知道它在地球地面附近大约为 9.8m/s²。这是它的代码片段:

private boolean isLandscape;

mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mSensorManager.registerListener(mSensorListener,     mSensorManager.getDefaultSensor(
                                      Sensor.TYPE_ACCELEROMETER),1000000);
private final SensorEventListener mSensorListener = new SensorEventListener() { 
    @Override 
    public void onSensorChanged(SensorEvent mSensorEvent) {   
        float X_Axis = mSensorEvent.values[0]; 
        float Y_Axis = mSensorEvent.values[1]; 

        if((X_Axis <= 6 && X_Axis >= -6) && Y_Axis > 5){
        isLandscape = false; 
        }
        else if(X_Axis >= 6 || X_Axis <= -6){
        isLandscape = true;
        }

    }

    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }
};  

请注意,此代码可能并非在所有情况下都适用,因为您需要考虑乘坐 stopping/accelerating 火车或在游戏中快速移动 phone 等场景 - 这是orders of magnitude page on wiki 让你开始。 Khalil 在他的代码中(在他的回答中)输入的值看起来很安全,但我会格外小心并研究在不同情况下可能产生的值。

这不是一个完美的想法,但我认为只要 API 是按原样构建的 - 不允许您获得它们的计算方向 - 我认为这是一个有益的解决方法。

我不知道这应该被视为解决方案还是只是一种解决方法。

正如您所说,您的问题来自 Android N 及其多 window 模式。当应用程序处于 multi window 时,您的 Activity 未绑定到完整显示尺寸。这重新定义了Activity方向的概念。引用 Ian Lake:

Turns out: “portrait” really just means the height is greater than the width and “landscape” means the width is greater than the height. So it certainly makes sense, with that definition in mind, that your app could transition from one to the other while being resized.

因此 Activity 方向改变和设备物理旋转之间不再存在 link。 (我认为现在唯一合理使用Activity方向变化就是更新你的资源。

由于您对设备尺寸感兴趣,just get its DisplayMetrics。引用文档,

If requested from non-Activity context metrics will report the size of the entire display based on current rotation and with subtracted system decoration areas.

所以解决方案是:

final Context app = context.getApplicationContext();
WindowManager manager = (WindowManager) app.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
boolean portrait = height >= width;

当设备倾斜时,宽度和高度值将交换(或多或少)。

如果这行得通,我会亲自 运行 每次都删除 isInMultiWindowMode() 分支,因为

  • 不贵
  • 我们的假设也符合非多window模式
  • 它可能会很好地与任何其他未来类型的 模式
  • 您避免了 isInMultiWindowMode() described by CommonsWare
  • 的竞争条件