基于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_TOUCHBEHAVIOR_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() :标题栏;

SystemBar(状态栏&导航栏) show / hide

InsetsController#notifyInsetsChanged

SystemBar(状态栏&导航栏) show / hide

这个过程中,大致执行了两件事:

  • 向注册了AppWindowInsetsChanged的View进行上报WindowInsets的变化情况,上述的数据为WindowInsets实例;

  • 修改Server端的WindowInsets信息;

    • 将Request的WindowInsets信息更新到对应Type的InsetsSource中;
    • 调用DisplayContent的forAllWindows()方法向当前DisplayContent的所有Window窗口通知;
    • 将更新信息通知给SystemUI进程;

InputManagerService#onInputEvent

当前Activity或View调用了hide()方法隐藏了SystemBar之后,通过手势下滑显示了下拉面板,会触发InputManagerService的Input Event;

SystemBar(状态栏&导航栏) show / hide

在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

SystemBar(状态栏&导航栏) show / hide

该情况下,在响应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

SystemBar(状态栏&导航栏) show / hide

同理,在requestTransientBars()方法中,同样也是会调用直接调用WindowState的showInsets()方法,但是在ViewRootImpl中,会对typesReady变量进行计算,计算后判断是否 == 0,在 == 0 的场景下,会直接return,不执行后续逻辑,即不执行hideDirectly()逻辑;

在瞬态的场景下,SystemBar的显示是通过Choreographer的doFrame回调触发了WindowAnimator的animate()方法,触发了WindowState的notifyInsetsControlChanged()回调,同样又回转到ViewRootImpl中响应;

ViewRootImpl#setControl

SystemBar(状态栏&导航栏) show / hide

大致也是分为两部分:

  • onStateChanged

    更新本地InsetsState(mState)中对应Type的InsetsSource信息;

  • onControlsChanged

    • 这个过程中,会执行setControl()方法,更新当前所有现有的源消费者;
    • 在setControl()方法中会检查是否需要恢复服务器可见性(if (source.isVisible() != serverVisibility) ),如果不满足,则需要调用InsetsController的notifyVisibilityChanged(),继而又调用到ViewRootImpl的notifyInsetsChanged()方法,这一块逻辑就续上了之前hide()操作过程中执行的notifyInsetsChanged逻辑;

ViewRootImpl#applyAnimation

这个过程对应的为下拉面板手动收起时对应的逻辑;

SystemBar(状态栏&导航栏) show / hide

同样的,还是会通过Choreographer的doFrame回调触发了WindowAnimator的animate()方法,触发了WindowState的notifyInsetsControlChanged()回调,同样又回转到ViewRootImpl中响应;

差分点在onControlsChanged()方法中,在下拉面板收起过程中,会调用applyAnimation方法,至此就又进入了hideDirectly()方法执行hide操作;