前言
NavigationBar 和 StatusBar 都属于 SystemBar,也叫做 decor,就是说给 App 装饰的意思。一般的 window 的布局是在 PhoneWindowManager 的 layoutWindowLw() 方法中,而 SystemBar 是在 beginLayoutLw() 方法中布局。
当前最上层的 Activity 可以修改 SystemBar 的 visibility,可以调用 View#setSystemUiVisibility() 方法,系统也有一些针对 SystemBar visibility 的策略。最终的 visibility 保存在 PhoneWindowManager 中的 mLastSystemUiFlags 变量中。
一、简单认识Android9中的DisplayFrames
mDisplayId 是跟物理屏幕相关的,DEFAULT_DISPLAY 的值是 0,mDisplayWidth 是物理屏幕宽度,mDisplayHeight 是物理屏幕高度。
public class DisplayFrames {
public final int mDisplayId;
/**
* The current size of the screen; really; extends into the overscan area of the screen and
* doesn't account for any system elements like the status bar.
*/
//当前的屏幕大小,包括过扫描区域。过扫描区域在输出到 TV 时会用到,
// 对于移动设备来说 mOverscan 大小就是物理设备的大小 (0,0)-(dw,dh)。
public final Rect mOverscan = new Rect();
/**
* The current visible size of the screen; really; (ir)regardless of whether the status bar can
* be hidden but not extending into the overscan area.
*/
//当前可见的屏幕大小,其实就是 (0,0)-(dw,dh)。
public final Rect mUnrestricted = new Rect();
/** Like mOverscan*, but allowed to move into the overscan region where appropriate. */
//是应用可显示的区域,包含 StatusBar 的区域,不包含 NavigationBar 区域。
public final Rect mRestrictedOverscan = new Rect();
/**
* The current size of the screen; these may be different than (0,0)-(dw,dh) if the status bar
* can't be hidden; in that case it effectively carves out that area of the display from all
* other windows.
*/
//一般情况下 mRestrictedOverscan 与 mRestricted 相同
public final Rect mRestricted = new Rect();
/**
* During layout, the current screen borders accounting for any currently visible system UI
* elements.
*/
//是布局过程中,当前画面的边界,包含 Translucent(半透明)区域。一般情况下 NavigationBar 区域不是 Translucent,而 StatusBar 是 Translucent。
public final Rect mSystem = new Rect();
/** For applications requesting stable content insets, these are them. */
//是应用窗口的显示区域,不包含 StatusBar 和 NavigationBar。
public final Rect mStable = new Rect();
/**
* For applications requesting stable content insets but have also set the fullscreen window
* flag, these are the stable dimensions without the status bar.
*/
//是当Activity设置Fullscreen flag 时候的窗口显示区域,这时 StatusBar 会隐藏。
public final Rect mStableFullscreen = new Rect();
/**
* During layout, the current screen borders with all outer decoration (status bar, input method
* dock) accounted for.
*/
// 是布局的时候除去外部装饰的窗口(例如 StatusBar 和输入法窗口)。
public final Rect mCurrent = new Rect();
/**
* During layout, the frame in which content should be displayed to the user, accounting for all
* screen decoration except for any space they deem as available for other content. This is
* usually the same as mCurrent*, but may be larger if the screen decor has supplied content
* insets.
*/
// 是当前应该给用户显示的窗口,通常与 mCurrent 相同。当装饰窗口提供内容插入的时候,有可能比 mCurrent 更大。
public final Rect mContent = new Rect();
/**
* During layout, the frame in which voice content should be displayed to the user, accounting
* for all screen decoration except for any space they deem as available for other content.
*/
//mVoiceContent 通常与 mContent 相同。
public final Rect mVoiceContent = new Rect();
/** During layout, the current screen borders along which input method windows are placed. */
//mDock 是输入法布局时的边界。
public final Rect mDock = new Rect();
/** The display cutout used for layout (after rotation) */
//用于刘海屏布局的剪刀工具,Android 9.0 新加入的。
@NonNull public WmDisplayCutout mDisplayCutout = WmDisplayCutout.NO_CUTOUT;
/** The cutout as supplied by display info */
//用于刘海屏布局的剪刀工具,Android 9.0 新加入的。
@NonNull public WmDisplayCutout mDisplayInfoCutout = WmDisplayCutout.NO_CUTOUT;
/**
* During layout, the frame that is display-cutout safe, i.e. that does not intersect with it.
*/
//是在刘海屏上可以安全显示的区域,即这个区域与刘海区域没有交集。
public final Rect mDisplayCutoutSafe = new Rect();
二、系统对SystemBar的布局
1、Android9系统主要是在PhoneWindowManager的beginLayoutLw() 方法中对系统的SystemBar进行布局的。
frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {
...代码省略...
public void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {
displayFrames.onBeginLayout();
mSystemGestures.screenWidth = displayFrames.mUnrestricted.width();
mSystemGestures.screenHeight = displayFrames.mUnrestricted.height();
mDockLayer = 0x10000000;
mStatusBarLayer = -1;
final Rect pf = mTmpParentFrame;
final Rect df = mTmpDisplayFrame;
final Rect of = mTmpOverscanFrame;
final Rect vf = mTmpVisibleFrame;
final Rect dcf = mTmpDecorFrame;
vf.set(displayFrames.mDock);
of.set(displayFrames.mDock);
df.set(displayFrames.mDock);
pf.set(displayFrames.mDock);
dcf.setEmpty(); // Decor frame N/A for system bars.
if (displayFrames.mDisplayId == DEFAULT_DISPLAY) {
// For purposes of putting out fake window up to steal focus, we will
// drive nav being hidden only by whether it is requested.
//获取窗口systemui的标记类型
final int sysui = mLastSystemUiFlags;
//navigationBar是否可见
boolean navVisible = (sysui & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
//navigationBar是否是半透明的
boolean navTranslucent = (sysui & (View.NAVIGATION_BAR_TRANSLUCENT | View.NAVIGATION_BAR_TRANSPARENT)) != 0;
boolean immersive = (sysui & View.SYSTEM_UI_FLAG_IMMERSIVE) != 0;
boolean immersiveSticky = (sysui & View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0;
//navigationBar是否允许隐藏
boolean navAllowedHidden = immersive || immersiveSticky;
navTranslucent &= !immersiveSticky; // transient trumps translucent
boolean isKeyguardShowing = isStatusBarKeyguard() && !mKeyguardOccluded;
if (!isKeyguardShowing) {
navTranslucent &= areTranslucentBarsAllowed();
}
boolean statusBarExpandedNotKeyguard = !isKeyguardShowing && mStatusBar != null
&& mStatusBar.getAttrs().height == MATCH_PARENT
&& mStatusBar.getAttrs().width == MATCH_PARENT;
if (navVisible || navAllowedHidden) {
if (mInputConsumer != null) {
mHandler.sendMessage(
mHandler.obtainMessage(MSG_DISPOSE_INPUT_CONSUMER, mInputConsumer));
mInputConsumer = null;
}
} else if (mInputConsumer == null && mStatusBar != null && canHideNavigationBar()) {
mInputConsumer = mWindowManagerFuncs.createInputConsumer(mHandler.getLooper(),
INPUT_CONSUMER_NAVIGATION,
(channel, looper) -> new HideNavInputEventReceiver(channel, looper));
InputManager.getInstance().setPointerIconType(PointerIcon.TYPE_NULL);
}
navVisible |= !canHideNavigationBar();
//调用layoutNavigationBar对NavigationBar进行布局
boolean updateSysUiVisibility = layoutNavigationBar(displayFrames, uiMode, dcf,
navVisible, navTranslucent, navAllowedHidden, statusBarExpandedNotKeyguard);
if (DEBUG_LAYOUT) Slog.i(TAG, "mDock rect:" + displayFrames.mDock);
//调用layoutStatusBar对StatusBar进行布局
updateSysUiVisibility |= layoutStatusBar(
displayFrames, pf, df, of, vf, dcf, sysui, isKeyguardShowing);
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
}
layoutScreenDecorWindows(displayFrames, pf, df, dcf);
if (displayFrames.mDisplayCutoutSafe.top > displayFrames.mUnrestricted.top) {
displayFrames.mDisplayCutoutSafe.top = Math.max(displayFrames.mDisplayCutoutSafe.top,
displayFrames.mStable.top);
}
}
...代码省略...
}
beginLayoutLw方法中首先会判断NavigationBar是否可见以及是否半透明,然后会
调用layoutNavigationBar对NavigationBar进行布局,随后还会调用layoutStatusBar对StatusBar进行布局。
三、布局导航栏的关键方法layoutNavigationBar
1、下面是layoutNavigationBar的相关代码,layoutNavigationBar方法首先会调用 navigationBarPosition()方法返回 NavigationBar 的位置,根据返回的位置的不同所执行的布局方式也会不同。这里以布局在下边为例,会计算 NavigationBar 的 top,然后更新 mTmpNavigationFrame和 DisplayFrames 的相关属性。其中比较重要的是更新DisplayFrames.mDock变量,然后用这个变量设置 mCurrent、mVoiceContent、mContent` 等变量。
private boolean layoutNavigationBar(DisplayFrames displayFrames, int uiMode, Rect dcf,
boolean navVisible, boolean navTranslucent, boolean navAllowedHidden,
boolean statusBarExpandedNotKeyguard) {
if (mNavigationBar == null) {
return false;
}
boolean transientNavBarShowing = mNavigationBarController.isTransientShowing();
//根据屏幕旋转角度,我们需要为导航栏设置相对应的合适位置和大小
final int rotation = displayFrames.mRotation;
final int displayHeight = displayFrames.mDisplayHeight;
final int displayWidth = displayFrames.mDisplayWidth;
final Rect dockFrame = displayFrames.mDock;
//获取导航栏的位置
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);
//cutoutSafeUnrestricted是安全的窗口(Android9针对刘海平新增的),当没有Overscan的时候与mUnrestricted相同
//即cutoutSafeUnrestricted.bottom的值与DisplayFrames.mDisplayHeight值相同。
final Rect cutoutSafeUnrestricted = mTmpRect;
cutoutSafeUnrestricted.set(displayFrames.mUnrestricted);
cutoutSafeUnrestricted.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
//导航栏在底部
if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
//计算导航栏的top,
final int top = cutoutSafeUnrestricted.bottom - getNavigationBarHeight(rotation, uiMode);
//mTmpNavigationFrame就是NavigationBar所对应的窗口区域。
mTmpNavigationFrame.set(0, top, displayWidth, displayFrames.mUnrestricted.bottom);
//mStable对应应用窗口的显示区域,mFullscreen对应应用窗口全屏的显示区域,这里的设置使得应用窗口正常状态和全屏的时候都在导航栏的上方
displayFrames.mStable.bottom = displayFrames.mStableFullscreen.bottom = top;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
} else if (navVisible) {
//如果NavigationBar可见的话,更新dockFrame、mRestricted、mRestrictedOverscan的bottom值
mNavigationBarController.setBarShowingLw(true);
dockFrame.bottom = displayFrames.mRestricted.bottom = displayFrames.mRestrictedOverscan.bottom = top;
} else {
// We currently want to hide the navigation UI - unless we expanded the status bar.
mNavigationBarController.setBarShowingLw(statusBarExpandedNotKeyguard);
}
if (navVisible && !navTranslucent && !navAllowedHidden
&& !mNavigationBar.isAnimatingLw()
&& !mNavigationBarController.wasRecentlyTranslucent()) {
// If the opaque nav bar is currently requested to be visible and not in the process
// of animating on or off, then we can tell the app that it is covered by it.
displayFrames.mSystem.bottom = top;
}
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
...代码省略...
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
...代码省略...
}
//使用dockFrame的参数去更新mCurrent,mVoiceContent,mContent
displayFrames.mCurrent.set(dockFrame);
displayFrames.mVoiceContent.set(dockFrame);
displayFrames.mContent.set(dockFrame);
mStatusBarLayer = mNavigationBar.getSurfaceLayer();
//计算NavigationBar的contentFrame大小
mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe, mTmpNavigationFrame, dcf,
mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe,
displayFrames.mDisplayCutout, false /* parentFrameWasClippedByDisplayCutout */);
//将NavigationBar的contentFrame绑定到mNavigationBarController。
mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + mTmpNavigationFrame);
return mNavigationBarController.checkHiddenLw();
}
NavigationBar 的 top 是 cutoutSafeUnrestricted.bottom 减去 NavigationBar 高度的结果。cutoutSafeUnrestricted 是安全的窗口(Android 9 针对刘海平新增的),当没有 Overscan 的时候与 mUnrestricted 相同,即 cutoutSafeUnrestricted.bottom 的值与 DisplayFrames.mDisplayHeight 值相同。计算 NavigationBar 的 top 后设置 mTmpNavigationFrame。mTmpNavigationFrame 就是 NavigationBar 的窗口区域。
之后更新 mStable 和 mFullscreen 的 bottom 值为 top。mStable 就是应用窗口的显示区域,这就是 NavigationBar 占用应用显示区域的原因。
如果 NavigationBar 可见的话更新 dockFrame、mRestricted、mRestrictedOverscan 的 bottom 值。然后用 dockFrame 去更新 mCurrent、mVoiceContent、mContent。
最后调用 computeFrameLw() 方法计算 NavigationBar 的 contentFrame 大小,然后绑定到 mNavigationBarController。
四、布局状态栏的关键方法layoutStatusBar
StatusBar 的布局在 PhoneWindowManager 的 beginLayoutLw() 方法中,调用 layoutStatusBar() 方法。
frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {
@Override
public void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {
boolean updateSysUiVisibility |= layoutNavigationBar(displayFrames, uiMode, dcf,
navVisible, navTranslucent, navAllowedHidden, statusBarExpandedNotKeyguard);
updateSysUiVisibility |= layoutStatusBar(
displayFrames, pf, df, of, vf, dcf, sysui, isKeyguardShowing);
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
}
private boolean layoutStatusBar(DisplayFrames displayFrames, Rect pf, Rect df, Rect of, Rect vf,
Rect dcf, int sysui, boolean isKeyguardShowing) {
// decide where the status bar goes ahead of time
if (mStatusBar == null) {
return false;
}
// apply any navigation bar insets
of.set(displayFrames.mUnrestricted);
df.set(displayFrames.mUnrestricted);
pf.set(displayFrames.mUnrestricted);
vf.set(displayFrames.mStable);
mStatusBarLayer = mStatusBar.getSurfaceLayer();
// Let the status bar determine its size.
mStatusBar.computeFrameLw(pf /* parentFrame */, df /* displayFrame */,
vf /* overlayFrame */, vf /* contentFrame */, vf /* visibleFrame */,
dcf /* decorFrame */, vf /* stableFrame */, vf /* outsetFrame */,
displayFrames.mDisplayCutout, false /* parentFrameWasClippedByDisplayCutout */);
// For layout, the status bar is always at the top with our fixed height.
displayFrames.mStable.top = displayFrames.mUnrestricted.top
+ mStatusBarHeightForRotation[displayFrames.mRotation];
// Make sure the status bar covers the entire cutout height
displayFrames.mStable.top = Math.max(displayFrames.mStable.top,
displayFrames.mDisplayCutoutSafe.top);
// Tell the bar controller where the collapsed status bar content is
mTmpRect.set(mStatusBar.getContentFrameLw());
mTmpRect.intersect(displayFrames.mDisplayCutoutSafe);
mTmpRect.top = mStatusBar.getContentFrameLw().top; // Ignore top display cutout inset
mTmpRect.bottom = displayFrames.mStable.top; // Use collapsed status bar size
mStatusBarController.setContentFrame(mTmpRect);
boolean statusBarTransient = (sysui & View.STATUS_BAR_TRANSIENT) != 0;
boolean statusBarTranslucent = (sysui
& (View.STATUS_BAR_TRANSLUCENT | View.STATUS_BAR_TRANSPARENT)) != 0;
if (!isKeyguardShowing) {
statusBarTranslucent &= areTranslucentBarsAllowed();
}
// If the status bar is hidden, we don't want to cause windows behind it to scroll.
if (mStatusBar.isVisibleLw() && !statusBarTransient) {
// Status bar may go away, so the screen area it occupies is available to apps but just
// covering them when the status bar is visible.
final Rect dockFrame = displayFrames.mDock;
dockFrame.top = displayFrames.mStable.top;
displayFrames.mContent.set(dockFrame);
displayFrames.mVoiceContent.set(dockFrame);
displayFrames.mCurrent.set(dockFrame);
if (DEBUG_LAYOUT) Slog.v(TAG, "Status bar: " + String.format(
"dock=%s content=%s cur=%s", dockFrame.toString(),
displayFrames.mContent.toString(), displayFrames.mCurrent.toString()));
if (!mStatusBar.isAnimatingLw() && !statusBarTranslucent
&& !mStatusBarController.wasRecentlyTranslucent()) {
// If the opaque status bar is currently requested to be visible, and not in the
// process of animating on or off, then we can tell the app that it is covered by it.
displayFrames.mSystem.top = displayFrames.mStable.top;
}
}
return mStatusBarController.checkHiddenLw();
}}
首先设定 of /* overscanFrame /、df / displayFrame /、pf / parentFrame /、vf / visibleFrame */,其中 of、df、pf 设定为 displayFrames.mUnrestricted,即屏幕大小。vf 设定为 displayFrames.mStable,mStable 的大小在调用 layoutNavigationBar() 方法后变成了除去 NavigationBar 的窗口。然后调用 mStatusBar.computeFrameLw() 方法计算 StatusBar 的 mContentFrame 大小。mContentFrame 在 IME 不存在时与 mDecorFrame 相同,IME 存在时是 mDecorFrame 除去 IME 窗口的大小。
// apply any navigation bar insets
of.set(displayFrames.mUnrestricted);
df.set(displayFrames.mUnrestricted);
pf.set(displayFrames.mUnrestricted);
vf.set(displayFrames.mStable);
mStatusBarLayer = mStatusBar.getSurfaceLayer();
// Let the status bar determine its size.
mStatusBar.computeFrameLw(pf /* parentFrame */, df /* displayFrame */,
vf /* overlayFrame */, vf /* contentFrame */, vf /* visibleFrame */,
dcf /* decorFrame */, vf /* stableFrame */, vf /* outsetFrame */,
displayFrames.mDisplayCutout, false /* parentFrameWasClippedByDisplayCutout */);
下一步修改 displayFrames.mStable,即应用的窗口。因为 StatusBar 默认显示在顶部的,所以修改 mStable.top 值。
// For layout, the status bar is always at the top with our fixed height.
displayFrames.mStable.top = displayFrames.mUnrestricted.top
+ mStatusBarHeightForRotation[displayFrames.mRotation];
// Make sure the status bar covers the entire cutout height
displayFrames.mStable.top = Math.max(displayFrames.mStable.top,
displayFrames.mDisplayCutoutSafe.top);
计算 StatusBar 的可显示区域,绑定到 mStatusBarController。
// Tell the bar controller where the collapsed status bar content is
mTmpRect.set(mStatusBar.getContentFrameLw());
mTmpRect.intersect(displayFrames.mDisplayCutoutSafe);
mTmpRect.top = mStatusBar.getContentFrameLw().top; // Ignore top display cutout inset
mTmpRect.bottom = displayFrames.mStable.top; // Use collapsed status bar size
mStatusBarController.setContentFrame(mTmpRect);
判断 StatusBar 是否是短暂显示的(Transient)或是半透明的(Translucent)。其中 sysui 就是 mLastSystemUiFlags。
boolean statusBarTransient = (sysui & View.STATUS_BAR_TRANSIENT) != 0;
boolean statusBarTranslucent = (sysui
& (View.STATUS_BAR_TRANSLUCENT | View.STATUS_BAR_TRANSPARENT)) != 0;
if (!isKeyguardShowing) {
statusBarTranslucent &= areTranslucentBarsAllowed();
}
如果 StatusBar 是可见的,且不是暂时显示的,则修改 displayFrames 的 mDock、mContent、mVoiceContent、mCurrent,其实就是除去 StatusBar 的窗口大小。还有,如果 StatusBar 是不透明的,则修改 mSystem.top 值为 mStable.top 值,这时 Activity 使用 @android:style/Theme.NoTitleBar.Fullscreen 也不会隐藏 StatusBar。
// If the status bar is hidden, we don't want to cause windows behind it to scroll.
if (mStatusBar.isVisibleLw() && !statusBarTransient) {
// Status bar may go away, so the screen area it occupies is available to apps but just
// covering them when the status bar is visible.
final Rect dockFrame = displayFrames.mDock;
dockFrame.top = displayFrames.mStable.top;
displayFrames.mContent.set(dockFrame);
displayFrames.mVoiceContent.set(dockFrame);
displayFrames.mCurrent.set(dockFrame);
if (DEBUG_LAYOUT) Slog.v(TAG, "Status bar: " + String.format(
"dock=%s content=%s cur=%s", dockFrame.toString(),
displayFrames.mContent.toString(), displayFrames.mCurrent.toString()));
if (!mStatusBar.isAnimatingLw() && !statusBarTranslucent
&& !mStatusBarController.wasRecentlyTranslucent()) {
// If the opaque status bar is currently requested to be visible, and not in the
// process of animating on or off, then we can tell the app that it is covered by it.
displayFrames.mSystem.top = displayFrames.mStable.top;
}
}
参考文章:https://xianzhu21.space/developer/navigationbar-statusbar-frame-and-layout/