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