前言
Android 8.0开始,各大手机厂商就开始提出刘海屏的概念,在2018 google开发者大会上公布的Android P版本,google官方支持了刘海屏,并提供官方API给开发者调用。那么系统开发者如何在自己的Aosp工程中实现刘海屏功能呢?本文将带领各位一同了解一下。
刘海屏
Android 9 新增了对在设备上实现不同类型刘海屏的支持。通过刘海屏,您可以打造沉浸式全面屏体验,同时继续在设备前端为重要传感器留出空间。
Android 9 支持以下类型的刘海屏:
- 顶部中间刘海屏:刘海屏位于顶部边缘的中间位置
- 顶部非中间刘海屏:刘海屏位于边角处或稍微偏离中心的位置
- 底部刘海屏:刘海屏位于底部
- 双刘海屏:一个刘海屏位于顶部,一个位于底部
代码实现
以下窗口管理器代码 (PhoneWindowManager.java )显示了如何在未设置 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
的情况下将显示屏框架嵌入安全区域。
// Ensure that windows with a DEFAULT or NEVER display cutout mode are laid out in
// the cutout safe zone.
if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
final Rect displayCutoutSafeExceptMaybeBars = mTmpDisplayCutoutSafeExceptMaybeBarsRect;
displayCutoutSafeExceptMaybeBars.set(displayFrames.mDisplayCutoutSafe);
if (layoutInScreen && layoutInsetDecor && !requestedFullscreen
&& cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) {
// At the top we have the status bar, so apps that are
// LAYOUT_IN_SCREEN | LAYOUT_INSET_DECOR but not FULLSCREEN
// already expect that there's an inset there and we don't need to exclude
// the window from that area.
displayCutoutSafeExceptMaybeBars.top = Integer.MIN_VALUE;
}
if (layoutInScreen && layoutInsetDecor && !requestedHideNavigation
&& cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) {
// Same for the navigation bar.
switch (mNavigationBarPosition) {
case NAV_BAR_BOTTOM:
displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE;
break;
case NAV_BAR_RIGHT:
displayCutoutSafeExceptMaybeBars.right = Integer.MAX_VALUE;
break;
case NAV_BAR_LEFT:
displayCutoutSafeExceptMaybeBars.left = Integer.MIN_VALUE;
break;
}
}
if (type == TYPE_INPUT_METHOD && mNavigationBarPosition == NAV_BAR_BOTTOM) {
// The IME can always extend under the bottom cutout if the navbar is there.
displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE;
}
// Windows that are attached to a parent and laid out in said parent already avoid
// the cutout according to that parent and don't need to be further constrained.
// Floating IN_SCREEN windows get what they ask for and lay out in the full screen.
// They will later be cropped or shifted using the displayFrame in WindowState,
// which prevents overlap with the DisplayCutout.
if (!attachedInParent && !floatingInScreenWindow) {
mTmpRect.set(pf);
pf.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
parentFrameWasClippedByDisplayCutout |= !mTmpRect.equals(pf);
}
// Make sure that NO_LIMITS windows clipped to the display don't extend under the
// cutout.
df.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
}
SystemUI 在刘海屏区域呈现,且需要确定可以绘制的位置。 PhoneStatusBarView.java提供了一个视图示例,它确定了刘海屏的位置、刘海屏大小,以及从导航栏嵌入是否可以避开刘海屏区域。
通过方法 onApplyWindowInsets()
,视图可以确定刘海屏的位置,并相应地更新其布局。
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if (updateOrientationAndCutout(mLastOrientation)) {
updateLayoutForCutout();
requestLayout();
}
return super.onApplyWindowInsets(insets);
}
这些方法概述了各种类型的刘海屏(即顶部中间刘海屏、顶部非中间刘海屏、底部刘海屏和双刘海屏)在状态栏中的处理方式。
官方要求
要确保刘海屏不会对应用造成负面影响,您必须确保:
- 在竖屏模式下,状态栏的高度至少与刘海屏的高度持平
- 在全屏模式和横屏模式下,刘海屏区域必须显示遮幅式黑边
您的设备最多可以在每个短边处(顶部和底部)各设一个刘海屏。
实现
要在设备上实现刘海屏,您必须为系统界面配置以下值。
值 | 说明 |
---|---|
quick_qs_offset_height | 定义“快捷设置”面板的上外边距。时钟和电池图标显示在该面板上方的空间。在 values-land 中,将此值设置为 status_bar_height_landscape,在纵屏模式下,将此值设置为默认值 48dp 或刘海屏高度(以较大者为准)。可以根据需要选择高于刘海屏的高度。 |
quick_qs_total_height | 展开通知栏时,“超快设置”面板(收起的“快捷设置”面板)的总高度(其中包括包含时钟图标的面板上方的空间)。受“快捷设置”面板的布局方式限制,“超快设置”面板(包括偏移量)的总高度必须是静态已知的,因此必须由同一增量 quick_qs_offset_height 调整此值。在 Values-land 中,此值默认为 152dp,在纵屏模式下,此值默认为 176dp。 |
status_bar_height_portrait | 从框架的角度而言,状态栏的默认高度。在大多数设备上,此值默认为 24dp。如果设备上有刘海屏,则将此值设置为刘海屏的高度。可以根据需要选择高于刘海屏的高度。 |
status_bar_height_landscape | 状态栏在横屏模式下的高度。由于仅支持在设备的短边上显示刘海屏,因此此值始终是未经更改的状态栏高度。在没有刘海屏的设备上,此值等同于 status_bar_height_portrait。如果设备上有刘海屏,则将此值保留为默认的状态栏高度。 |
config_mainBuiltInDisplayCutout | 用于定义刘海屏形状的路径。这是一个可由 android.util.PathParser 解析的字符串,并且是向系统定义刘海屏大小和形状的方式。可在路径中指定 @dp 以便模拟针对不同设备的形状。由于实际的刘海屏具有精确的像素尺寸,因此在定义硬件刘海屏的路径时,请勿使用 @dp 指定符。 |
config_fillMainBuiltinDisplayCutout | 一个确定是否在软件中绘制刘海屏路径(在上文中进行了定义)的布尔值。可用于模拟刘海屏,或填充实际刘海屏,以实现抗锯齿。如果为 true,则系统会以黑色填充 config_mainBuiltInDisplayCutout。 |
有关默认定义,请参阅以下 dimens
文件:
core/res/res/values-land/dimens.xml
-
core/res/res/values/dimens.xml
模拟刘海屏的示例叠加层:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- The bounding path of the cutout region of the main built-in display.
Must either be empty if there is no cutout region, or a string that is parsable by
{@link android.util.PathParser}.
The path is assumed to be specified in display coordinates with pixel units and in
the display's native orientation, with the origin of the coordinate system at the
center top of the display.
To facilitate writing device-independent emulation overlays, the marker `@dp` can be
appended after the path string to interpret coordinates in dp instead of px units.
Note that a physical cutout should be configured in pixels for the best results.
-->
<string translatable="false" name="config_mainBuiltInDisplayCutout">
M 0,0
L -48, 0
L -44.3940446283, 36.0595537175
C -43.5582133885, 44.4178661152 -39.6, 48.0 -31.2, 48.0
L 31.2, 48.0
C 39.6, 48.0 43.5582133885, 44.4178661152 44.3940446283, 36.0595537175
L 48, 0
Z
@dp
</string>
<!-- Whether the display cutout region of the main built-in display should be forced to
black in software (to avoid aliasing or emulate a cutout that is not physically existent).
-->
<bool name="config_fillMainBuiltInDisplayCutout">true</bool>
<!-- Height of the status bar -->
<dimen name="status_bar_height_portrait">48dp</dimen>
<dimen name="status_bar_height_landscape">28dp</dimen>
<!-- Height of area above QQS where battery/time go (equal to status bar height if > 48dp) -->
<dimen name="quick_qs_offset_height">48dp</dimen>
<!-- Total height of QQS (quick_qs_offset_height + 128) -->
<dimen name="quick_qs_total_height">176dp</dimen>
</resources>
验证
要验证刘海屏的实现,请在以下位置运行 CTS 测试:tests/framework/base/windowmanager/src/android/server/wm
参考文章
https://source.android.com/devices/tech/display/display-cutouts
本文已独家授权公众号ApeClub使用,更多好文章,请关注ApeClub。