如何在Android N多窗口模式下确定设备的正确方向?

23
多窗口文档 中得知: 多窗口模式下被禁用的功能 某些功能在设备处于多窗口模式时会被禁用或忽略,因为它们对于可能与其他活动或应用程序共享设备屏幕的活动来说没有意义。这些功能包括:
  • 一些系统UI自定义选项被禁用;例如,如果应用程序没有运行在全屏模式下,则无法隐藏状态栏。
  • 系统会忽略android:screenOrientation属性的更改。
对于大多数应用程序而言,区分纵向和横向模式可能并不重要,但我正在开发一个包含相机视图的SDK,用户可以将其放置在他们希望的任何活动中,包括支持多窗口模式的活动。问题在于,相机视图包含SurfaceView/TextureView,用于显示相机预览,在所有活动方向上正确显示预览,需要了解正确的活动方向,以便可以正确旋转相机预览。
我的代码通过检查当前配置方向(纵向或横向)和当前屏幕旋转来计算正确的活动方向。问题在于,在多窗口模式下,当前配置方向不反映实际的活动方向。这导致相机预览被旋转90度,因为Android报告的配置与方向不同。
我目前的解决方法是检查请求的活动方向并以此为基础使用,但这有两个问题:
  1. 请求的活动方向不一定反映实际的活动方向(即请求可能仍未得到满足)
  2. 请求的活动方向可以是“后面”、“传感器”、“用户”等,这些并不透露任何有关当前活动方向的信息。
  3. 根据文档,在多窗口模式下实际上忽略屏幕方向,因此1和2将无法正常工作。
是否有一种稳健的方法可以在多窗口配置中计算正确的活动方向?
以下是我目前使用的代码(请参见注释以了解存在问题的部分):
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问题跟踪器报告了此问题。


请注意,isInMultiWindowMode()存在竞争条件,这加剧了问题。 - CommonsWare
为了完整起见,我在这里添加了我们SDK的问题报告(https://github.com/PDF417/pdf417-android/issues/19),其中还包含屏幕截图和一些关于该主题的讨论。 - DoDo
你可以查看屏幕尺寸吗?getMetrics():“如果从非Activity上下文请求,则度量将报告基于当前旋转和减去系统装饰区域的整个显示器的大小。” 然后比较宽度和高度。 - natario
@natario,我会尝试这样做 - 但我不确定如果宽度大于高度是否可以得出活动一定是横向的结论。我首先要检查这种方法在多窗口模式下返回什么。无论如何,还是谢谢! - DoDo
1
请不要关注活动本身,而是手机。活动存在于它自己的窗口中,你知道现在它可以根据拖动位置是横屏还是竖屏。然而,通过查看手机尺寸,你应该能够确定它当前是横屏还是竖屏,而这并不取决于拖动位置。如果这对你有帮助,请参考。 - natario
@natario,你的解决方案可行。非常感谢。请将解决方案作为答案提供(而不是评论),这样我就会接受它,你也可以从CommonsWare收集声望奖励。 - DoDo
2个回答

6
我不知道这是否应该被视为解决方案或只是一种解决方法。
正如您所说,您的问题出现在Android N及其多窗口模式中。当应用程序处于多窗口模式时,您的Activity未绑定到完整的显示尺寸。这重新定义了Activity方向的概念。引用Ian Lake

事实证明:“纵向”真的只意味着高度大于宽度,“横向”意味着宽度大于高度。因此,如果考虑到这个定义,您的应用程序在调整大小时可以从一个方向过渡到另一个方向,这是有意义的。

因此,Activity方向更改和设备物理旋转之间不再有任何链接。(我认为现在唯一合理使用Activity方向更改的原因是更新资源。

如果您对设备尺寸感兴趣,只需获取其DisplayMetrics即可。引用文档所述,

如果从非Activity上下文请求,则度量将报告基于当前旋转的整个显示器的大小,并减去系统装饰区域。

因此解决方案是:

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() 分支,因为:

  • 它不会增加太多开销
  • 我们的假设同样适用于非多窗口模式
  • 它将很可能适用于任何未来的其他模式
  • 你可以避免 isInMultiWindowMode() 由CommonsWare描述的竞争条件

这对于默认方向为横屏的平板电脑(例如三星Tab SM-P600)完全是错误的。您不能仅仅依赖widthPixelsheightPixels来给出当前方向下的准确值。 - IgorGanapolsky
@Igor,宽度和高度应该是设备无关的,也就是说,默认方向不应该影响它们。我没有测试过,但我相信DoDo已经测试过了。 - natario
@natario 看看你的代码:boolean portrait = height >= width; 这很重要。 - IgorGanapolsky
@DoDo可能会回答你。我认为指标的宽度和高度不取决于默认方向。 - natario
1
@Igor 是的,标题是错误的。OP 询问如何获取设备方向,而不考虑当前活动的方向(因为它们现在不一定相同)。 - natario
显示剩余8条评论

1
我认为你可以利用加速度计来检测“下方”位置,从而确定手机的方向。工程师小哥讲解 这就是手机本身的工作方式。
我在这里搜索了一种方法,并找到了这个答案。基本上,你需要检查三个加速计中哪一个检测到重力拉力最显著的分量,你知道在地球附近大约是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) {
    }
};  

请注意,这段代码可能在某些情况下无法正常工作,因为您需要考虑像在停止/加速的火车上或在游戏中快速移动手机等场景 - 这里是维基百科上数量级页面的链接,以帮助您入门。看起来您可以放心使用 Khalil 在他的代码(在他的回答中)中提供的值,但我建议您要更加谨慎,并研究在不同情况下可能生成的值。

这不是一个完美的想法,但我认为只要 API 构建方式不允许您获取其计算出的方向,我认为这是一个有益的解决方法。


不错的想法,但对我来说,计算“向下”并不是问题(我可以通过 getDisplayOrientation 间接获取)- 问题在于检测“活动对我的视图进行了什么转换,以便我可以抵消它”。问题在于当活动旋转时,它也会旋转所有的视图,包括显示相机预览的视图。因此,我需要知道 Android 所做的确切旋转,以便在相机预览视图本身内部进行本地旋转来抵消它。 - DoDo
1
为了更好地展示问题 - 想象一下,您的代码从传感器信息中正确地得出设备处于横向模式,因为用户实际上将其保持在横向模式。但是,用户已在设置中禁用了屏幕旋转,并且多窗口活动实际上是以纵向模式显示的。将相机预览显示为横向模式是错误的,因为它会创建相机预览的不正确旋转。这正是多窗口功能引入的问题。 - DoDo
是的,我明白你的意思。你说得对,我的解决方案不会有帮助。 - et_l
这段代码毫无意义:if((X_Axis <= 6 && X_Axis >= -6) - IgorGanapolsky
为什么这没有意义呢?你可以用另一种形式来写,就像这样:if((Math.abs(X_Axis)<=6) - et_l

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接