一、刘海缺口原理
1. 切换刘海缺口形状入口
显示刘海缺口窗口的开关入口在 设置->开发者选项->模拟“刘海屏”,点击选择不同的刘海形状,会调用OverlayManagerServiceImpl.setEnabledExclusive()
,在这个方法中选择启用frameworks/base/packages/overlays
中对应的资源应用,这些应用都只包含资源文件,其中定义了R.string.config_mainBuiltInDisplayCutout
来描述刘海的形状。
2. 更新DisplayCutout数据供应用适配
上面讲到选择不同的刘海形状,会调用OverlayManagerServiceImpl
的相关方法,其中同时也会触发onOverlaysChanged
回调来通知资源变化:
DisplayManagerService.LocalService.onOverlayChanged() -> LocalDisplayAdapter.LocalDisplayDevice.requestDisplayModesLocked() -> DisplayManagerService.handleDisplayDeviceChanged() -> DisplayCutout.fromResources()
DisplayCutout.java
中更新屏幕边界和安全区域(mBounds、mSafeInsets)
的相关数据,提供了public
方法来供上层应用获取来进行刘海屏的适配,包括:
getBoundingRects()
:返回Rects的列表,表示显示屏上非功能区域的边界矩形。getSafeInsetXXX()
:返回安全区域距离屏幕的距离(XXX表示Left,Right等)。
3. 显示刘海缺口
刘海缺口的显示在SystemUI
中ScreenDecorations.java
,其中注册了显示屏变化的回调DisplayManager.DisplayListener()
,当刘海缺口有变化时,读取实际的刘海形状来进行绘制(DisplayCutoutView)
。
4. 调整应用窗口
Android P
中为应用适配新增了窗口属性windowLayoutInDisplayCutoutMode
,有以下三种类型:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
:默认值,只有当DisplayCutout
完全包含在系统状态栏中时,才允许窗口延伸到DisplayCutout
区域显示。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
:该窗口决不允许与DisplayCutout
区域重叠。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
:该窗口始终允许延伸到屏幕短边上的DisplayCutout
区域。
该属性的作用,主要涉及以下三处:
PhoneWindowManager.java
中,计算并调整窗口大小的layoutWindowLw()
,在这里会将未适配刘海屏的应用窗口移动到只在安全区域内显示;ViewRootImpl.java
中,View Tree
遍历流程中设置View
的屏幕边距;WindowState.java
中,判断是否需要在刘海区域绘制黑条Letterbox
,显示Letterbox
的场景包括未适配刘海屏的全屏页面、未适配刘海屏的横屏页面等。
其中,Letterbox
的原理可以简单描述为: Letterbox
由system_server
进程进行管理,在其中维护一个屏幕尺寸outer
和实际窗口尺寸inner
,当窗口切换时,也就是由一个WindowState
切换到另一个WindowState
时,由于对应窗口的实际显示区域不同,outer
、inner
会有变化时,此时Letterbox
就需要更新,若outer
>inner
,则SurfaceControl
创建的黑色surface
背景就会显示出来,刘海区域矩形就显示为黑条。
二、状态栏原理
1. 状态栏背景色修改
状态栏与导航栏都属于SystemUI
的管理范畴,拥有自己独立的窗口,而且这两个窗口的优先级较高,会悬浮在所有窗口之上,其本身是全透明的,而我们看到的背景颜色是因为下层APP
在状态栏对应的区域添加了颜色,对应的视图在应用的顶层布局DecorView
中。
应用修改状态栏背景色,可以通过两种方式:
-
使用
getWindow().setStatusBarColor()
方法在代码中修改; -
在
style.xml
中通过定制colorPrimaryDark
属性的方式修改。
这两种方式都是通过 DecorView.java
中的updateColorViews()
方法来实现,最终设置的状态栏颜色由 calculateStatusBarColor()
方法计算得到。
例外情况是,适配了刘海屏的全屏应用,会将状态栏整体移除,导致刘海区域显示为透明。
看一下状态栏的属性定义:
public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
new ColorViewAttributes(
SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
Gravity.TOP, Gravity.LEFT, Gravity.RIGHT,
Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME,
com.android.internal.R.id.statusBarBackground,
FLAG_FULLSCREEN);
ColorViewAttributes
中定义了窗口是否布局和可见条件的方法isPresent()
和isVisible()
。
可以看到,对于设置了窗口属性 SYSTEM_UI_FLAG_FULLSCREEN
或 FLAG_FULLSCREEN
的全屏页面,状态栏背景是不显示的。
2. 状态栏图标着色
状态栏图标有light
和dark
两种模式,如果状态栏背景设为黑色同时图标着色为dark
模式(显示黑色),那么会导致图标看不到的问题,因此也需要在隐藏刘海的同时禁止设置图标着色模式为dark。
应用修改状态栏图标颜色有两种方法:
- 在代码中使用
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
方法; styles.xml
中设置windowLightStatusBar
属性。
例外情况是,Android 8.0
原生添加了 壁纸变化自动更新主题 的机制(notifyWallpaperColorsChanged()
相关流程),当由深色壁纸切换到浅色壁纸时,SystemUI
会切换到浅色主题,此时状态栏图标颜色会被设置为暗色,反之亦然。