3

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
    }
}

问题

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

重现问题

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

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

对于 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">

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

@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
        }
    }
}

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

现在,如果您在我们尝试获取显示切口 ( 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 函数可以完成运行;但线程立即将执行发送回 UI 线程以检查切口。

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

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

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

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

4

1 回答 1

5

我想出了一堆可能对其他人有帮助的东西。
首先,我将回答最初的问题,然后我将解释它为什么有效,并为那些与我遇到的相同问题苦苦挣扎的人提供一些替代方案:如何通过正确地对我的应用进行信箱处理来在手机上显示带有切口的全屏应用
注意:我的所有布局都是以编程方式完成的(我的布局目录中什至没有任何 xml 文件)。这样做是出于跨平台的原因,也许这就是为什么我遇到这个问题而其他人没有的原因。

操作答案

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

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

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

解释

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

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

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

有用的观察

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

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

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

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

方法一

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

<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,则此尺寸补偿切口,这意味着您的内容可以适合提供的空间,但它也会为导航栏保留额外的空间。
如果您使用的是没有镂空的设备,底部仍会为导航栏预留空间。
这不是我想要的。

方法二

获取屏幕尺寸的另一种方法是

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

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

方法三

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

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

结论

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

我推荐方法2

于 2020-04-17T04:50:19.460 回答