基于Android R版本分析
SystemBar show / hide
Android提供了API接口用于实现状态栏、导航栏、手势等一些功能的隐藏;
Android Q
在Android 10版本支持的API中,还是使用SystemUiVisibility 属性来支持SystemUI View的属性修改;
SystemUiVisibility属性主要用来控制系统状态栏和导航栏的行为,状态栏和导航栏都属于SystemUI模块的StatusBar,所以SystemUiVisibility属性的消费者包含StatusBar,同时当状态栏和导航栏发生变化时,窗口的布局一般也会跟着发生变化,这就意味着PhoneWindowManager也要消费SystemUiVisibility属性;
属性 | 说明 |
---|---|
View.SYSTEM_UI_FLAG_VISIBLE | 默认显示状态栏和导航栏 |
View.SYSTEM_UI_FLAG_LOW_PROFILE | 低调模式,隐藏不重要的状态栏图标,导航栏中相应的图标都变成了一个小点,点击状态栏或者导航栏还原成正常状态 |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | 隐藏导航栏,点击屏幕任意区域,导航栏将重新出现 |
View.SYSTEM_UI_FLAG_FULLSCREEN | 隐藏状态栏,从状态栏位置下拉,状态栏将重新出现 |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 将布局内容拓展到导航栏和状态栏后面 |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 将布局内容拓展到状态栏后面 |
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | 顶部状态栏的风格设置,6.0版本后生效 |
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | 底部导航栏的风格设置,6.0版本后生效 |
…………………… | ……………………(参考:setSystemUiVisibility属性详解) |
Android R
WindowInsetsController是一个接口类,它主要作用就是控制窗口行为,此类在Android R(API 30) 添加,意在简化原先代码动态修改窗口SystemBars(StatusBar&NavigationBar)表现;
主要功能包括:
- 显示/隐藏 System bar;
- 设置 System bar 前景(如状态栏的文字图标)是亮色还是暗色;
- 逐帧控制 insets 动画,例如可以让软键盘弹出得更丝滑
属性 | 说明 |
---|---|
APPEARANCE_OPAQUE_STATUS_BARS | 使状态栏变成不透明的纯深色背景和浅色前景 |
APPEARANCE_OPAQUE_NAVIGATION_BARS | 使导航栏变成不透明的纯深色背景和浅色前景 |
APPEARANCE_LOW_PROFILE_BARS | 使System Bar上的项目在不改变栏的布局的情况下变得不那么明显 |
APPEARANCE_LIGHT_NAVIGATION_BARS | 控制NavigationBar(导航栏)反色的标志,相当于SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR |
APPEARANCE_LIGHT_STATUS_BARS | 控制StatusBar(状态栏)反色的标志,相当于SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR |
BEHAVIOR_SHOW_BARS_BY_TOUCH | 向后模式,与 hide() 结合 后,从隐藏栏的屏幕边缘滑动后,会固定显示 |
BEHAVIOR_SHOW_BARS_BY_SWIPE | 沉浸模式,与 hide() 结合 后,从隐藏栏的屏幕边缘滑动后,会固定显示 |
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE | 粘性沉浸模式,与 hide() 结合 后,从隐藏栏的屏幕边缘滑动后,系统栏会再次显示且会在一段时间后再次自动隐藏 |
Android S
Android 12之后,针对沉浸式手势导航进行了简化,使手势导航更加轻松,并且与其他活动(如观看视频和读书)的体验保持一致。即使处于沉浸式模式下,系统手势也会立即响应,应用仍然可以防止全屏游戏体验中的意外手势,因此用户在玩游戏时不会意外退出游戏;
要做到这一点,对于非粘性身临其境的体验现有的行为(BEHAVIOR_SHOW_BARS_BY_TOUCH
, BEHAVIOR_SHOW_BARS_BY_SWIPE
)已被弃用Android中12开始,他们已被替换缺省行为(BEHAVIOR_DEFAULT
),允许比划着一个刷卡隐藏系统栏时。此标志根据模式显示不同的视觉和功能行为:
-
在三键模式下,视觉和功能行为与12之前的Android版本中的沉浸模式相同;
-
在手势导航模式下,行为如下:
- 在视觉上,它与Android 11及更低版本中的沉浸模式相同;
- 从功能上讲,即使隐藏了栏,也允许使用手势。系统后部仅需一次滑动即可调用,而无需使用Android 11的两次滑动,无需其他滑动即可拉下通知栏;
-
对于以Android 12和更低版本为目标的在Android 12上运行的应用:
-
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
在功能和视觉上都表现相同; - 默认设置映射到
BEHAVIOR_SHOW_BARS_BY_SWIPE
;
-
-
对于在Android 11(API级别30)及更低版本(针对Android 12)上运行的应用:
- 除了
BEHAVIOR_SHOW_BARS_BY_TOUCH
映射到之外,预期具有相同的行为BEHAVIOR_SHOW_BARS_BY_SWIPE
; - 确保更新您的SDK级别以使用新的默认值(
BEHAVIOR_SHOW_BARS_BY_SWIPE
)。否则,BEHAVIOR_SHOW_BARS_BY_TOUCH
保留默认值;
- 除了
Android R hide
WindowInsetsController#hide
hide方法支持隐藏多种类型的System UI:
- WindowInsets.Type.statusBars() :状态栏;
- WindowInsets.Type.navigationBars() :导航栏;
- WindowInsets.Type.systemBars() :系统栏(包括状态栏、导航栏);
- WindowInsets.Type.ime() :键盘 (需要界面有输入控件且获取到焦点才能起作用);
- WindowInsets.Type.captionBar() :标题栏;
InsetsController#notifyInsetsChanged
这个过程中,大致执行了两件事:
-
向注册了AppWindowInsetsChanged的View进行上报WindowInsets的变化情况,上述的数据为WindowInsets实例;
-
修改Server端的WindowInsets信息;
- 将Request的WindowInsets信息更新到对应Type的InsetsSource中;
- 调用DisplayContent的forAllWindows()方法向当前DisplayContent的所有Window窗口通知;
- 将更新信息通知给SystemUI进程;
InputManagerService#onInputEvent
当前Activity或View调用了hide()方法隐藏了SystemBar之后,通过手势下滑显示了下拉面板,会触发InputManagerService的Input Event;
在DisplayPolicy中,会判断当前Activity或者View的hide()对应的behavior属性,会区分为两种情况:
-
systemBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
该属性定义了SystemBar后续的显示逻辑为瞬态,即在显示一段时间之后,会再次隐藏;
-
systemBehavior != BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
该属性定义了SystemBar后续显示逻辑不为瞬态,一旦显示,则一直显示;
systemBehavior != BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
该情况下,在响应Input Event之后,判断systemBehavior不是瞬态属性,则直接会调用对应Activity所属的WindowState的showInsets()方法,通过跨进程的方式调用了mClient的showInsets()方法,而mClient就是ViewRootImpl中的W内部类,继承了IWindow.Stub;
至此就回调回了View的处理逻辑中,通过调用applyAnimation()方法,实现SystemBar的显示;
在applyAnimation()方法中,最核心的逻辑,就是对typesReady变量的判断,如果是瞬态场景下,typesReady == 0,否则typesReady != 0;
systemBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
同理,在requestTransientBars()方法中,同样也是会调用直接调用WindowState的showInsets()方法,但是在ViewRootImpl中,会对typesReady变量进行计算,计算后判断是否 == 0,在 == 0 的场景下,会直接return,不执行后续逻辑,即不执行hideDirectly()逻辑;
在瞬态的场景下,SystemBar的显示是通过Choreographer的doFrame回调触发了WindowAnimator的animate()方法,触发了WindowState的notifyInsetsControlChanged()回调,同样又回转到ViewRootImpl中响应;
ViewRootImpl#setControl
大致也是分为两部分:
-
onStateChanged
更新本地InsetsState(mState)中对应Type的InsetsSource信息;
-
onControlsChanged
- 这个过程中,会执行setControl()方法,更新当前所有现有的源消费者;
- 在setControl()方法中会检查是否需要恢复服务器可见性(if (source.isVisible() != serverVisibility) ),如果不满足,则需要调用InsetsController的notifyVisibilityChanged(),继而又调用到ViewRootImpl的notifyInsetsChanged()方法,这一块逻辑就续上了之前hide()操作过程中执行的notifyInsetsChanged逻辑;
ViewRootImpl#applyAnimation
这个过程对应的为下拉面板手动收起时对应的逻辑;
同样的,还是会通过Choreographer的doFrame回调触发了WindowAnimator的animate()方法,触发了WindowState的notifyInsetsControlChanged()回调,同样又回转到ViewRootImpl中响应;
差分点在onControlsChanged()方法中,在下拉面板收起过程中,会调用applyAnimation方法,至此就又进入了hideDirectly()方法执行hide操作;