我的全屏 Java 应用程序中遇到getDisplayCutout()
问题。 我似乎只能在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工作室中,我谈论的是名为"基本活动"和"全屏活动"的手机和平板电脑项目。 如果创建其中之一,并进行以下更改:
对于"基本",通过在"活动"标记下添加android:configChanges="orientation|keyboardHidden|screenSize"
来更改清单以自行处理配置更改,如下所示:
<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模拟器上运行它,在该模拟器上,我启用了模拟切口并选择了两者(因此底部和顶部都有一个切口)
现在,如果您在我们尝试获取显示切口 (DisplayCutout displayCutout = insets.getDisplayCutout();
) 的行上断点,您将看到在基本应用程序上,它在启动时和更改方向时有效,但在全屏应用程序中它仅在启动时有效。
实际上,在我的应用程序中,我已经在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 中找不到一种方法来检查当前处于活动状态的景观类型(例如,左侧或右侧是手机顶部)。 如果我能检查一下,我至少只能在需要的地方应用手机边缘的插图。
我想出了一堆可能对其他人有帮助的东西。
首先,我将回答原始问题,然后我将解释为什么它有效,并为与我相同的问题而苦苦挣扎的人们提供一些替代方案:如何通过正确对我的应用程序进行上下黑边,在带有切口的手机上显示全屏应用程序。
注意:我的所有布局都是以编程方式完成的(我的布局目录中甚至没有任何 xml 文件)。这是出于跨平台原因而完成的,也许这就是为什么我遇到此问题而其他人没有的原因。
OP 答案
使用以下代码获取显示切口(使用适当的空检查):
<activity>.getWindowManager().getDefaultDisplay().getCutout();
在我的测试中,这在onConfigurationChanged
中返回了正确的切口,并且在onAttachedToWindow
之外调用时不为null(当然,如果有切口的话)。
解释
当您的应用程序全屏时,如果在onAttachedToWindow
函数外部访问getWindow().getDecorView().getRootWindowInsets().getDisplayCutout()
似乎始终为 NULL;我没有找到解决这个问题的方法。
如果您通过执行以下操作使应用退出全屏:
rootView.setSystemUiVisibility(0
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
从外部调用 getDisplayCutout 时,您将从 getDisplayCutout 获得一个值onAttachedToWindow
。
这当然不是解决方案。
更糟糕的是,您在onConfigurationChanged
内部时从此函数调用中获得的值似乎在大多数情况下实际上是错误的;它既过时(显示方向更改之前的切口位置),有时又完全缺少切口(当有多个切口时),导致 safeInset 错误。
有用的观察
在尝试解决此问题时,我发现了一些有用的知识,这可能有助于其他尝试处理切口的人。
在我的方案中,我有一个全屏应用程序,并且不想通过尝试使用整个屏幕来增加复杂性,并且必须根据剪切大小和位置调整布局。
完美地处理切口远远超出了我作为开发应用程序的单个开发人员所值得的。
我想要的只是能够以编程方式可靠地布置我的所有视图,而不必处理剪切;这意味着我只需要知道一个矩形的大小和位置,在其中可以安全地放置我的内容。
这听起来很容易,但由于上面的问题而变得困难,以及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