欢迎转载,转载请注明出处 xianzhu21.space

基于 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 的窗口区域。

之后更新 mStablemFullscreen 的 bottom 值为 top。mStable 就是应用窗口的显示区域,这就是 NavigationBar 占用应用显示区域的原因。

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

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

2. StatusBar 的布局

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 */,其中 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);

计算 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 的 mDockmContentmVoiceContentmCurrent,其实就是除去 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;
        }
    }

Xianzhu21

Xianzhu21
An Android framework developer.

Window Touchable Region

当用户触屏后,InputReader 从驱动读取一个输入事件加入到队列,InputDispatcher 从队列中读取一个输入事件准备分发。如果该输入事件是一个触摸事件...
Continue reading

Dropping event bug in Dialog

Published on March 10, 2020

InputChannel and InputDispatcher in Android

Published on February 27, 2020