WindowInsets.getDisplayCutout имеет значение NULL везде, кроме onAttachedToWindow в полноэкранном приложении Java.

У меня проблемы с getDisplayCutout()в моем полноэкранном приложении Java. Мне кажется, что я могу получить значение DisplayCutout только в пределахonAttachedToWindowфункция. После завершения этой функции я больше никогда не смогу ее получить. Код для вырезания:

WindowInsets insets = myActivity.getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
    DisplayCutout displayCutout = insets.getDisplayCutout();
    if (displayCutout != null && displayCutout.getBoundingRects().size() > 0) {
        // we have cutouts to deal with
    }
}

Вопрос

Как я могу надежно получить вырезы отображения из любого места кода в любое время, когда они были присоединены к иерархии представлений?

Воспроизвести проблему

После долгих исследований я сузил ее до ОЧЕНЬ обширной проблемы с полноэкранными приложениями, и я удивлен, что об этом никто не спрашивает. Фактически, мы можем игнорировать мое приложение и просто работать над двумя шаблонными проектами, которые вы можете сделать сами прямо сейчас.

В студии Android я говорю о проектах телефонов и планшетов под названием "Базовая активность" и "Полноэкранная активность". Если вы создадите по одному и внесете следующие изменения:

Для Basic измените манифест, чтобы самостоятельно обрабатывать изменения конфигурации, добавив android:configChanges="orientation|keyboardHidden|screenSize" под тегом Activity так:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:configChanges="orientation|keyboardHidden|screenSize"
    android:theme="@style/AppTheme.NoActionBar">

Теперь для них обоих добавьте в файл активности следующие две функции:

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
    if (insets != null) {
        DisplayCutout displayCutout = insets.getDisplayCutout();
        if (displayCutout != null && displayCutout.getBoundingRects().size() > 0) {
            // we have cutouts to deal with
        }
    }
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
    if (insets != null) {
        DisplayCutout displayCutout = insets.getDisplayCutout();
        if (displayCutout != null && displayCutout.getBoundingRects().size() > 0) {
            // we have cutouts to deal with
        }
    }
}

Это все, что вам нужно сделать, чтобы воспроизвести эту проблему. Я запускаю это на симуляторе для Pixel 3 на API Q, в котором я включил моделирование вырезов и выбрал ОБЕИХ (так что есть вырезы внизу и вверху)

Теперь, если вы поставите точку останова на строке, где мы пытаемся получить вырезы для отображения (DisplayCutout displayCutout = insets.getDisplayCutout();), вы увидите, что в приложении Basic оно работает при запуске и при изменении ориентации, но в полноэкранном приложении оно работает только при запуске.

Фактически, в моем приложении я тестировал, используя следующий код в onAttachedToWindow:

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    // start a thread so that UI thread execution can continue
    WorkerThreadManager.StartWork(new WorkerCallback() {
        @Override
        public void StartWorkSafe() {
            // run the following code on the UI thread
            XPlatUtil.RunOnUiThread(new SafeRunnable() {
                public synchronized void RunSafe() {
                    WindowInsets insets = myActivity.getWindow().getDecorView().getRootWindowInsets();
                    if (insets != null) {
                        DisplayCutout displayCutout = insets.getDisplayCutout();
                        if (displayCutout != null && displayCutout.getBoundingRects().size() > 0) {
                            // we have cutouts to deal with
                        }
                    }
                }
            });
        }
    });
}

Этот код запускает поток, чтобы функция onAttachedToWindow могла завершить работу; но поток немедленно отправляет выполнение обратно в поток пользовательского интерфейса для проверки вырезов.

Задержка между завершением выполнения функции onAttachedToWindow и проверкой моего кода для displayCutout должна быть порядка наносекунд, но вырезы сразу же недоступны.

Есть идеи? Это как-то ожидается?

Без доступа к вырезам при изменении ориентации у меня нет другого выхода, кроме как записать самую большую вставку (верхнюю или нижнюю в портретной ориентации, поскольку длинные края не могут иметь их) и применить ее как к верху, так и снизу в портретной ориентации или слева и справа в пейзаже.

Это потому, что я не могу найти в Android способ проверить, КАКОЙ вид ландшафта активен в настоящее время (например, верхняя часть телефона слева или справа). Если бы я мог это проверить, я мог бы, по крайней мере, применить вставку только на краю телефона, где это необходимо.

1 ответ

Я придумал много вещей, которые могут помочь другим людям.
Сначала я отвечу на исходный вопрос, а затем объясню, почему это работает, с некоторыми альтернативами для людей, которые борются с той же проблемой, что и я: как отобразить полноэкранное приложение на телефоне с вырезами, правильно разместив мое приложение в виде почтового ящика.
ПРИМЕЧАНИЕ. Все мои макеты выполняются программно (у меня даже нет файлов xml в моем каталоге макетов). Это было сделано по причинам кроссплатформенности, и, возможно, именно поэтому у меня возникает эта проблема, а у других - нет.

OP Ответ

Используйте следующий код, чтобы получить вырезы на дисплее (с соответствующими нулевыми проверками):

<activity>.getWindowManager().getDefaultDisplay().getCutout();

В моем тестировании это вернуло правильные вырезы в onConfigurationChangedи не является нулевым при вызове внеonAttachedToWindow (если есть вырезы конечно).

Объяснил

getWindow().getDecorView().getRootWindowInsets().getDisplayCutout() всегда имеет значение NULL при доступе за пределами onAttachedToWindowработать, когда ваше приложение находится в полноэкранном режиме; Я не нашел способа обойти это.
Если вы выведите приложение из полноэкранного режима, выполнив следующие действия:

rootView.setSystemUiVisibility(0
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

Вы получите значение от getDisplayCutout при вызове его извне onAttachedToWindow.
Конечно, это не выход.
Хуже того, значение, которое вы получаете от вызова этой функции, когда внутриonConfigurationChangedбольшую часть времени кажется на самом деле неправильным; он как устаревший (показывает места вырезов до изменения ориентации), так и иногда полностью отсутствует вырез (когда есть несколько вырезов), что приводит к неправильным значениям safeInsets.

Полезные наблюдения

Я узнал кое-что полезное, пытаясь решить эту проблему, и это может помочь другим, пытающимся справиться с вырезами.
В моем сценарии у меня есть полноэкранное приложение, и я не хочу усложнять, пытаясь использовать весь экран и настраивая макет в зависимости от размера и местоположения выреза.
Идеальная обработка вырезов - это гораздо больше усилий, чем для меня как отдельного разработчика, работающего над приложением.

Все, что я хочу, - это иметь возможность надежно выкладывать все мои представления программно, не прибегая к вырезкам; это означает, что все, что мне нужно знать, - это размер и положение прямоугольника, в котором можно безопасно разместить мой контент.
Это звучит просто, но было затруднено из-за проблемы, описанной выше, и из-за некоторой причуды в почтовом ящике Android, где задействованы системные наложения и вырезы.

Ниже приведены несколько советов о том, как с этим можно справиться с разными результатами.

Получение доступного размера экрана (полноэкранное приложение)

Способ 1

Вы можете получить доступный размер экрана, используя

<activity>.getWindowManager().getDefaultDisplay().getSize()

Кажется, что этот размер всегда безопасен для использования, с вырезами или без них, при использовании LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER для автоматической вставки содержимого во избежание вырезов.

Тем не менее, похоже, что он резервирует место для панели навигации.
Со следующими установленными флагами:

setSystemUiVisibility(0
    | View.SYSTEM_UI_FLAG_LOW_PROFILE
    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // hide nav bar
    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // hide status bar
    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
    | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // if they swipe to show the soft-navigation, hide it again after a while.
);

В getSize()появляется функция, чтобы зарезервировать место для панели навигации внизу экрана в портретной ориентации.
Если вы используетеLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, то этот размер будет компенсировать вырезы, что означает, что ваш контент может поместиться в предоставленном пространстве, но также будет зарезервировано дополнительное пространство для панели навигации.
Если вы используете устройство без вырезов, внизу все равно останется место для панели навигации.
Я не хочу этого.

Способ 2

Другой способ получить размер экрана - сделать

DisplayMetrics metrics = new DisplayMetrics();
MainActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);

Это позволит получить фактический размер экрана в пикселях, от края до края, игнорируя системные наложения или полосы, а также вырез в телефоне.
Похоже, что это лучший способ выбрать полноэкранное приложение в сочетании с точным и надежным методом определения вставок выреза.
Строка состояния и мягкая навигация будут немного странно перекрываться с пустым пространством на краю вашего приложения, когда пользователь проводит пальцем, чтобы показать их, но это, безусловно, лучшее решение, которое я нашел, где я могу избежать попыток фактически обернуть вырезы с моей раскладкой.

Использовать LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, затем используйте вырезы на экране, чтобы уменьшить и изменить положение корневого вида.

// Get the actual screen size in pixels
DisplayMetrics metrics = new DisplayMetrics();
MainActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
// if there are no cutouts, top and left offsets are zero
int top = 0;
int left = 0;
// get any cutouts
DisplayCutout displayCutout = MainActivity.getWindowManager().getDefaultDisplay().getCutout();
// check if there are any cutouts
if (displayCutout != null && displayCutout.getBoundingRects().size() > 0) {
    // add safe insets together to shrink width and height
    width -= (displayCutout.getSafeInsetLeft() + displayCutout.getSafeInsetRight());
    height -= (displayCutout.getSafeInsetTop() + displayCutout.getSafeInsetBottom());
    // NOTE:: with LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, we can render on the whole screen
    // NOTE::    and therefore we CAN and MUST set the top/left offset to avoid the cutouts.
    top = displayCutout.getSafeInsetTop();
    left = displayCutout.getSafeInsetLeft();
}

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

Способ 3

Здесь мы используем тот же метод, что и в #2, чтобы определить доступные ширину и высоту, но используяLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVERтак что Android имеет дело с положением этого контента.
Я думал, что это сработает нормально, в конце концов, меня не интересует рендеринг в вырезанном пространстве.
Однако это вызывает странную проблему в портретной ориентации - высота, которую Android зарезервировал для строки состояния, была больше, чем высота верхнего выреза.
Это приводит к тому, что мой код рассчитывает, что у меня больше высоты, чем на самом деле, и в результате мой контент обрезается внизу.

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

Вывод

Использовать <activity>.getWindowManager().getDefaultDisplay().getCutout();чтобы получить вырезы для дисплея.
После этого просто решите, какой метод использовать, чтобы определить безопасную область для рендеринга.

Я рекомендую способ 2

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