WindowInsets.getDisplayCutout 在全屏 Java 应用程序中除了 onAttachedToWindow 之外的任何地方都是 NULL

Lil*_*bug 3 java null android fullscreen display-cutouts

getDisplayCutout()我的全屏 Java 应用程序有问题。我似乎只能在onAttachedToWindow函数中获取 DisplayCutout 的值。该功能完成后,我再也无法获得它。获取切口的代码:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

一旦附加到视图层次结构,如何从代码中的任何位置可靠地获取显示切口?

重现问题

经过大量调查,我将其范围缩小到全屏应用程序的一个非常广泛的问题,我很惊讶没有其他人询问它。事实上,我们可以忽略我的应用程序,只需处理两个模板项目,您现在就可以自己制作。

在 Android Studio 中,我谈论的是名为“基本活动”和“全屏活动”的手机和平板电脑项目。如果您创建其中之一,并进行以下更改:

对于 Basic,更改 Manifest 以通过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">
Run Code Online (Sandbox Code Playgroud)

现在,将以下两个函数添加到活动文件中:

@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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是重现此问题所需要做的全部工作。我在 API Q 上的 Pixel 3 模拟器上运行它,在该模拟器上我启用了模拟切口并选择了两者(因此底部和顶部都有一个切口)

现在,如果您在我们尝试获取显示切口 ( 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
                        }
                    }
                }
            });
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

此代码启动一个线程,以便 onAttachedToWindow 函数可以完成运行;但线程立即将执行发送回 UI 线程以检查切口。

onAttachedToWindow 函数完成其执行与我的代码检查 displayCutout 之间的延迟必须在纳秒的数量级,但这些切口立即不可用。

有任何想法吗?这是以某种方式预期的吗?

当方向改变时无法访问切口,我别无选择,只能记录最大的插图(纵向的顶部或底部,因为长边不能有它们),并将其应用于纵向的顶部和底部,或左右在风景中。

这是因为我无法在 android 中找到一种方法来检查当前处于活动状态的景观类型(例如,手机顶部位于左侧还是右侧)。如果我可以检查一下,我至少可以只在需要它的手机边缘应用插图。

Lil*_*bug 5

我想出了一堆可能对其他人有帮助的东西。
首先,我将回答最初的问题,然后我将解释它为什么起作用,并为我遇到同样问题的人们提供一些替代方案:How to display a fullscreen app on a phone with cutouts by correct letterboxing my app
注意:我的所有布局都是以编程方式完成的(我的布局目录中甚至没有任何 xml 文件)。这样做是出于跨平台的原因,也许这就是为什么我遇到这个问题而其他人没有的原因。

操作答案

使用以下代码获取显示切口(带有适当的空检查):

<activity>.getWindowManager().getDefaultDisplay().getCutout();
Run Code Online (Sandbox Code Playgroud)

在我的测试中,这返回了正确的切口,onConfigurationChanged并且在外部调用时不为空onAttachedToWindow(当然如果有切口)。

解释

getWindow().getDecorView().getRootWindowInsets().getDisplayCutout()onAttachedToWindow当您的应用程序全屏时,如果在函数外部访问,则似乎始终为 NULL ;我没有找到解决方法。
如果您通过执行以下操作将您的应用程序从全屏模式中移除:

rootView.setSystemUiVisibility(0
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
Run Code Online (Sandbox Code Playgroud)

从外部调用时,您将从 getDisplayCutout 获得一个值onAttachedToWindow
这当然不是解决方案。
更糟糕的是,你从这个函数调用中获得的值在大多数情况下onConfigurationChanged似乎实际上是错误的;它既已过时(显示方向更改之前的切口位置),有时也完全缺少切口(当有多个切口时)导致安全插入错误。

有用的观察

我在尝试解决这个问题时发现了一些有用的东西,这可能会帮助其他人尝试处理切口。
在我的场景中,我有一个全屏应用程序,不想通过尝试使用整个屏幕来增加复杂性,并且不得不根据切口大小和位置调整布局。
作为一个开发应用程序的单一开发人员,完美地处理剪裁比我值得付出的努力要多得多。

我想要的只是能够以编程方式可靠地布置我的所有视图,而无需处理切口;这意味着我只需要知道可以安全放置内容的矩形的大小和位置。
这听起来很容易,但由于上面的问题而变得困难,并且 Android 的内容信箱中涉及系统覆盖和剪切的一些怪癖。

以下是有关如何处理不同结果的一些提示。

获取可用的屏幕尺寸(全屏应用)

方法一

您可以使用获得可用的屏幕尺寸

<activity>.getWindowManager().getDefaultDisplay().getSize()
Run Code Online (Sandbox Code Playgroud)

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.
);
Run Code Online (Sandbox Code Playgroud)

getSize()功能似乎为纵向屏幕底部的导航栏保留空间。
如果您使用的是LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,那么此大小补偿切口,这意味着您的内容可以适合提供的空间,但它也会为导航栏保留额外的空间。
如果您使用的是没有切口的设备,导航栏的底部仍然会保留空间。
这不是我想要的。

方法二

获取屏幕大小的另一种方法是做

DisplayMetrics metrics = new DisplayMetrics();
MainActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
Run Code Online (Sandbox Code Playgroud)

这将获取以像素为单位的实际屏幕大小,边缘到边缘,忽略系统覆盖或条,以及手机切口。
当与确定切口插图的准确可靠方法相结合时,这看起来是全屏应用程序的最佳方式。
当用户滑动以显示它们时,状态栏和软导航将与应用程序边缘的空白区域有些奇怪地重叠,但这是迄今为止我发现的最佳解决方案,我可以避免尝试实际环绕与我的布局的切口。

使用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();
}
Run Code Online (Sandbox Code Playgroud)

这是我最终使用的方法,到目前为止没有发现任何问题。

方法三

在这里,我们使用与#2 中相同的方法来确定我们可用的宽度和高度,但使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER该方法是为了让 Android 处理该内容的位置。
我认为这会工作得很好,毕竟我对在剪切空间中渲染不感兴趣。
这会导致纵向出现一个奇怪的问题 - android 为状态栏保留的高度大于顶部切口的高度。
这会导致我的代码计算出我的高度比实际高,结果我的内容在底部被截断了。

如果您可以确定状态栏的高度,那么您可以相应地调整您计算的高度,但我不会打扰,因为它似乎是一个较差的解决方案。
顺便找了一段时间状态栏的高度,但是没能管理(隐藏的时候好像为零)。

结论

使用<activity>.getWindowManager().getDefaultDisplay().getCutout();得到显示切口。
之后只需决定使用哪种方法来确定要渲染的安全​​区域在哪里。

我推荐方法2