Как определить правильную ориентацию устройства в многооконном режиме Android N?

Из многооконной документации:

Отключенные функции в многооконном режиме

Некоторые функции отключены или игнорируются, когда устройство находится в многооконном режиме, потому что они не имеют смысла для действия, которое может делиться экраном устройства с другими действиями или приложениями. Такие функции включают в себя:

  • Некоторые параметры пользовательского интерфейса системы отключены; например, приложения не могут скрыть строку состояния, если они не работают в полноэкранном режиме.
  • Система игнорирует изменения атрибута 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.

2 ответа

Решение

Я не знаю, должно ли это рассматриваться как решение или просто обходной путь.

Как вы говорите, ваши проблемы связаны с Android N и его многооконным режимом. Когда приложение находится в мультиокне, ваш Activity не привязан к полному размеру дисплея. Это переопределяет концепцию Activity ориентации. Цитирую Яна Лейка:

Оказывается: "портрет" действительно означает, что высота больше ширины, а "пейзаж" означает, что ширина больше высоты. Таким образом, с учетом этого определения, безусловно, имеет смысл, что ваше приложение может переходить из одного в другое при изменении его размера.

Так что больше нет никакой связи между Activity изменение ориентации и физическое вращение устройства. (Я думаю, что единственное разумное использование изменений ориентации на активность сейчас - это обновление ваших ресурсов.)

Поскольку вас интересуют размеры устройства, просто получите егоDisplayMetrics, Цитирование документов,

Если запрошено из неактивных контекстных метрик, будет сообщаться размер всего дисплея на основе текущего поворота и с вычтенными областями декорирования системы.

Итак, решение таково:

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

Я думал, что вы могли бы использовать акселерометр, чтобы обнаружить, где "вниз" - и, следовательно, ориентацию телефона. Парень-инженер объясняет, что так поступает сам телефон.

Я искал здесь на SO способ сделать это и нашел этот ответ. По сути, вам необходимо проверить, какой из 3 акселерометров обнаруживает наиболее значительный компонент гравитационного притяжения, который, как вы знаете, составляет примерно 9,8 м / с² у земли. Вот фрагмент кода из него:

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) {
    }
};  

Будьте осторожны, так как этот код может не работать в каждой ситуации, так как вам необходимо учитывать такие сценарии, как остановка / ускорение поезда или быстрое перемещение телефона в игре - вот страница вики порядка, чтобы начать работу. Похоже, вы в безопасности со значениями, которые Халил вставил в свой код (в своем ответе), но я бы проявил особую осторожность и изучил, какие значения могут быть сгенерированы в различных сценариях.

Это не безупречная идея, но я думаю, что до тех пор, пока API построен таким, каким он есть - без предоставления вам расчетной ориентации - я думаю, что это полезный обходной путь.

Другие вопросы по тегам