基于 Android 9 源码。

0. 概述

NavigationBar 和 StatusBar 都属于 SystemBar,也叫做 decor,就是说给 App 装饰的意思。一般的 window 的布局是在 PhoneWindowManager 的 layoutWindowLw() 方法中,而 SystemBar 是在 beginLayoutLw() 方法中布局。

当前最上层的 Activity 可以修改 SystemBar 的 visibility,可以调用 View#setSystemUiVisibility() 方法,系统也有一些针对 SystemBar visibility 的策略。最终的 visibility 保存在 PhoneWindowManager 中的 mLastSystemUiFlags 变量中。

1. NavigationBar 的布局

PhoneWindowManager 实现了 WindowManagerPolicy 接口。beginLayoutLw() 中会调用 layoutNavigationBar() 方法布局 NavigationBar。

layoutNavigationBar() 方法会先调用 navigationBarPosition() 方法返回 NavigationBar 的位置,根据不同的位置布局方式不同。以布局在下边为例,会计算 NavigationBar 的 top,然后更新 mTmpNavigationFrame 和 DisplayFrames 的相关属性。其中比较重要的是更新 DisplayFrames.mDock 变量,然后用这个变量设置 mCurrentmVoiceContentmContent 等变量。DisplayFrames 的各个属性在「DisplayFrames 分析:Android 中都有哪些 Frame?」有说明。

// frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {
    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();
        // Force the navigation bar to its appropriate place and size. We need to do this directly,
        // instead of relying on it to bubble up from the nav bar, because this needs to change
        // atomically with screen rotations.
        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);

        if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
            // It's a system nav bar or a portrait screen; nav bar goes on bottom.
            final int top = cutoutSafeUnrestricted.bottom
                    - getNavigationBarHeight(rotation, uiMode);
            mTmpNavigationFrame.set(0, top, displayWidth, displayFrames.mUnrestricted.bottom);
            displayFrames.mStable.bottom = displayFrames.mStableFullscreen.bottom = top;
            if (transientNavBarShowing) {
                mNavigationBarController.setBarShowingLw(true);
            } else if (navVisible) {
                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) {
            // Landscape screen; nav bar goes to the right.
            ...
        } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
            // Seascape screen; nav bar goes to the left.
            ...
        }

        // Make sure the content and current rectangles are updated to account for the restrictions
        // from the navigation bar.
        displayFrames.mCurrent.set(dockFrame);
        displayFrames.mVoiceContent.set(dockFrame);
        displayFrames.mContent.set(dockFrame);
        mStatusBarLayer = mNavigationBar.getSurfaceLayer();
        // And compute the final frame.
        mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
                mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe, mTmpNavigationFrame, dcf,
                mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe,
                displayFrames.mDisplayCutout, false /* parentFrameWasClippedByDisplayCutout */);
        mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
        return mNavigationBarController.checkHiddenLw();
    }
}

NavigationBar 的 top 是 cutoutSafeUnrestricted.bottom 减去 NavigationBar 高度的结果。cutoutSafeUnrestricted 是安全的窗口(Android 9 针对刘海平新增的),当没有 Overscan 的时候与 mUnrestricted 相同,即 cutoutSafeUnrestricted.bottom 的值与 DisplayFrames.mDisplayHeight 值相同。计算 NavigationBar 的 top 后设置 mTmpNavigationFramemTmpNavigationFrame 就是 NavigationBar 的窗口区域。

如果 NavigationBar 可见的话更新 dockFramemRestrictedmRestrictedOverscan 的 bottom 值。然后用 dockFrame 去更新 mCurrentmVoiceContentmContent

最后调用 computeFrameLw() 方法计算 NavigationBar 的 contentFrame 大小,然后绑定到 mNavigationBarController

2. StatusBar 的布局

StatusBar 的布局在 PhoneWindowManager 的 beginLayoutLw() 方法中,调用 layoutStatusBar() 方法。

首先设定 of /* overscanFrame */df /* displayFrame */pf /* parentFrame */vf /* visibleFrame */,其中 ofdfpf 设定为 displayFrames.mUnrestricted,即屏幕大小。vf 设定为 displayFrames.mStablemStable 的大小在调用 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);