经过前面的五篇WMS文章,我们基本把整个WMS的流程走了一遍。在上一篇的文章里面,我们说了这篇文章会分析一下关于窗口的测量和布局,我们知道view有测量,布局,绘制三个大步骤,同样窗口也有测量和布局,测量就是计算每个窗口的大小,而布局是在测量的基础上,不同一个屏幕的窗口进行有效的排列,比如最上面是状态栏窗口,中间是用户的Activity窗口,底部是导航栏窗口。
# 测量和布局窗口的入口performLayout方法
熟悉view的同学也许知道,view的测量布局过程中调用的方法和窗口测量布局有些是相同的,所以如果熟悉view的流程的话,对于窗口的绘制肯定也是熟悉的。我们这里主要是分析WMS的文章,所以这里不对view的部分展开,我们这里重点分析前面文章中分析到的设计窗口的方法。在前面的文章中,我们有分析到,如果焦点窗口有变化的话,会调用到WindowManagerService的updateFocusedWindowLocked方法,这个方法里面会调用DisplayContent中的performLayout方法,这个方法会对当前屏幕中的所有窗口进行测量,我们就从这个方法开始看,这个方法对于了解窗口部分的测量是足够了,其他关于View的部分,我们在分析View的时候还会说。
```java
void performLayout(boolean initial, boolean updateInputWindows) {
// 没有需要重新布局的标志,return
if (!isLayoutNeeded()) {
return;
}
// 清除触发布局的标志
clearLayoutNeeded();
// 获取屏幕宽高
final int dw = mDisplayInfo.logicalWidth;
final int dh = mDisplayInfo.logicalHeight;
if (DEBUG_LAYOUT) {
Slog.v(TAG, "-------------------------------------");
Slog.v(TAG, "performLayout: needed=" + isLayoutNeeded() + " dw=" + dw + " dh=" + dh);
}
// 初始化一些区域的rect,比如什么过扫描,内容区域,可见区域等,并且测量状态栏和导航栏
mService.mPolicy.beginLayoutLw(isDefaultDisplay, dw, dh, mRotation,
getConfiguration().uiMode);
// 如果是默认屏幕,设置屏幕大小
if (isDefaultDisplay) {
// Not needed on non-default displays.
// 获取状态栏和导航栏的Z轴层序
mService.mSystemDecorLayer = mService.mPolicy.getSystemDecorLayerLw();
// 设置屏幕大小
mService.mScreenRect.set(0, 0, dw, dh);
}
// 获取内容区域,赋给mContentRect
mService.mPolicy.getContentRectLw(mContentRect);
.................
}
```
这个方法我们分2部分来看,以上是第一部分。首先调用isLayoutNeeded方法看看,需不需要重新测量布局,这个在窗口内容有变化的时候都会调用setLayoutNeeded方法设置,表示需要重新测量布局,比如像我们现在焦点窗口变化了就会需要重新测量布局,而且之后我们分析view的时候也会看到,需要对view重新测量布局的时候,也会调用这个方法,所以这个逻辑对窗口和view是一样的。
如果需要重新测量就继续下去调用clearLayoutNeeded方法,这个方法很简单,就是把上面需要重新测量布局的标记重置:
```java
private void clearLayoutNeeded() {
if (DEBUG_LAYOUT) Slog.w(TAG_WM, "clearLayoutNeeded: callers=" + Debug.getCallers(3));
mLayoutNeeded = false;
}
```
这个方法看一眼就行,没什么好多说的。我们回到前面,接着会调用PhoneWindowManager的beginLayoutLw方法,这个方法算是一个关键的方法,里面会测量各种不同区域的尺寸,同时这个方法里面会测量状态栏和导航栏的尺寸,有了状态栏和导航栏,其他的窗口布局就可以知道怎么排列了,比如我们常见的都会排在状态栏的下面。这个方法我们稍等在进入看,我们先往下看现在这个方法。
之后如果是默认屏幕的话,会调用getSystemDecorLayerLw方法获取系统装饰栏的z轴层序,然后把当前屏幕大小保存在windowManagerService的mScreenRect中,这里屏幕大小是从DisplayInfo中获取的逻辑屏幕大小。这里所谓装饰栏就是指的状态栏和导航栏,我们知道一般屏幕上除了我们自己的窗口外,主要就是这2个窗口会显示在其他窗口上面,所以这里会获取他们的z轴层序,我们看下获取的方法:
```java
public int getSystemDecorLayerLw() {
if (mStatusBar != null && mStatusBar.isVisibleLw()) {
return mStatusBar.getSurfaceLayer();
}
if (mNavigationBar != null && mNavigationBar.isVisibleLw()) {
return mNavigationBar.getSurfaceLayer();
}
return 0;
}
```
这个方法可以看到,首先会获取状态栏的z轴层序,如果没有状态栏的话在获取导航栏的z轴层序,上面方法的逻辑很清楚,就不多说了。我们回到前面performLayout方法,最后会调用PhoneWindowManager中的getContentRectLw方法获取用户的内容区域,我们先看下这个方法:
```java
public void getContentRectLw(Rect r) {
r.set(mContentLeft, mContentTop, mContentRight, mContentBottom);
}
```
这个方法很简单,就是把内容区域的四个边赋值给传入的参数,这个参数是保存在DisplayContent中的。这里内容区域的四个参数是前面调用beginLayoutLw方法时候初始化的,前面说过的beginLayoutLw方法会初始化一些窗口的区域,所以这里的主要问题是什么是内容区域?这里先简单的理解,我们有说过装饰栏就是状态栏和导航栏,而内容区域简单说就是中间我们自己的窗口了,这里就先说这一句,我们下面分析beginLayoutLw时候,会重点先把这些概念梳理一下,现在我们先把这个方法看完:
```java
void performLayout(boolean initial, boolean updateInputWindows) {
.............
// 设置layout次数
int seq = mService.mLayoutSeq + 1;
if (seq < 0) seq = 0;
// 把layout序号赋给mLayoutSeq
mService.mLayoutSeq = seq;
// Used to indicate that we have processed the dream window and all additional windows are
// behind it.
mTmpWindow = null;
mTmpInitial = initial;
// 第一次遍历每个没有父窗口的窗口,比如根窗口,对他们进行测量布局
// First perform layout of any root windows (not attached to another window).
forAllWindows(mPerformLayout, true /* traverseTopToBottom */);
// Used to indicate that we have processed the dream window and all additional attached
// windows are behind it.
mTmpWindow2 = mTmpWindow;
mTmpWindow = null;
// Now perform layout of attached windows, which usually depend on the position of the
// window they are attached to. XXX does not deal with windows that are attached to windows
// that are themselves attached.
// 对已经有父窗口的进行测量布局
forAllWindows(mPerformLayoutAttached, true /* traverseTopToBottom */);
// Window frames may have changed. Tell the input dispatcher about it.
mService.mInputMonitor.layoutInputConsumers(dw, dh);
mService.mInputMonitor.setUpdateInputWindowsNeededLw();
if (updateInputWindows) {
mService.mInputMonitor.updateInputWindowsLw(false /*force*/);
}
mService.mPolicy.finishLayoutLw();
mService.mH.sendEmptyMessage(UPDATE_DOCKED_STACK_DIVIDER);
}
```
这是performLayout方法的后半部分。前面部分最重要的方法就是beginLayoutLw,这个方法我们后面会看到主要就是对一些窗口区域进行初始化,所以你可以理解为,前半部分是对窗口进行测量,那么后半部分就是在前半部分测量的基础上,对屏幕中的每个窗口进行布局了。所以这里你可以看到有两个forAllWindows方法,这个是对屏幕中所有的窗口进行遍历,第一个参数是一个lambda表达式,每次遍历到的测量过程就是这个lambda表达式,我们主要就看这两个lambda表达式就可以了,这里的2个forAllWindows方法,一个是对没有父窗口的窗口,比如Activity进行布局,另一个是对有父窗口的窗口进行布局,稍后我们会进入这2个lambda表达式看下是怎么对一个窗口布局的。最后就是根据窗口是否有变化,对输入事件的处理,这方面这里不多说。
# 几个窗口区域的介绍
performLayout方法分析完了,下面我们会进入里面调用的几个方法看一下。一个是beginLayoutLw方法,是对屏幕各个区域尺寸初始化,然后还会对状态栏和导航栏进行布局。另外就是两个lambda表达式,会对屏幕里的窗口进行布局。我们会先看下beginLayoutLw方法,在看beginLayoutLw方法之前,我们先介绍下一下在beginLayoutLw中将会看到了一些概念。
我们先来说下几个窗口的区域,我们在之前分析WMS文章里面也遇到过几个区域,当时简单提了一下,现在详细的说一下。
1. stable区域。一般来说我们代码中看到stableFrame,stableInset之类的变量,就是指stable区域,这个区域主要是状态栏和导航栏所占用的区域,不管实际状态栏和导航栏有没有显示,这个区域都是一个定制。
2.stableFull区域。与前面stable对应的有个stableFull区域,他和stabl区域差别就是他只包含导航栏的区域,不包含状态栏,其他和都一样。
3.Dock区域。这个区域就是指输入法窗口占用的区域,输入法区域一般是排除掉状态栏和导航栏的区域,他是根据装饰栏变化的而改变,但是原则就是不能覆盖装饰栏,比如默认的情况Dock区域会覆盖导航栏位置,如果导航栏显示的话,Dock区域就会显示在导航栏上面。
4. content区域。内容区域就是常见的我们自己开发的窗口区域,正常情况下他和Dock区域一样的,但是如果有输入法弹出的话,会根据输入法显示模式变化。比如我们常见的输入法弹出后,界面会随着输入法一起往上移动,这种情况下content区域就会变了,他的底部就在输入法的上面了。而如果界面的不随着输入法移动的话,那么content区域就是不变的,和输入法弹出前是一样的。
5. visible区域。可见区域和内容区域是类似的,还是拿上面输入法弹窗情况距离,内容区域会根据软键盘的设置模式变化,而可见区域就比较直接,不管软键盘怎么设置,反正可见区域就是可以被用户看到的区域,所以当键盘不弹出的时候,可见区域和内容区域是一样的,当弹出软键盘后,可见区域就可能和内容区域不一样了。
6. overScan区域。过扫描区域一般指屏幕边上有一些小区域,是不会被用户显示的,比如我们看到的黑边,过扫描区域是用来描述这些的。
7. system区域。系统区域如果在装饰栏有显示的时候,不会包含装饰栏区域,也就是不会包含状态栏和导航栏,如果装饰区域不显示的时候,就会包含逻辑屏幕的区域。
以上几个区域大致包含了后面代码中我们需要知道的一些区域的概念,后面的代码中还会看到一个cur区域,也就是当前区域,可以理解为可见区域,这个区域并不是一个固定的窗口概念,但是有些方法里面会出现,这里先提一下。
看完了上面介绍的几个窗口区域,下面这张图简单的标了一下大致各个区域的情况,这张图主要是在状态栏和导航栏都存在的情况下,那些固定区域的位置,有些位置不固定的就没标上,比如像输入法Dock区域根据导航栏是否显示底部会不一样,包括内容区域和可见区域的底部也会有变化,所以一些区域的底部就没标上,但是大部分顶部区域差不多是这样,可以稍稍参考下这个图来理解。

这个图的中间矩形就是一个屏幕,可以看到这里画了两层,最外面一层就是整个屏幕,这里也就是DisplayInfo获取的屏幕宽和高。第一层和第二层之间就是过扫描区域,我们简单理解为屏幕的黑边就行,大多数屏幕最外面会有很窄的一圈不会显示,这就是过扫描区域,不过黑边太宽也不美观,所以现在很多厂家都追求窄边框。最里面一层就是我们平时看到的内容了,可以看到这里上面和下面分别是状态栏和导航栏,中间一般是我们自己的界面。关于这里两层屏幕的含义,大致了解一下就行。
接着我们看这张图右边标出的就是前面介绍的几个区域,大家可以对照着前面的文字介绍来理解一下,这里Dock和Content区域图上只标了top,bottom由于根据导航栏的显示情况会有不同,所以这里没标,其余标出的区域大致不会发生变化。
然后我们看到图上的左边有几个变量,这几个变量下面在看beginLayoutLw方法的时候会看到,他们其实也就是对应着屏幕的不同区域,上面图中标出了他们的高度,后面代码中一些计算的地方会使用到他们,由于代码中这些值的初始化都是非常细的,尤其是一些计算公式的加加减减都是这些区域之间的互相计算,所以不可能分析到每个计算公式的程度,所以大家对细节可以对照着上面的图来看,这样就好理解了。
# 进入beginLayoutLw方法
好了,下面我们就开始看beginLayoutLw方法:
```java
public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight,
int displayRotation, int uiMode) {
mDisplayRotation = displayRotation;
// 过扫描区域的四个边距
final int overscanLeft, overscanTop, overscanRight, overscanBottom;
if (isDefaultDisplay) {
// 根据选择屏幕旋转方法,设置过扫描区域
switch (displayRotation) {
case Surface.ROTATION_90:
overscanLeft = mOverscanTop;
overscanTop = mOverscanRight;
overscanRight = mOverscanBottom;
overscanBottom = mOverscanLeft;
break;
case Surface.ROTATION_180:
overscanLeft = mOverscanRight;
overscanTop = mOverscanBottom;
overscanRight = mOverscanLeft;
overscanBottom = mOverscanTop;
break;
case Surface.ROTATION_270:
overscanLeft = mOverscanBottom;
overscanTop = mOverscanLeft;
overscanRight = mOverscanTop;
overscanBottom = mOverscanRight;
break;
default:
overscanLeft = mOverscanLeft;
overscanTop = mOverscanTop;
overscanRight = mOverscanRight;
overscanBottom = mOverscanBottom;
break;
}
} else {
// 非默认屏幕就不设置了
overscanLeft = 0;
overscanTop = 0;
overscanRight = 0;
overscanBottom = 0;
}
// 全屏的左边,包含过扫描区域。Restricted底部在有导航栏时候,不超过导航栏
mOverscanScreenLeft = mRestrictedOverscanScreenLeft = 0;
mOverscanScreenTop = mRestrictedOverscanScreenTop = 0;
mOverscanScreenWidth = mRestrictedOverscanScreenWidth = displayWidth;
mOverscanScreenHeight = mRestrictedOverscanScreenHeight = displayHeight;
// 排除系统UI时候的大小,如果状态里和导航栏都没有,就是全屏
mSystemLeft = 0;
mSystemTop = 0;
mSystemRight = displayWidth;
mSystemBottom = displayHeight;
// 不包括过扫描区域
mUnrestrictedScreenLeft = overscanLeft;
mUnrestrictedScreenTop = overscanTop;
mUnrestrictedScreenWidth = displayWidth - overscanLeft - overscanRight;
mUnrestrictedScreenHeight = displayHeight - overscanTop - overscanBottom;
// 排除导航栏后屏幕的可见区域
mRestrictedScreenLeft = mUnrestrictedScreenLeft;
mRestrictedScreenTop = mUnrestrictedScreenTop;
mRestrictedScreenWidth = mSystemGestures.screenWidth = mUnrestrictedScreenWidth;
mRestrictedScreenHeight = mSystemGestures.screenHeight = mUnrestrictedScreenHeight;
// 以下是输入法,用户内容,stalbleinset等的边距
mDockLeft = mContentLeft = mVoiceContentLeft = mStableLeft = mStableFullscreenLeft
= mCurLeft = mUnrestrictedScreenLeft;
mDockTop = mContentTop = mVoiceContentTop = mStableTop = mStableFullscreenTop
= mCurTop = mUnrestrictedScreenTop;
mDockRight = mContentRight = mVoiceContentRight = mStableRight = mStableFullscreenRight
= mCurRight = displayWidth - overscanRight;
mDockBottom = mContentBottom = mVoiceContentBottom = mStableBottom = mStableFullscreenBottom
= mCurBottom = displayHeight - overscanBottom;
// 输入法窗口z序,这个值设的很大,所以输入法可以在所有窗口之上
mDockLayer = 0x10000000;
// 状态栏窗口z序
mStatusBarLayer = -1;
// start with the current dock rect, which will be (0,0,displayWidth,displayHeight)
final Rect pf = mTmpParentFrame; // 父窗口
final Rect df = mTmpDisplayFrame; // 屏幕
final Rect of = mTmpOverscanFrame; // 过扫描
final Rect vf = mTmpVisibleFrame; // 可见区域
final Rect dcf = mTmpDecorFrame; // 不包含状态栏和导航栏,装饰区
// 设置这些区域的默认值
pf.left = df.left = of.left = vf.left = mDockLeft;
pf.top = df.top = of.top = vf.top = mDockTop;
pf.right = df.right = of.right = vf.right = mDockRight;
pf.bottom = df.bottom = of.bottom = vf.bottom = mDockBottom;
dcf.setEmpty(); // Decor frame N/A for system bars.
................
}
```
这个方法不算短,我们分几段来看,上面这个是第一段,这段主要就是初始化前面介绍的几个区域的默认值。这里的几个区域基本上也都介绍过,大家对应着上面的解释可以自己看下初始值,不过由于这里是初始化值,所以其实也不太准确,下面的代码还会根据具体的情况,比如是否导航栏显示等,重新计算其中的某些值。
这个有个mStatusBarLayer变量,代码着输入法窗口在Z轴的层序,我们知道输入法窗口一般是显示在最上面的,所以这里初始化值给了个很大的值,这样在绘制的时候可以绘制的最上面。后面还有个mStatusBarLayer,这个代表状态栏的Z轴层序,默认这里设置为-1,后面在布局状态栏的时候会更新这个值,我们后面会看到。第一段主要就是初始化各个区域,都是些计算的细节,大家可以对照注释和代码去理解,我们看下一段代码:
```java
public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight,
int displayRotation, int uiMode) {
......................
// 如果是默认屏幕
if (isDefaultDisplay) {
// systemUI的flag,主要有状态栏和导航栏
final int sysui = mLastSystemUiFlags;
// 是否显示导航栏
boolean navVisible = (sysui & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
// 导航栏背景透明
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;
// 是否上面2中沉浸模式之一
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) {
// 可以接受导航栏位置的输入事件,所以把mInputConsumer取消掉
if (mInputConsumer != null) {
mHandler.sendMessage(
mHandler.obtainMessage(MSG_DISPOSE_INPUT_CONSUMER, mInputConsumer));
mInputConsumer = null;
}
} else if (mInputConsumer == null) {
// 到这里说明导航栏不可见并且不是沉浸模式
// 创建mInputConsumer来处理导航栏部位的输入事件,这个只会侦测到这个部位的用户事件,不会起作用
mInputConsumer = mWindowManagerFuncs.createInputConsumer(mHandler.getLooper(),
INPUT_CONSUMER_NAVIGATION,
(channel, looper) -> new HideNavInputEventReceiver(channel, looper));
// As long as mInputConsumer is active, hover events are not dispatched to the app
// and the pointer icon is likely to become stale. Hide it to avoid confusion.
InputManager.getInstance().setPointerIconType(PointerIcon.TYPE_NULL);
}
navVisible |= !canHideNavigationBar();
..............
}
```
这段方法主要是处理和导航栏相关的。这里首先会根据systemUI的flag获取和导航栏相关的一些相关状态,比如导航栏是否显示,是否处于沉浸式状态等。如果导航栏显示或者处于SYSTEM_UI_FLAG_IMMERSIVE或SYSTEM_UI_FLAG_IMMERSIVE_STICKY这两种沉浸式的模式的话,导航栏是可以操作的。SYSTEM_UI_FLAG_IMMERSIVE或SYSTEM_UI_FLAG_IMMERSIVE_STICKY这两种模式一般会结合全屏来使用,当触碰屏幕的时候会显示出状态栏和导航栏,这个使用过的同学应该比较了解,这里稍微提一下这个场景。
一旦是上面这种情况的话,说明导航栏是可以操作的,这里会把InputConsumer这个类的对象置空,InputConsumer是一个处理输入事件的特殊接口,他有多个实现类,可以对特地的输入事件进行处理,这里的InputConsumer就是一个专门处理导航栏输入事件的实现类,由于这里可以对导航栏进行操作,所以这里把他置空。如果当前导航栏不是可见的,我们可以看到上面代码最后的else分支里面,会创建一个InputConsumer实现类来专门处理导航栏事件,这里类会捕获导航栏的输入事件,但是不会产生什么作用,这个我们知道一下就可以。
有些特殊设备可能便于用户的使用,不运行隐藏导航栏,所以最后会调用canHideNavigationBar方法来看下是否运行隐藏导航栏,这样就能最终判断是否导航栏是显示的。我们看最后一段代码:
```java
.....................
boolean updateSysUiVisibility = layoutNavigationBar(displayWidth, displayHeight,
displayRotation, uiMode, overscanLeft, overscanRight, overscanBottom, dcf, navVisible, navTranslucent,
navAllowedHidden, statusBarExpandedNotKeyguard);
updateSysUiVisibility |= layoutStatusBar(pf, df, of, vf, dcf, sysui, isKeyguardShowing);
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
```
最后这一段代码就开始进行对窗口的布局了,这里我们看到会分别对导航栏和状态栏进行布局,如果如果有更新的话,会对导航栏或者状态栏显示状态进行更新。这里我们会着重看下对导航栏或者状态栏的布局过程。首先我们看下对导航栏的布局,调用的是layoutNavigationBar方法:
# 对导航栏进行布局
```java
private boolean layoutNavigationBar(int displayWidth, int displayHeight, int displayRotation,
int uiMode, int overscanLeft, int overscanRight, int overscanBottom, Rect dcf,
boolean navVisible, boolean navTranslucent, boolean navAllowedHidden,
boolean statusBarExpandedNotKeyguard) {
if (mNavigationBar != null) { // 导航栏窗口非空
// 是否导航栏正在显示过程中
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.
// 获取导航栏的位置,底部,左部或者右部
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight,
displayRotation);
// 如果在底部的话
if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
// It's a system nav bar or a portrait screen; nav bar goes on bottom.
// 导航栏的顶部 = 屏幕高度 - 过扫描底部 - 导航栏高度
int top = displayHeight - overscanBottom
- getNavigationBarHeight(displayRotation, uiMode);
// 设置导航栏的Rect
mTmpNavigationFrame.set(0, top, displayWidth, displayHeight - overscanBottom);
// StableFrame和StableFullScreenFrame的底部 = 导航栏的顶部
mStableBottom = mStableFullscreenBottom = mTmpNavigationFrame.top;
// 导航栏正在显示过程中
if (transientNavBarShowing) {
// 设置显示导航栏
mNavigationBarController.setBarShowingLw(true);
} else if (navVisible) { // 进入到这里说明状态栏已经显示了
// 设置显示导航栏
mNavigationBarController.setBarShowingLw(true);
// 输入法窗口的底部 = 导航栏的顶部
mDockBottom = mTmpNavigationFrame.top;
// 可见区域的高度 = 输入法的底部 - 可见区域的顶部。这个应该是不包含状态栏
mRestrictedScreenHeight = mDockBottom - mRestrictedScreenTop;
// 同上。但是这个应该是只排除过扫描区域,但是包含状态栏
mRestrictedOverscanScreenHeight = mDockBottom - mRestrictedOverscanScreenTop;
} 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.
// 把系统UI排除在外后,底部 = 导航栏顶部
mSystemBottom = mTmpNavigationFrame.top;
}
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) { // 导航栏在右边
// Landscape screen; nav bar goes to the right.
// 到这里是横屏。
// 导航栏的left = 屏幕宽度 - 过扫描右边 - 导航栏横屏宽度
int left = displayWidth - overscanRight
- getNavigationBarWidth(displayRotation, uiMode);
// 设置导航栏rect
mTmpNavigationFrame.set(left, 0, displayWidth - overscanRight, displayHeight);
// stableRight和stableFullRight = 导航栏rect的左边
mStableRight = mStableFullscreenRight = mTmpNavigationFrame.left;
// 正在显示导航栏
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
} else if (navVisible) {
mNavigationBarController.setBarShowingLw(true);
// 输入法窗口右边 = 导航栏rect左边
mDockRight = mTmpNavigationFrame.left;
// 屏幕宽度
mRestrictedScreenWidth = mDockRight - mRestrictedScreenLeft;
mRestrictedOverscanScreenWidth = mDockRight - mRestrictedOverscanScreenLeft;
} 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 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.
// 把系统UI排除在外后可视区域,右边 = 导航栏左边
mSystemRight = mTmpNavigationFrame.left;
}
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
// Seascape screen; nav bar goes to the left.
// 这个和横屏差不多,只不过是横屏的180度
// 右边 = 过扫描left + 导航栏宽度
int right = overscanLeft + getNavigationBarWidth(displayRotation, uiMode);
// 设置导航栏rect
mTmpNavigationFrame.set(overscanLeft, 0, right, displayHeight);
// stableleft和stableFullLeft = 导航栏right
mStableLeft = mStableFullscreenLeft = mTmpNavigationFrame.right;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
} else if (navVisible) {
mNavigationBarController.setBarShowingLw(true);
mDockLeft = mTmpNavigationFrame.right;
// TODO: not so sure about those:
mRestrictedScreenLeft = mRestrictedOverscanScreenLeft = mDockLeft;
mRestrictedScreenWidth = mDockRight - mRestrictedScreenLeft;
mRestrictedOverscanScreenWidth = mDockRight - mRestrictedOverscanScreenLeft;
} 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 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.
// 不包括系统bar的可视区域left = 导航栏right
mSystemLeft = mTmpNavigationFrame.right;
}
}
..............
}
}
```
我们也分两段来看,上面这个是第一段,这段代码看起来很长,其实就做一件事情,根据导航栏显示在屏幕上的位置给他布局。一般来说在手机上我们看到导航栏都是显示在下面的,但是如果横屏的话,会显示在左边或者右边,这个方法开始调用navigationBarPosition方法来判断导航栏当前需要显示在哪个位置,我们跟进这个方法看下:
```java
private int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
if (mNavigationBarCanMove && displayWidth > displayHeight) {
if (displayRotation == Surface.ROTATION_270) {
return NAV_BAR_LEFT;
} else {
return NAV_BAR_RIGHT;
}
}
return NAV_BAR_BOTTOM;
}
```
这个方法很简单,通过当前屏幕宽高比,以及旋转方向来返回导航栏的位置,这里我们可以看到导航栏一共就三种位置,左边,右边和下边,接着就会根据这三种不同位置进行布局,我们回到前面layoutNavigationBar方法。
我们先来看导航栏在底部的时候。首先计算导航栏的顶部,这里计算公式为导航栏顶部 = 屏幕高度 - 过扫描区域的底部 - 导航栏高度,这样里的过扫描区域如下图:

通过这张图我们可以很清楚的理解最终导航栏顶部放置的位置了吧,还是用图来解释比较清楚,导航栏的顶部和底部都是去掉了过扫描区域后计算出来的,之后我们可以看到会把导航栏的大小保存在mTmpNavigationFrame这个变量,他是一个Rect,这个变量目前也是临时保存一下,最终的数据需要保存到导航栏窗口的WindowState中,我们在下面会看到。
计算好了导航栏的大小后,需要调用BarController的setBarShowingLw方法,对导航栏进行显示或者隐藏的设置,我们跟进这个方法看下:
```java
public boolean setBarShowingLw(final boolean show) {
if (mWin == null) return false;
// 如果bar正在隐藏过程中,但是要求显示,那么返回false
if (show && mTransientBarState == TRANSIENT_BAR_HIDING) {
mPendingShow = true;
return false;
}
// 是否当前这个窗口显示着
final boolean wasVis = mWin.isVisibleLw();
// 是否当前这个窗口正在动画中
final boolean wasAnim = mWin.isAnimatingLw();
// 要显示导航栏的话,调用WindowState的showLw方法,否则调用hideLw。
// 返回结果表示,是否显示或者隐藏的要求有改动
final boolean change = show ? mWin.showLw(!mNoAnimationOnNextShow && !skipAnimation())
: mWin.hideLw(!mNoAnimationOnNextShow && !skipAnimation());
mNoAnimationOnNextShow = false;
// 返回应该的显示状态
final int state = computeStateLw(wasVis, wasAnim, mWin, change);
// 是否状态有变化
final boolean stateChanged = updateStateLw(state);
// 状态变化的回调函数
if (change && (mVisibilityChangeListener != null)) {
mHandler.obtainMessage(MSG_NAV_BAR_VISIBILITY_CHANGED, show ? 1 : 0, 0).sendToTarget();
}
return change || stateChanged;
}
```
这个方法开始会做一些检查判断,如果需要显示的话,但是当前导航栏处于正在隐藏的状态,那么会标记一下在等待显示,暂时就不处理。如果一切正常的话,会根据是要显示还是非显示调用showLw或者hideLw方法,我们先来看下showLw方法,这个方法是为显示一个窗口做准备:
```java
boolean showLw(boolean doAnimation, boolean requestAnim) {
// 这个窗口由于是一个全屏或者userId问题,不能显示,所以这个返回false
if (isHiddenFromUserLocked()) {
return false;
}
// 这个窗口的由于权限问题,不能显示,返回false
if (!mAppOpVisibility) {
// Being hidden due to app op request.
return false;
}
// 为true这个窗口不能再次被显示,返回false
if (mPermanentlyHidden) {
// Permanently hidden until the app exists as apps aren't prepared
// to handle their windows being removed from under them.
return false;
}
// 已经是可以显示状态了,返回false
if (mPolicyVisibility && mPolicyVisibilityAfterAnim) {
// Already showing.
return false;
}
if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility true: " + this);
if (doAnimation) { // 窗口需要执行动画
if (DEBUG_VISIBILITY) Slog.v(TAG, "doAnimation: mPolicyVisibility="
+ mPolicyVisibility + " mAnimation=" + mWinAnimator.mAnimation);
// 屏幕不可以显示
if (!mService.okToDisplay()) {
doAnimation = false; // 不能执行动画
} else if (mPolicyVisibility && mWinAnimator.mAnimation == null) {
// 到这里屏幕可以显示,但是没有可以执行的动画,把执行窗口动画doAnimation设置为false
// Check for the case where we are currently visible and
// not animating; we do not want to do animation at such a
// point to become visible when we already are.
doAnimation = false;
}
}
// 下面2个窗口可见性设置为true
mPolicyVisibility = true;
mPolicyVisibilityAfterAnim = true;
if (doAnimation) { // 如果有窗口动画的话
// 更新当前进入窗口的动画到窗口的动画变量中
mWinAnimator.applyAnimationLocked(WindowManagerPolicy.TRANSIT_ENTER, true);
}
// 要求有窗口动画的话,准备启动动画
if (requestAnim) {
mService.scheduleAnimationLocked();
}
// 如果窗口可以接受输入事件,那么这个可能作为一个焦点窗口,下面重新更新所有窗口的测量,布局等
if ((mAttrs.flags & FLAG_NOT_FOCUSABLE) == 0) {
// 更新最新的焦点窗口,处理窗口的布局顺序等,以及输入法窗口对新焦点窗口的影响,移除不同uid的toast窗口等
mService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, false /* updateImWindows */);
}
return true;
}
```
这个方法开始是做一些检查,比如是否有权限显示等待,如果检查没通过就返回false,如果一切都正常的话,会看下是否需要窗口动画,同样也会做一些检查,如果不能显示窗口或者没有窗口动画的话,会标记下不能执行窗口动画。否则就会设置窗口的动画,这里有2个变量mPolicyVisibility和mPolicyVisibilityAfterAnim,表示的都是这个窗口是否将要被显示或者隐藏,这里的2个值标记了这个窗口未来的状态,细心的同学可能已经注意到了在这个showLw或者hideLw方法的时候,已经保存了这2个值,这样等这个方法执行完,就可以知道窗口状态有没有变化了,这个下面我们再说。最后如果这个窗口不是不需要和键盘进行交互的,换句话说就是需要和软键盘进行交互的,会调用updateFocusedWindowLocked这个方法来重新测量布局,这个方法大家应该很熟悉,我们现在的流程就是从这个方法引出来对窗口进行测量布局的,但是导航栏不需要和键盘有交互,所以这里不会走这个流程。其实这个方法主要就是设置窗口动画和重新测量窗口,这些都是为后面绘制做准备。我们再看下隐藏的hideLw方法,这个方法基本和showLw差不多,我们看一眼就可以:
```java
boolean hideLw(boolean doAnimation, boolean requestAnim) {
if (doAnimation) {
// 屏幕不可显示
if (!mService.okToDisplay()) {
// 不能播放动画
doAnimation = false;
}
}
// 获取当前的显示状态。
boolean current = doAnimation ? mPolicyVisibilityAfterAnim : mPolicyVisibility;
// 如果当前是不显示状态,return
if (!current) {
// Already hiding.
return false;
}
// 如果要执行动画的话
if (doAnimation) {
// 这个方法是隐藏窗口,所以设置退出动画给窗口
mWinAnimator.applyAnimationLocked(WindowManagerPolicy.TRANSIT_EXIT, false);
// 如果没有可以设置的退出动画,doAnimation置为false,表示没有动画
if (mWinAnimator.mAnimation == null) {
doAnimation = false;
}
}
// 设置动画后不显示窗口
mPolicyVisibilityAfterAnim = false;
if (!doAnimation) { // 没有过渡动画
if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility false: " + this);
mPolicyVisibility = false; // 直接设置不显示
// Window is no longer visible -- make sure if we were waiting
// for it to be displayed before enabling the display, that
// we allow the display to be enabled now.
// 确保开机已经完成了,否则屏幕是不能用的状态
mService.enableScreenIfNeededLocked();
// 由于当前这个焦点窗口要隐藏了,所以焦点窗口会改变
// 设置mFocusMayChange值为true
if (mService.mCurrentFocus == this) {
if (DEBUG_FOCUS_LIGHT) Slog.i(TAG,
"WindowState.hideLw: setting mFocusMayChange true");
mService.mFocusMayChange = true;
}
}
if (requestAnim) { // 如果要求有动画,初始化动画
mService.scheduleAnimationLocked();
}
// 如果是当前焦点窗口
if (mService.mCurrentFocus == this) {
// 更新最新的焦点窗口,处理窗口顺序布局等,处理输入法对新焦点窗口的影响,以及移除不同uid的toast窗口
mService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, false /* updateImWindows */);
}
return true;
}
```
可以看到基本和现实的差不多,主要也是设置退场动画和重新测量布局窗口,这里调用测量布局的方法updateFocusedWindowLocked的条件是如果当前的聚焦窗口是本窗口,这里很容易理解,既然当前聚焦窗口都要退出了,那肯定要调用updateFocusedWindowLocked方法重新计算聚焦窗口啊。好了我们回到前面setBarShowingLw方法。
经过设置了导航栏的显示或者隐藏的方法后,我们知道了当前窗口需要显示或者隐藏,所以接着会调用computeStateLw方法看下是否这个窗口状态有变化,我们看下这个方法:
```java
private int computeStateLw(boolean wasVis, boolean wasAnim, WindowState win, boolean change) {
if (win.isDrawnLw()) {
// 是否窗口显示着
final boolean vis = win.isVisibleLw();
// 是否窗口有动画
final boolean anim = win.isAnimatingLw();
// 如果之前状态是正在隐藏过程中,现在状态没变化,窗口不显示,那么最新状态是WINDOW_STATE_HIDDEN
if (mState == StatusBarManager.WINDOW_STATE_HIDING && !change && !vis) {
return StatusBarManager.WINDOW_STATE_HIDDEN;
} else if (mState == StatusBarManager.WINDOW_STATE_HIDDEN && vis) {
// 如果之前窗口状态是隐藏,现在显示了,那么现在状态是WINDOW_STATE_SHOWING
return StatusBarManager.WINDOW_STATE_SHOWING;
} else if (change) {
// 走到这里,change表示当前请求的显隐性有变化,而这里wasVis和vis都表示当前是显示的
// 所以既然请求有变化,那么就切换为隐藏
if (wasVis && vis && !wasAnim && anim) {
return StatusBarManager.WINDOW_STATE_HIDING;
} else {
return StatusBarManager.WINDOW_STATE_SHOWING;
}
}
}
return mState;
}
```
在这个方法里,第一个参数和第二个参数是在调用前面showLw或者hideLw方法前窗口是否显示和窗口是否有动画,第三个参数就是窗口,第四个参数是是否调用过showLw或者hideLw方法。对于导航栏或者状态栏来说,主要有3种状态,WINDOW_STATE_SHOWING,WINDOW_STATE_HIDING,WINDOW_STATE_HIDDEN,分别是显示,隐藏中和已经隐藏,注意这里没有显示就一种状态,不区分显示中和已经显示。
下面的逻辑也比较容易懂,如果当前导航栏是隐藏中状态,并且没有调用过showLw或者hideLw方法,当前也是隐藏状态,那么就返回已经隐藏状态WINDOW_STATE_HIDDEN。如果当前是已经隐藏状态,并且最新的显示了,说明肯定调用过showLw方法了,那么返回WINDOW_STATE_SHOWING。最后如果有调用过showLw或者hideLw方法,之前和状态和现在的不一样了,那么如果之前是显示状态,现在就是WINDOW_STATE_HIDING状态,反之就是WINDOW_STATE_SHOWING状态。好了,我们再回到setBarShowingLw方法。通过上面返回的最新状态值,需要调用updateStateLw更新下窗口的状态,这个也不用多说,最后返回到前面layoutNavigationBar方法。
经过前面的设置显隐后,layoutNavigationBar方法还需要更新几个区域的值,这里mStableBottom就是导航栏的顶部mTmpNavigationFrame.top,输入法底部mDockBottom也是导航栏顶部,系统UI底部mSystemBottom也是导航栏顶部,mRestrictedScreenHeight和mRestrictedOverscanScreenHeight这两个底部也都是从导航栏顶部算起的,他们的高度一个是包括过扫描区域,一个是不包括过扫描区,这个可以结合上面给出的区域的图再看下,这里就是当导航栏在底部的时候计算的计算过程,他的主要过程就是会更新下前面介绍的那些和导航栏有关的区域的边界,然后还会设置窗口动画等准备启动的相关操作。之后如果导航栏在屏幕左边或者右边的时候,也就是横屏的时候,同样也会对导航栏做类似的计算操作,整个过程和在底部的逻辑是一样的,这里也就不多介绍了,有兴趣的同学可以自己对照着去看下。我们再来看下布局导航栏layoutNavigationBar方法的后半部分:
```java
..........
// Make sure the content and current rectangles are updated to
// account for the restrictions from the navigation bar.
mContentTop = mVoiceContentTop = mCurTop = mDockTop;
mContentBottom = mVoiceContentBottom = mCurBottom = mDockBottom;
mContentLeft = mVoiceContentLeft = mCurLeft = mDockLeft;
mContentRight = mVoiceContentRight = mCurRight = mDockRight;
// z轴层序
mStatusBarLayer = mNavigationBar.getSurfaceLayer();
// And compute the final frame.
// dcf是输入法窗口rect
mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
mTmpNavigationFrame, mTmpNavigationFrame, mTmpNavigationFrame, dcf,
mTmpNavigationFrame, mTmpNavigationFrame);
if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + mTmpNavigationFrame);
if (mNavigationBarController.checkHiddenLw()) {
return true;
}
........
```
这部分方法开始还是继续更新几个区域,这里我们看到会更新内容区域和可见区域,如果不考虑输入法弹窗覆盖的情况,内容区域和可见区域是一样的,和输入法窗口的区域也是一样的,前面输入法区域Dock区域已经被安排在了导航栏顶部了,所以这里会使用Dock区域给内容区域和可见区域赋值。之后会获取导航栏的Z轴层序,这个层序是导航栏WindowState的mLayer值,这个值我们前面在介绍Z轴层序的时候有说过,每一个窗口都会根据他的类型等情况赋给一个值,这里会获取导航栏的Z轴层序值。
经过上面的计算,我们前面已经把导航栏的大小保存在mTmpNavigationFrame中了,前面我们说过这个只是临时保存一下,之后会调用WindowState的computeFrameLw方法,经过最终的计算,会把导航栏的大小保存在WindowState中,computeFrameLw这个方法比较长,我们稍后等分析完状态栏后回头来看,状态栏中也会用到这个方法来最终计算状态栏的值,最后返回导航栏状态有没有更新,这样导航栏的布局就算完成了。我们再去看下状态栏的布局过程:
# 对状态栏进行布局
```java
private boolean layoutStatusBar(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) {
// apply any navigation bar insets
/// 父窗口区域left和top就是不包括过扫描区域的开始
pf.left = df.left = of.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
// right和bottom就是屏幕宽度或者高度+过扫描的区域
pf.right = df.right = of.right = mUnrestrictedScreenWidth + mUnrestrictedScreenLeft;
pf.bottom = df.bottom = of.bottom = mUnrestrictedScreenHeight
+ mUnrestrictedScreenTop;
// 可见区域,简单理解就是去掉系统bar的区域
vf.left = mStableLeft;
vf.top = mStableTop;
vf.right = mStableRight;
vf.bottom = mStableBottom;
// z轴层序
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 */);
// For layout, the status bar is always at the top with our fixed height.
// 经过状态栏的测量后,稳定top重新计算,过扫描+状态栏高度
mStableTop = mUnrestrictedScreenTop + mStatusBarHeight;
// 状态栏是否透明
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.
// 状态栏正常显示,所以输入法顶部安排在状态栏的下面,即过扫描+状态栏高度
mDockTop = mUnrestrictedScreenTop + mStatusBarHeight;
// 同导航栏一样,用户区安排在除去过扫描和系统bar之外的区域
mContentTop = mVoiceContentTop = mCurTop = mDockTop;
mContentBottom = mVoiceContentBottom = mCurBottom = mDockBottom;
mContentLeft = mVoiceContentLeft = mCurLeft = mDockLeft;
mContentRight = mVoiceContentRight = mCurRight = mDockRight;
if (DEBUG_LAYOUT) Slog.v(TAG, "Status bar: " +
String.format(
"dock=[%d,%d][%d,%d] content=[%d,%d][%d,%d] cur=[%d,%d][%d,%d]",
mDockLeft, mDockTop, mDockRight, mDockBottom,
mContentLeft, mContentTop, mContentRight, mContentBottom,
mCurLeft, mCurTop, mCurRight, mCurBottom));
}
// 状态栏是显示的,非动画状态,非透明和半透明
if (mStatusBar.isVisibleLw() && !mStatusBar.isAnimatingLw()
&& !statusBarTransient && !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.
// 系统区域的top安排在状态栏下面
mSystemTop = mUnrestrictedScreenTop + mStatusBarHeight;
}
// 检查bar是否隐藏了,隐藏的话更新下隐藏的状态值
if (mStatusBarController.checkHiddenLw()) {
return true;
}
}
return false;
}
```
这个方法总体上和前面导航栏的布局流程是差不多的,我们过一下。开始可以看到还是设置几个区域,比如父窗口区域,过扫描区域,内容区域等等,这些还是根据前面介绍的区域概念来设置的,对照前面的图看一下就可以。之后同样获得状态栏的Z轴层序,后面会调用computeFrameLw方法也是最终把状态栏的大小会保存到他的WindowStatte中,computeFrameLw这个方法我们下面马上会分析,这里先把状态栏看完。
由于状态栏的大小计算出来了,所以又会影响到一些区域的位置,比如Stable区域是介于状态栏和导航栏之间的,之前在看导航栏布局的时候,我们已经看到把一些区域的bottom修改了,这里状态栏主要会修改这些区域的top,上面代码中可以看到会修改Dock和Content区域,这两个区域的top被设置在状态栏的下面,这样就可以保证具体加载的各个应用的界面显示在状态栏下面了,上面代码相信大家也都可以理解,这里不多说了。接着我们就来看下前面遇到了computeFrameLw方法,他会根据给出的这些区域值,最终计算一个窗口的大小保存在WindowState中,我们来看下这个方法。
# 计算窗口大小的computeFrameLw方法
```java
public void computeFrameLw(Rect parentFrame, Rect displayFrame, Rect overscanFrame,
Rect contentFrame, Rect visibleFrame, Rect decorFrame, Rect stableFrame,
Rect outsetFrame) {
// 当前窗口要被替换同时正在退出或者等待着替换(即没有要被移除的要求)
if (mWillReplaceWindow && (mAnimatingExit || !mReplacingRemoveRequested)) {
return;
}
// 设置mHaveFrame为true,表示这个窗口已经有过测量了
mHaveFrame = true;
final Task task = getTask();
// 是否全屏窗口,非Activity是全屏窗口,Activity如果有mBounds就是非全屏,否则看是否是多窗口
// 可以看出这里的全屏并非指窗口和屏幕一样大
final boolean inFullscreenContainer = inFullscreenContainer();
// 是自由模式或者画中画
final boolean windowsAreFloating = task != null && task.isFloating();
final DisplayContent dc = getDisplayContent();
// If the task has temp inset bounds set, we have to make sure all its windows uses
// the temp inset frame. Otherwise different display frames get applied to the main
// window and the child window, making them misaligned.
// 暂时把窗口区域保存在mInsetFrame中
if (inFullscreenContainer) {
mInsetFrame.setEmpty();
} else if (task != null && isInMultiWindowMode()) {
// 保存下task的裁剪区域,即去掉状态栏,导航栏等的区域
task.getTempInsetBounds(mInsetFrame);
}
.............
}
```
这个方法是比较长的,我们分几段来看,这里是第一段。方法开始会先做一些判断,如果当前这个窗口是一个要被替换的窗口,这里被替换窗口的场景一般是这个窗口可能需要重新启动,并且重启后窗口会改变,比如从一个正常的Activity切换到一个分屏中的Activity,窗口就不是原来的那个了,所以这种情况下这里直接return了,不计算这个窗口了。
之后这里会获取下这个窗口的一些状态,比如是否是一个全屏容器类型的,或者是否是一个浮窗类型的。这里全屏容器类的是调用inFullscreenContainer方法来获取,我们看下这个全屏容器类型的窗口是指什么:
```java
private boolean inFullscreenContainer() {
// 非Activity表示全屏窗口
if (mAppToken == null) {
return true;
}
// Activity的mBounds非空
if (mAppToken.hasBounds()) {
return false;
}
// 非多窗口模式
return !isInMultiWindowMode();
}
```
可以看到这里只要是非Activty的都是全屏容器类型的窗口,一般非Activity的就是系统窗口,系统窗口一般会安排在全屏里面进行布局。而对于Activity来说,默认他的大小也是可以作为全屏模式来显示的,除非有设置给他固定的大小,如果有设置固定大小的话调用他的hasBounds方法返回true就表示有设置大小,他的大小其实也就是一个Rect,如果有值的话就是表示有设置固定大小。最后如果在多窗口模式下,也就是分屏情况下,也不是全屏的了,这个直观上也很好理解。
好的,我们再来看下什么是浮窗类型:
```java
boolean isFloating() {
return StackId.tasksAreFloating(mStack.mStackId)
&& !mStack.isAnimatingBoundsToFullscreen() && !mPreserveNonFloatingState;
}
public static boolean tasksAreFloating(int stackId) {
return stackId == FREEFORM_WORKSPACE_STACK_ID
|| stackId == PINNED_STACK_ID;
}
```
这里isFloating方法中还会调用tasksAreFloating来判断ActivityStack的类型,我们主要看下哪些ActivityStack是浮窗模式。这里可以看到自由模式和画中画类型的窗口都是属于浮窗模式,我们这个了解一下就好,我们回到前面computeFrameLw方法。
最后这段代码会获取这个窗口的父容器大小,上面的方法已经可以判断这个窗口是否是一个全屏容器,如果是全屏容器的话,其实也不需要父容器给它限制大小,所以这里描述父容器的mInsetFrame变量会是空,否则如果不是全屏容器的话,同时是一个Activity,那么会把这个Activity的Task大小赋给mInsetFrame,代表他的父容器。第一段代码看完了,主要就是知道这个窗口是不是一个全屏容器类型的,然后获取他父容器的大小,我们看第二段代码:
```java
..........
// 布局用的父窗口大小
final Rect layoutContainingFrame;
// 布局用的窗口大小
final Rect layoutDisplayFrame;
// The offset from the layout containing frame to the actual containing frame.
final int layoutXDiff;
final int layoutYDiff;
// 如果是全屏类型大小或者和父窗口一样大小的子窗口
if (inFullscreenContainer || layoutInParentFrame()) {
// We use the parent frame as the containing frame for fullscreen and child windows
// 当前父窗口大小
mContainingFrame.set(parentFrame);
// 赋值窗口大小给mDisplayFrame
mDisplayFrame.set(displayFrame);
layoutDisplayFrame = displayFrame;
layoutContainingFrame = parentFrame;
layoutXDiff = 0;
layoutYDiff = 0;
} else {
// 到这里说明可能mbounds有大小,从task或者Activity中获取大小
getContainerBounds(mContainingFrame);
// 如果是Activity并且有冻结的区域
if (mAppToken != null && !mAppToken.mFrozenBounds.isEmpty()) {
// If the bounds are frozen, we still want to translate the window freely and only
// freeze the size.
Rect frozen = mAppToken.mFrozenBounds.peek();
mContainingFrame.right = mContainingFrame.left + frozen.width();
mContainingFrame.bottom = mContainingFrame.top + frozen.height();
}
// 输入法窗口
final WindowState imeWin = mService.mInputMethodWindow;
// IME is up and obscuring this window. Adjust the window position so it is visible.
// 如果输入法窗口有显示,并且当前这个窗口是输入法目标窗口
if (imeWin != null && imeWin.isVisibleNow() && mService.mInputMethodTarget == this) {
// 取出当前的taskId
final int stackId = getStackId();
// 如果是自由模式并且父窗口的底部内容区域的底部,这里和内容区域比较
// 不和父窗口比较,把top往上调一下就可以,底部不管,允许显示在外面
if (stackId == FREEFORM_WORKSPACE_STACK_ID
&& mContainingFrame.bottom > contentFrame.bottom) {
// In freeform we want to move the top up directly.
// TODO: Investigate why this is contentFrame not parentFrame.
// 把mContainingFrame往上调一下,相当于把显示内容往上挪动一点,底部就不管了
mContainingFrame.top -= mContainingFrame.bottom - contentFrame.bottom;
// 如果不是画中画,如果stack底部大于父窗口了,那么将底部和父窗口对齐
} else if (stackId != PINNED_STACK_ID
&& mContainingFrame.bottom > parentFrame.bottom) {
// But in docked we want to behave like fullscreen and behave as if the task
// were given smaller bounds for the purposes of layout. Skip adjustments for
// the pinned stack, they are handled separately in the PinnedStackController.
// 底部对齐
mContainingFrame.bottom = parentFrame.bottom;
}
}
// 浮动模式窗口,即自由模式或画中画。如果没设置父窗,设置为内容区域即可
if (windowsAreFloating) {
// In floating modes (e.g. freeform, pinned) we have only to set the rectangle
// if it wasn't set already. No need to intersect it with the (visible)
// "content frame" since it is allowed to be outside the visible desktop.
if (mContainingFrame.isEmpty()) {
mContainingFrame.set(contentFrame);
}
}
................
}
...................
```
经过第一段代码的处理,我们知道了当前这个窗口的父容器大小,在全屏容器下是没有父容器大小的,而在Activity给的大小情况下,可以从他的Task中获取父容器大小。所以这里第二段代码会继续计算父容器大小,为后面的窗口大小计算做准备。
首先可以看到,如果是在全屏容器类型的情况下,这里父容器大小就是该方法参数传入的父容器大小,这里会保存在mContainingFrame这个变量中,而这里我们看到有layoutContainingFrame这个变量,这个是实际参数当前窗口时候的布局大小,因为后面会看到父窗口不一定能全部使用,可能还需要做一些限制,最终可以使用的父窗口大小会保存在layoutContainingFrame这个变量中。这里如果是全屏容器类型的话,分别会用参数传入的父窗口和屏幕大小给这里的变量赋值。
如果非全屏类型容器的话,首先会调用getContainerBounds方法来获取父容器大小,我们看一眼这个方法:
```java
private void getContainerBounds(Rect outBounds) {
// 多窗口从task中获取bound
if (isInMultiWindowMode()) {
getTask().getBounds(outBounds);
} else if (mAppToken != null){
// Activity获取bound
mAppToken.getBounds(outBounds);
} else {
// 其余的是空
outBounds.setEmpty();
}
}
```
这个方法的逻辑其实和我们前面判断是否是全屏类型的逻辑差不多的,如果是多窗口的话,会从Task中获取他的大小,否则如果是Activity的话从这个Activity中获取大小,其余情况肯定就没有给的的大小了,这里会赋值空。我们大概看一眼知道这里的逻辑就行,回到前面computeFrameLw方法。
接下去就会根据几个不同的窗口场景来调整父窗口,我们稍稍来理解一下其中的意思,有些场景可能我们实际中也没遇到过,所以也不知道是什么含义,这里通过代码来理解一下。首先如果是冻屏的状态,这里从代码中我们可以看到首先会获取冻屏的区域大小,然后把内容区域设置为冻屏大小,这样显示的区域就和冻屏的区域一样大了,这里我们从代码中的理解,实际场景在实际开发中也没遇到过,所以就仅仅做为个人理解,有对这块比较了解的小伙伴欢迎指教。
之后是对输入法目标窗口的调整。这里如果是一个输入法目标窗口,会有两种场景,第一种如果是一个自由模式窗口并且父窗口的底部比当前的内容区域还有低,那么说明底部可能会显示不出,所以这里仅仅把父窗口的顶部往上移动一些就可以,自由模式任意移动,而且注意到这里父
第二种情况,如果当前从Task这里获取的父窗口区域底部低于参数传入的父窗口底部,并且非画中画模式的话,那么把Task获取的父窗口底部和传入的父窗口底部对齐就可以,由于画中画模式是固定的,所以这里不对画中画大小调整。
接着如果是浮窗类型窗口的话,也就是画中画和自由模式,如果他们没有父窗口的大小,那么就把参数传入的内容区域大小设置给他们的父窗口。好了,这一段就是对一些类型窗口的父容器进行了一下调整。我们接着看下一段代码:
```java
....................
// 整个窗口的显示区域默认值是父窗口
mDisplayFrame.set(mContainingFrame);
// 下面2个就是拿Task中获取的区域和前面调整的父窗口区域比较,left和top不能超过Task区域
layoutXDiff = !mInsetFrame.isEmpty() ? mInsetFrame.left - mContainingFrame.left : 0;
layoutYDiff = !mInsetFrame.isEmpty() ? mInsetFrame.top - mContainingFrame.top : 0;
// 默认父窗口设置为task区域或者前面算出的父窗口
layoutContainingFrame = !mInsetFrame.isEmpty() ? mInsetFrame : mContainingFrame;
// 当前屏幕的大小
mTmpRect.set(0, 0, dc.getDisplayInfo().logicalWidth, dc.getDisplayInfo().logicalHeight);
// 参数1,整个窗口的显示区域,并且整个屏幕的区域
// 参数2 父窗口的大小
// 参数3 状态栏displayFrame默认是Dock区域,导航栏是自己计算出的区域
// 参数4 逻辑屏幕区域
subtractInsets(mDisplayFrame, layoutContainingFrame, displayFrame, mTmpRect);
// 如果不是子窗口类,也不依附父窗口大小,计算出mContainingFrame和mInsetFrame大小
if (!layoutInParentFrame()) {
// 计算父窗口的大小
subtractInsets(mContainingFrame, layoutContainingFrame, parentFrame, mTmpRect);
subtractInsets(mInsetFrame, layoutContainingFrame, parentFrame, mTmpRect);
}
// layoutDisplayFrame和上面mDisplayFrame类似,表示窗口可以布局的大小
// mDisplayFrame的值代表距离四边的距离,layoutDisplayFrame表示rect的正常含义,即距离左边和上面的距离
layoutDisplayFrame = displayFrame;
// 取layoutDisplayFrame和layoutContainingFrame的交集
layoutDisplayFrame.intersect(layoutContainingFrame);
........................
```
这段代码会计算出显示区域,这里的显示区域是指可以提供给用于窗口显示的最大区域,不是指该窗口最终的大小,该窗口最终大小会根据里面View测量的大小来最终决定,我们继续看下去就能看到最终大小是怎么计算的了。
这里mDisplayFrame和layoutDisplayFrame都可以表示显示区域。区别是mDisplayFrame的值表示距离四边的距离,layoutDisplayFrame就是正常我们理解的Rect的含义,表示距离左边和上边的距离。当然如果这里是子窗口的话,还会计算父窗口mContainingFrame。这里计算显示区域距离屏幕四边距离mDisplayFrame的方法是subtractInsets,我们稍后跟进这个方法。而layoutDisplayFrame的计算则是使用参数传入的这个窗口显示区域和从Task中获取的大小或者计算出的父窗口大小中取交集。我们现在看下subtractInsets这个方法:
```java
// 参数1,待计算的整个窗口的显示区域,非整个屏幕的区域
// 参数2 父窗口的大小
// 参数3 状态栏Unrestricted区域,导航栏是自己计算出的区域
// 参数4 逻辑屏幕区域
private void subtractInsets(Rect frame, Rect layoutFrame, Rect insetFrame, Rect displayFrame) {
final int left = Math.max(0, insetFrame.left - Math.max(layoutFrame.left, displayFrame.left));
final int top = Math.max(0, insetFrame.top - Math.max(layoutFrame.top, displayFrame.top));
final int right = Math.max(0, Math.min(layoutFrame.right, displayFrame.right) - insetFrame.right);
final int bottom = Math.max(0, Math.min(layoutFrame.bottom, displayFrame.bottom) - insetFrame.bottom);
frame.inset(left, top, right, bottom);
}
```
这个方法主要使用后面的3个参数,经过计算后赋值给第一个参数。这里第二个参数代表了一个父窗口的大小,比如前面说的Task的大小或者根据前面方法传入的父窗口的大小。第三个参数是前面方法传入的一个显示区域,比如导航栏就是屏幕下面导航栏的区域,状态栏是默认传入的是包含状态栏和导航栏的区域。第四个参数是逻辑屏幕的大小。
这里的计算结果是显示区域四周的边距。比如我们拿导航栏来举例,这里第二个和第三个参数对于导航栏来说都是一样的,就是底部导航栏的范围。这里我们用top来举例,首先会计算第二个参数也就是父窗口的的top和屏幕的top取较大值,这里肯定是导航的区域的top比较大,之后用第三个参数也就是导航栏传入的显示区域减去前面top计算的较大值,这里两者是相同的,所以为0,最终和0取最大值所以就是0。这个值代表的也就是这个窗口参数给定的显示区域和这里计算出的显示区域边距是多少。而回到前面方法中另一个参数layoutDisplayFrame就是代表了这个显示区域用Rect对象表示的结果。这里方法描述起来还是比较麻烦的,几个参数的值对于不同窗口是不一样的,所以如果有兴趣的话,可以仔细去琢磨琢磨。我们回到前面computeFrameLw方法,继续看后面的代码:
```java
............
final int pw = mContainingFrame.width();
final int ph = mContainingFrame.height();
// 当前WindowState中保存的父窗口大小和传入的父窗口大小不一样,更新
if (!mParentFrame.equals(parentFrame)) {
mParentFrame.set(parentFrame);
mContentChanged = true;
}
// 如果和客户端view测量的宽高不一样,更新。默认是0
if (mRequestedWidth != mLastRequestedWidth || mRequestedHeight != mLastRequestedHeight) {
mLastRequestedWidth = mRequestedWidth;
mLastRequestedHeight = mRequestedHeight;
mContentChanged = true;
}
// 更新过扫描区域
mOverscanFrame.set(overscanFrame);
// 更新内容区域,包含输入法
mContentFrame.set(contentFrame);
// 更新可见区域。不包含输入法
mVisibleFrame.set(visibleFrame);
// 更新装饰区域
mDecorFrame.set(decorFrame);
// stable区域
mStableFrame.set(stableFrame);
// 这个一般给刘海屏用的,没有刘海的话outsetFrame和mContentFrame一样
final boolean hasOutsets = outsetFrame != null;
// 把外部填充保存在mOutsetFrame,注意和后面mOutsets区别
if (hasOutsets) {
mOutsetFrame.set(outsetFrame);
}
// 只有自由模式和画中画mFrame才有值,默认是displayInfo大小
final int fw = mFrame.width();
final int fh = mFrame.height();
// 根据gravity,长宽等计算最终的窗口大小,赋值给mFrame
applyGravityAndUpdateFrame(layoutContainingFrame, layoutDisplayFrame);
............
```
这段代码就会涉及到计算最终的窗口大小的地方了,我们看一下。首先会获取下前面计算出来的父窗口的宽和高,之后会用传入的参数来更新下窗口的几个区域。这里我们看到有mRequestedWidth和mRequestedHeight这2个变量,这个2个变量是窗口中View请求的大小,也就是真正绘制时候需要的大小,这个会通过view的测量的传过来,下面窗口的布局也会利用这个值来确定窗口的大小,下面我们就会看到。这里会把这个值保存在窗口的WindowState中,每次调用这个方法的时候会判断如果和上一次不一样就会更新,这里我们记住这个值,后面计算窗口最终大小的时候我们会看到。
这里还有一个mOutsetFrame对象,这个是刘海区域,我们现在经常会看到刘海屏,这个对象就是对应刘海的区域,我们这就提一下。接着会从mFrame对象中获取窗口宽和高,这个对象就是真正的窗口的大小了,一般类型的窗口这个对象的默认值是0,只有自由模式和画中画的时候,这个对象是有默认值的,原因是一般其他窗口都是会有大小的,而自由模式和画中画大小是不定的,所以为了不让窗口大小变成0,所以会有个默认的大小值,初始化是在WindowManagerService的applyAnimationLocked方法中设置的,这里提一下就不看了。
最后这段方法我们看到会调用applyGravityAndUpdateFrame方法,这个方法就是最终计算窗口大小的方法了,最终的窗口大小会保存在mFrame中,这里传入的参数是父窗口的大小和显示区域的大小,我们稍后回头来看这个方法,我们先往下继续看完这个方法的代码。
```java
............
if (hasOutsets) {
// 如果有外部填充区域的话,算一下外部填充区域和内容区域的差值,保存在mOutsets中
// 保存处理刘海的区域
mOutsets.set(Math.max(mContentFrame.left - mOutsetFrame.left, 0),
Math.max(mContentFrame.top - mOutsetFrame.top, 0),
Math.max(mOutsetFrame.right - mContentFrame.right, 0),
Math.max(mOutsetFrame.bottom - mContentFrame.bottom, 0));
} else {
mOutsets.set(0, 0, 0, 0);
}
// Make sure the content and visible frames are inside of the
// final window frame.
// 如果是自由模式或者画中画
if (windowsAreFloating && !mFrame.isEmpty()) {
// For pinned workspace the frame isn't limited in any particular
// way since SystemUI controls the bounds. For freeform however
// we want to keep things inside the content frame.
// 如果是画中画,获取mFrame。自由模式获取mContentFrame
final Rect limitFrame = task.inPinnedWorkspace() ? mFrame : mContentFrame;
// Keep the frame out of the blocked system area, limit it in size to the content area
// and make sure that there is always a minimum visible so that the user can drag it
// into a usable area..
// 对于下面的宽高。如果是画中画,应该就是mFrame的值。自由模式的话去mFrame和mContentFrame中小值
// 因为自由模式被限制在内容区域
final int height = Math.min(mFrame.height(), limitFrame.height());
final int width = Math.min(limitFrame.width(), mFrame.width());
final DisplayMetrics displayMetrics = getDisplayContent().getDisplayMetrics();
// 最小可视区域高度
final int minVisibleHeight = Math.min(height, WindowManagerService.dipToPixel(
MINIMUM_VISIBLE_HEIGHT_IN_DP, displayMetrics));
// 最小可视区域宽度
final int minVisibleWidth = Math.min(width, WindowManagerService.dipToPixel(
MINIMUM_VISIBLE_WIDTH_IN_DP, displayMetrics));
// 计算最终窗口top。top先计算mFrame和limitFrame底部到顶部最小要求高度所在top中较上面的,这样算出
// 来可能是mFrame,但是自由模式用的是limitFrame,所以再比较2者较大的
final int top = Math.max(limitFrame.top,
Math.min(mFrame.top, limitFrame.bottom - minVisibleHeight));
// 计算最终窗口left
final int left = Math.max(limitFrame.left + minVisibleWidth - width,
Math.min(mFrame.left, limitFrame.right - minVisibleWidth));
// 设置mFrame的大小,即设置自由模式或者画中画窗口的大小
mFrame.set(left, top, left + width, top + height);
// 内容区域和mFrame一样
mContentFrame.set(mFrame);
// 可视区域一样
mVisibleFrame.set(mContentFrame);
// stable区域一样
mStableFrame.set(mContentFrame);
} else if (mAttrs.type == TYPE_DOCK_DIVIDER) { // 分屏分界线
// 获取分屏分界线窗口的大小
dc.getDockedDividerController().positionDockedStackedDivider(mFrame);
// 设置给mContentFrame
mContentFrame.set(mFrame);
// 如果分屏窗口大小和最后一次不一样,说明窗口大小有改变
if (!mFrame.equals(mLastFrame)) {
mMovedByResize = true;
}
} else { // 根据mFrame,计算几个窗口的大小
mContentFrame.set(Math.max(mContentFrame.left, mFrame.left),
Math.max(mContentFrame.top, mFrame.top),
Math.min(mContentFrame.right, mFrame.right),
Math.min(mContentFrame.bottom, mFrame.bottom));
mVisibleFrame.set(Math.max(mVisibleFrame.left, mFrame.left),
Math.max(mVisibleFrame.top, mFrame.top),
Math.min(mVisibleFrame.right, mFrame.right),
Math.min(mVisibleFrame.bottom, mFrame.bottom));
mStableFrame.set(Math.max(mStableFrame.left, mFrame.left),
Math.max(mStableFrame.top, mFrame.top),
Math.min(mStableFrame.right, mFrame.right),
Math.min(mStableFrame.bottom, mFrame.bottom));
}
..................
```
这里代码开始会设置刘海区域四周和内容区域的大小,一般来说内容区域需要避开刘海防止内容区域被刘海覆盖,所以这里分别计算出内容区域的四边和刘海的四边相差的距离,为后面需要用到时候做准备,我们这里理解一下刘海就可以。
由于前面applyGravityAndUpdateFrame方法已经计算出了窗口的大小,所以代码执行到这里已经知道窗口的大小了,但是可能还需要调整下。比如我们前面说自由模式下会有一个窗口的最小值,所以会看看是否窗口符合最小的尺寸,如果不符合就会调整长宽和窗口位置,然后更新mFrame的值以及其余几个区域,比如内容区域,可视区域,Stable区域的值等。
同样上面代码对于分屏分界线窗口也会有处理,我们知道下分屏分界线就行,这里不多细看。最后对应其他大部分窗口类型来说,会更新内容区域,可视区域和stable区域的值,这个3个区域在方法的参数中都有传入,这里会取他们的交集来更新。
好了,上面这个一段代码处理对几个特殊窗口类型的处理外,主要就是用计算出的窗口大小来更新几个其他的区域,我们看下一段代码:
```java
..............
// 全屏窗口且非浮动窗口(即非自由模式或者画中画)
if (inFullscreenContainer && !windowsAreFloating) {
// 设置过扫描区域
mOverscanInsets.set(Math.max(mOverscanFrame.left - layoutContainingFrame.left, 0),
Math.max(mOverscanFrame.top - layoutContainingFrame.top, 0),
Math.max(layoutContainingFrame.right - mOverscanFrame.right, 0),
Math.max(layoutContainingFrame.bottom - mOverscanFrame.bottom, 0));
}
if (mAttrs.type == TYPE_DOCK_DIVIDER) { // 分屏分界线
mStableInsets.set(Math.max(mStableFrame.left - mDisplayFrame.left, 0),
Math.max(mStableFrame.top - mDisplayFrame.top, 0),
Math.max(mDisplayFrame.right - mStableFrame.right, 0),
Math.max(mDisplayFrame.bottom - mStableFrame.bottom, 0));
mContentInsets.setEmpty();
mVisibleInsets.setEmpty();
} else {
// 获取逻辑屏幕的大小保存到mTmpRect
getDisplayContent().getLogicalDisplayRect(mTmpRect);
// 非浮动窗口(非自由模式和画中画),非全屏(有mbounds),测量出的窗口代表右边比屏幕右边大
boolean overrideRightInset = !windowsAreFloating && !inFullscreenContainer
&& mFrame.right > mTmpRect.right;
// 其余同上,窗口下边比屏幕下边大
boolean overrideBottomInset = !windowsAreFloating && !inFullscreenContainer
&& mFrame.bottom > mTmpRect.bottom;
// 下面是设置3个区域的四边宽度
// 内容区域的四周宽度
mContentInsets.set(mContentFrame.left - mFrame.left,
mContentFrame.top - mFrame.top,
overrideRightInset ? mTmpRect.right - mContentFrame.right
: mFrame.right - mContentFrame.right,
overrideBottomInset ? mTmpRect.bottom - mContentFrame.bottom
: mFrame.bottom - mContentFrame.bottom);
// 可见区域的四周宽度
mVisibleInsets.set(mVisibleFrame.left - mFrame.left,
mVisibleFrame.top - mFrame.top,
overrideRightInset ? mTmpRect.right - mVisibleFrame.right
: mFrame.right - mVisibleFrame.right,
overrideBottomInset ? mTmpRect.bottom - mVisibleFrame.bottom
: mFrame.bottom - mVisibleFrame.bottom);
// stable区域的四周宽度
mStableInsets.set(Math.max(mStableFrame.left - mFrame.left, 0),
Math.max(mStableFrame.top - mFrame.top, 0),
overrideRightInset ? Math.max(mTmpRect.right - mStableFrame.right, 0)
: Math.max(mFrame.right - mStableFrame.right, 0),
overrideBottomInset ? Math.max(mTmpRect.bottom - mStableFrame.bottom, 0)
: Math.max(mFrame.bottom - mStableFrame.bottom, 0));
}
..................
```
这段代码主要是计算几个区域的衬边。前面我们计算区域的时候都是用xxxFrame这样的变量,这些变量是代表这些对象的Rect包围的就是需要绘制的区域,而这里xxxInsets表示的是衬边,也就是前面xxxFrame的区域外围需要空出多少边距。上面就对这些区域的衬边进行计算。
首先如果是全屏容器且不是浮动窗口(即非自由窗口和画中画)的话,会计算过扫描区域的衬边。这里也很好理解只有全屏的容器类才会需要考虑过扫描,非全屏的话一般都已经缩小了区域范围,所以这里只考虑全屏才设置过扫描区域。
然后如果是分屏分界线窗口的话,就只设置了stable衬边,其他衬边不设置。最后如果是正常的一个窗口的话,就会设置内容,可视,stable三个衬边。这里我们注意一下,上面计算出来的窗口大小mFrame是有可能会超过屏幕的,所以这里会有做判断如果窗口的right和bottom超过了屏幕,那么这里衬边的计算会使用窗口的right和bottom,这里我们要注意一下。最后一段代码我们在看一下:
```java
.........
mFrame.offset(-layoutXDiff, -layoutYDiff);
mCompatFrame.offset(-layoutXDiff, -layoutYDiff);
// 内容区域
mContentFrame.offset(-layoutXDiff, -layoutYDiff);
// 可见区域
mVisibleFrame.offset(-layoutXDiff, -layoutYDiff);
// stable区域
mStableFrame.offset(-layoutXDiff, -layoutYDiff);
// 兼容模式下窗口大小默认等于mFrame
mCompatFrame.set(mFrame);
// 适配模式的话,会缩放这几个区域宽度大小
if (mEnforceSizeCompat) {
mOverscanInsets.scale(mInvGlobalScale);
mContentInsets.scale(mInvGlobalScale);
mVisibleInsets.scale(mInvGlobalScale);
mStableInsets.scale(mInvGlobalScale);
mOutsets.scale(mInvGlobalScale);
mCompatFrame.scale(mInvGlobalScale);
}
// 壁纸窗口
if (mIsWallpaper && (fw != mFrame.width() || fh != mFrame.height())) {
final DisplayContent displayContent = getDisplayContent();
if (displayContent != null) {
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
getDisplayContent().mWallpaperController.updateWallpaperOffset(
this, displayInfo.logicalWidth, displayInfo.logicalHeight, false);
}
}
..................
```
以上这段代码是这个方法的最后一部分了,这里会最终会前面计算出来的窗口大小和各个区域进行一些适配。比如这里我们看到的layoutXDiff和layoutYDiff在前面计算时候表示他的父窗口可能超出了这个方法传给他的父窗口,是所有会做一些调整。这里就是对各个区域做一些异常情况引起的偏移,然后如果是在兼容模式下,会对窗口以及各个区域进行缩放,这样最终这个窗口显示的大小就确定了。
最后如果这里mFrame有更新的话,说明大小或者位置有变化,这对壁纸来说会更新下壁纸的位置,具体调用WallpaperController的updateWallpaperOffset方法来更新壁纸窗口的位置,壁纸模块这里就不深入跟进了,有兴趣的同学可以跟进去再研究一下。
# applyGravityAndUpdateFrame方法计算窗口大小
到这里整个这个方法就算完成了,这个方法虽然很长,也计算了很多东西,但是说到底就是计算这个窗口的大小以及几个区域,这里的核心计算窗口的方法是applyGravityAndUpdateFrame,这个前面我们没跟进去分析,下面就跟进这个方法看看计算窗口大小的过程:
```java
void applyGravityAndUpdateFrame(Rect containingFrame, Rect displayFrame) {
// 获取父窗口的宽和高
final int pw = containingFrame.width();
final int ph = containingFrame.height();
// 获取Task
final Task task = getTask();
// 是否是全屏
final boolean inNonFullscreenContainer = !inFullscreenContainer();
// 是否允许窗口在屏幕外面
final boolean noLimits = (mAttrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
// 是否需要对窗口进程适配
// 如果没有task或者是全屏的,或者不是第一个Activity同时有限制窗口大小
final boolean fitToDisplay = (task == null || !inNonFullscreenContainer)
|| ((mAttrs.type != TYPE_BASE_APPLICATION) && !noLimits);
float x, y;
int w,h;
// 如果有窗口缩放系数
if ((mAttrs.flags & FLAG_SCALED) != 0) {
// 窗口属性宽度小于0的,用父窗口宽度
if (mAttrs.width < 0) {
w = pw;
} else if (mEnforceSizeCompat) {// 适配模式
w = (int)(mAttrs.width * mGlobalScale + .5f);
} else {
w = mAttrs.width;
}
if (mAttrs.height < 0) { // 同理高度
h = ph;
} else if (mEnforceSizeCompat) {
h = (int)(mAttrs.height * mGlobalScale + .5f);
} else {
h = mAttrs.height;
}
} else {
// 没有缩放系数,宽度是MATCH_PARENT等于父窗口
if (mAttrs.width == MATCH_PARENT) {
w = pw;
} else if (mEnforceSizeCompat) { // 适配模式
w = (int)(mRequestedWidth * mGlobalScale + .5f);
} else { // 窗口请求的宽度
w = mRequestedWidth;
}
// 高度同理
if (mAttrs.height == MATCH_PARENT) {
h = ph;
} else if (mEnforceSizeCompat) {
h = (int)(mRequestedHeight * mGlobalScale + .5f);
} else {
h = mRequestedHeight;
}
}
// 会获取x,y坐标轴(区分是否适配模式)
if (mEnforceSizeCompat) {
x = mAttrs.x * mGlobalScale;
y = mAttrs.y * mGlobalScale;
} else {
x = mAttrs.x;
y = mAttrs.y;
}
// 非全屏并且非子窗口
if (inNonFullscreenContainer && !layoutInParentFrame()) {
// Make sure window fits in containing frame since it is in a non-fullscreen task as
// required by {@link Gravity#apply} call.
// 父窗口,和这个窗口(也即DecorView)宽高中取小者
w = Math.min(w, pw);
h = Math.min(h, ph);
}
...........
}
```
这个方法的2个参数先介绍一下,第一个参数containingFrame表示这个窗口所在的父容器,第二个参数displayFrame表示这个窗口所在的屏幕区域,也可以理解为在整个屏幕中的位置。根据前面computeFrameLw方法中我们可以知道,这些全屏类型的窗口的父容器和现实区域都是参数传过来的,代表着窗口的具体内容会放置在这些区域中,这里我们要知道的是,父窗口和现实区域都可能是小于真正屏幕的,而窗口的内容是在父窗口中的,显示区域包含了父窗口,屏幕包含了显示区域,所以这个方法除了计算窗口的大小外,还需要根据属性信息来把窗口或者显示区域安排在屏幕合适的位置,下面我们看具体的方法。
这个方法开始会获得父窗口的宽和高,然后获取窗口的Task(如果有的话)以及是否窗口是全屏类型的和是否运行窗口显示在屏幕外面。接着下面会判断计算窗口是否要适配屏幕,这里判断条件是如果task为null,或者是一个全屏容器模式的,或者不是一个应用的第一个Activity且允许显示在屏幕外,这些都需要再对显示区域进行适配。这里之所以要适配屏幕,根据前面我们说的,如果一个窗口本身就不是全屏类型的,那么他就有父容器,既然有父容器,那么父容器外部的显示区域说明在之前已经在屏幕中放置好了,这里就不需要再对显示区域放置了,而如果是一个全屏类型的窗口,他的显示区域也都是参数传递给他的,可能是一个大略的估值,所以在对窗口计算完毕后,还需要对现实区域在进行计算,使他能够放在一个合适的位置。这里如果是一个Activity并且运行这个Activity显示在屏幕外面的话,也需要重新对显示区域进行计算。
经过上面的一些准备工作后,就开始获取窗口的宽和高了,这里我们看到会根据窗口属性中的值来计算,比如是否有缩放的需要,有的话会对前面父窗口中获取宽和高进行缩放,如果宽和高是MATCH_PARENT的话,会使用前面我们介绍过的View测量得到传过来的mRequestedWidth和mRequestedHeight来赋值给窗口的宽和高。除了获取宽和高,也会从属性中获取这个窗口的开始坐标x和y。最后要保证计算出的窗口的宽和高要不超过父窗口的宽和高,所以会取他们中较小的值。这样的话,窗口的宽和高就计算完了,然后就会进行窗口在显示区域内的摆放,这就是后半部分方法主要做的事情,我们看下后半部分方法:
```java
.........
// 根据gravity和长宽,计算出窗口大小赋给mFrame
// Set mFrame
// 参数1 对齐方式
// 参数2 宽度
// 参数3 高度
// 参数4 父窗口rect
// 参数5 水平方向上的margin
// 参数6 垂直方向上的margin
// 参数7 计算目标窗口的rect
Gravity.apply(mAttrs.gravity, w, h, containingFrame,
(int) (x + mAttrs.horizontalMargin * pw),
(int) (y + mAttrs.verticalMargin * ph), mFrame);
// Now make sure the window fits in the overall display frame.
// 上面是在父窗口中计算大小,下面会根据水平或者垂直方式在整个屏幕中对齐计算
if (fitToDisplay) {
// 根据剪裁方式,根据屏幕的rect,设置下窗口大小mFrame
Gravity.applyDisplay(mAttrs.gravity, displayFrame, mFrame);
}
// We need to make sure we update the CompatFrame as it is used for
// cropping decisions, etc, on systems where we lack a decor layer.
// 兼容模式下,也是同样的值
mCompatFrame.set(mFrame);
// 进行缩放
if (mEnforceSizeCompat) {
// See comparable block in computeFrameLw.
mCompatFrame.scale(mInvGlobalScale);
}
.................
```
经过前面的计算,窗口的宽和高以及他在父容器的x和y轴都已经知道了,现在就开始根据这些数据进行位置的摆放,这里首先调用了Gravity的apply方法来计算窗口在父窗口内位置,然后如果窗口还需要在这个屏幕内进行计算的话,会调用Gravity的applyDisplay方法,这两个方法我们马山会跟进看下。最后如果是兼容模式的话,会对计算出来的窗口进行缩放,兼容模式保存在mCompatFrame中。这样这个窗口就计算完了,下面我们回过头去看看前面提到的两个计算窗口位置的方法,在看这两个方法前,我们先说一下关于Gravity方向的知识,关于Gravity相信开发android的同学是再熟悉不过的了,这个是描述一个元素和另一个元素之间位置关系的属性,上面两个计算窗口位置的方法中会用到这个属性,我们这里先熟悉下,下面分析方法的时候就可以不用在单独讲了。
```java
// 左右对齐的mask
public static final int AXIS_PULL_BEFORE = 0x0002;
// 上下对齐的mask
public static final int AXIS_PULL_AFTER = 0x0004;
// 可以理解为左右对齐的位移
public static final int AXIS_X_SHIFT = 0;
// 可以理解为上下对齐的位移
public static final int AXIS_Y_SHIFT = 4;
// 顶对齐
public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
// 底对齐
public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
// 左对齐
public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
// 右对齐
public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
// 裁剪方式
public static final int AXIS_CLIP = 0x0008;
// 垂直裁剪,垂直超过父窗口裁剪掉
public static final int CLIP_VERTICAL = AXIS_CLIP<<AXIS_Y_SHIFT;
// 水平裁剪
public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT;
// 垂直剪裁,垂直方向超过屏幕的会剪切掉
public static final int DISPLAY_CLIP_VERTICAL = 0x10000000;
// 水平剪裁,水平方向超过屏幕的会剪切掉
public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000;
```
上面代码中的这些属性我们在下面分析计算窗口位置的方法中都会遇到,这里先说一下。首先AXIS_PULL_BEFORE和AXIS_PULL_AFTER是表示水平对齐和垂直对齐,而水平和垂直又分为左右对齐和上下对齐,所以AXIS_PULL_BEFORE是代表左对齐和上对齐的,AXIS_PULL_AFTER是代表右对齐和下对齐的,但是只有2个变量怎么区分四种对齐方式呢?那么就会再结合AXIS_X_SHIFT和AXIS_Y_SHIFT来区分上,下,左,右四种对齐方式了。上面代码中TOP,BOTTOM,LEFT,RIGHT就是这四种结合的方式,相信大家应该都能理解,这里使用的都是为运算,对于我们软件开发来说位运算是一种比较常用的方法,如果对位运算不了解的同学可以去学习下,这里就不多说了。
接着我们看上面代码中的AXIS_CLIP这个属性代表的是裁剪方式,所谓裁剪方式,试想下如果我们的一个界面超过了父容器的时候应该怎么办呢,方法有很多,比如那边超过了可以让那一边往反方向移动一些使他重新显示在父容器的空间中,或者直接把超过的部分裁剪掉,不让他显示。这里AXIS_CLIP就是和裁剪有关的一个属性,这个裁剪主要就分为水平裁剪和垂直裁剪,水平裁剪包括的左右裁剪,垂直裁剪包括了上下裁剪,具体是左右还是上下根据实际场景中遇到的作处理就行,后面的方法分析中我们就会遇到这个场景。这里AXIS_CLIP和AXIS_Y_SHIFT结合就表示垂直裁剪,和AXIS_X_SHIFT集合表示水平裁剪,这里和前面对齐方式的逻辑是一样的。最后还有2个针对屏幕的裁剪方法DISPLAY_CLIP_VERTICAL和DISPLAY_CLIP_HORIZONTAL,含义和上面水平和垂直裁剪是一样的,只不过前面都是用在父子元素布局中,DISPLAY_CLIP_VERTICAL和DISPLAY_CLIP_HORIZONTAL是用在容器和屏幕之间的关系中所以单独分出来,这也正符合我们将要看的2个方法,一个是处理在父容器中的布局,一个是处理在屏幕中的布局。好了,上面关于Gravity相关的知识介绍完了,我们下面就开始看下前面提到的两个计算窗口位置的方法,首先看下在父窗口内进行位置计算的方法Gravity的apply:
```java
// 根据对齐方式,并且根据父窗口的大小,设置窗口的大小
// 参数1 对齐方式
// 参数2 宽度
// 参数3 高度
// 参数4 父窗口rect
// 参数5 水平方向上的margin
// 参数6 垂直方向上的margin
// 参数7 计算目标窗口的rect
public static void apply(int gravity, int w, int h, Rect container,
int xAdj, int yAdj, Rect outRect) {
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
case 0:
// 没有设置左右
// left = 父窗口居中后 + margin
outRect.left = container.left
+ ((container.right - container.left - w)/2) + xAdj;
// right = left + 宽度,保存宽度不变
outRect.right = outRect.left + w;
// 设置了水平裁剪CLIP_HORIZONTAL,如果left或者right
// 超过了父窗口,那么就设置为父窗口的left或者right
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
if (outRect.left < container.left) {
outRect.left = container.left;
}
if (outRect.right > container.right) {
outRect.right = container.right;
}
}
break;
// 左对齐
case AXIS_PULL_BEFORE<<AXIS_X_SHIFT:
// 左边和父窗口的margin
outRect.left = container.left + xAdj;
// 右边等于左边加上宽度
outRect.right = outRect.left + w;
// 等于CLIP_HORIZONTAL,水平裁剪
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
// 右边超过父窗口了,right设置为父窗口right
if (outRect.right > container.right) {
outRect.right = container.right;
}
}
break;
// 右对齐
case AXIS_PULL_AFTER<<AXIS_X_SHIFT:
// 右边和父窗口的margin
outRect.right = container.right - xAdj;
// 左边等于右边减去宽度
outRect.left = outRect.right - w;
// 水平裁剪CLIP_HORIZONTAL
if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
== (AXIS_CLIP<<AXIS_X_SHIFT)) {
// 左边超过父窗口了,left设置为父窗口left
if (outRect.left < container.left) {
outRect.left = container.left;
}
}
break;
default:
// 默认父窗口+margin,保持宽度不变,不需要保持在父窗口中间
outRect.left = container.left + xAdj;
outRect.right = container.right + xAdj;
break;
}
// 垂直对齐
switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
case 0:
outRect.top = container.top
+ ((container.bottom - container.top - h)/2) + yAdj;
outRect.bottom = outRect.top + h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.top < container.top) {
outRect.top = container.top;
}
if (outRect.bottom > container.bottom) {
outRect.bottom = container.bottom;
}
}
break;
case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT:
outRect.top = container.top + yAdj;
outRect.bottom = outRect.top + h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.bottom > container.bottom) {
outRect.bottom = container.bottom;
}
}
break;
case AXIS_PULL_AFTER<<AXIS_Y_SHIFT:
outRect.bottom = container.bottom - yAdj;
outRect.top = outRect.bottom - h;
if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
== (AXIS_CLIP<<AXIS_Y_SHIFT)) {
if (outRect.top < container.top) {
outRect.top = container.top;
}
}
break;
default:
outRect.top = container.top + yAdj;
outRect.bottom = container.bottom + yAdj;
break;
}
}
```
这段方法主要有两部分,分别是对左右对齐和上下对齐做不同的处理,上面这段是对左右对齐做处理。我们看到这里和gravity参数做与操作的就是前面介绍的左右对齐的位操作,前面由于做过介绍了,这里就不做详细分析了,这里下面的switch中有3个结果,0表示的是默认的其实会居中处理,AXIS_PULL_BEFORE<<AXIS_X_SHIFT表示左对齐,AXIS_PULL_AFTER<<AXIS_X_SHIFT表示右对齐,这里都是前面介绍的Gravity中的知识,理解前面的内容这里相信大家都能看得懂。
我们先看一下没有设置左右对齐时候的默认处理。这里可以看到窗口的left等于父窗口的left + 该窗口在父窗口居中后开始位置 + margin,而窗口的right等于left + 窗口宽度,其实就是把窗口居中在父窗口中,当然如果有margin的话,会加上一个偏移距离。之后如果设置了水平裁剪的话,会依次检查下左右两边是否超过了父窗口,如果超过了父窗口,则设置为父窗口的左右两边。这样不设置左右对齐的时候就处理完了,逻辑其实不难是吧,我们接着看左对齐的时候。
左对齐的时候其实更简单,窗口left = 父窗口的left,窗口right = 窗口left + 窗口宽度。如果设置了水平裁剪的话,只要检查下窗口right是否超过了父窗口,如果超过了父窗口就把窗口right设置为父窗口的right。右对齐的话,逻辑完全和左对齐一样,这里就不多说了。最后如果设置了其他的值,那么左右两边和父窗口的左右两边对齐,当然如果有margin的话也会加上margin,这样水平对齐就处理完了。方法的后半段是垂直对齐处理,经过水平对齐处理的分析,再看垂直对齐处理就很简单了。如果0的话就是把子窗口居中在父窗口中间,top对齐的话,就是子窗口的top对齐父窗口的top,bottom对齐的话,就是子窗口的bottom对齐父窗口的buttom,其余对齐的话就是子窗口的top和bottom和父窗口的bottom对齐,当然以上这些如果有margin的话,都需要加上。
至此这个方法就分析完了,其实通过上面方法我们可以看到摆放子窗口到父窗口中其实很容易,我们就主要根据对齐方法和父窗口的各个边对齐,然后如果有裁剪的话,根据裁剪做相应的处理就可以了。我们分析完了子窗口在父窗口中的布局方法,再看下整个窗口在屏幕中的布局,由于根据前面的分析,只有父窗口不确定的时候才会需要对屏幕进行适配,这个时候的父窗口是之前窗口计算后传过来的一个参数,他和屏幕之间是什么关系也不能确定,说不定会超出屏幕外也有可能,所以这里需要对屏幕在进行一下适配,我们看下这个方法:
```java
// 参数1 剪裁方式,水平或者垂直
// 参数2 屏幕大小
// 参数3 计算窗口的rect
// 根据剪裁方式,根据屏幕的rect,设置下窗口的rect
public static void applyDisplay(int gravity, Rect display, Rect inoutObj) {
// 如果垂直剪裁
if ((gravity&DISPLAY_CLIP_VERTICAL) != 0) {
// 窗口的top小于屏幕的top,把屏幕top设置给窗口
if (inoutObj.top < display.top) inoutObj.top = display.top;
// 窗口bottom大于屏幕bottom,把屏幕bottom设置给窗口
if (inoutObj.bottom > display.bottom) inoutObj.bottom = display.bottom;
} else {
// 非垂直剪裁,可能是水平的,那么还是要看下窗口高度是否和屏幕高度匹配
// 如果窗口高度大于屏幕,要缩小到和屏幕一样。如果窗口高度小于
int off = 0;
// 窗口top小于屏幕top
if (inoutObj.top < display.top) off = display.top-inoutObj.top;
// 窗口bottom大于屏幕bottom
else if (inoutObj.bottom > display.bottom) off = display.bottom-inoutObj.bottom;
// 如果窗口top或者bottom起码有一条边超出屏幕
if (off != 0) {
// 窗口高度大于屏幕高度。要把窗口限制限制在屏幕高度范围内,即top和bottom都设置为屏幕的
if (inoutObj.height() > (display.bottom-display.top)) {
inoutObj.top = display.top;
inoutObj.bottom = display.bottom;
} else {
// 如果窗口高度小于屏幕高度,那么肯定top和bottom只有一处是超过屏幕的,所以这里+off
// 后高度肯定还是不变的,但是把范围限定在屏幕内
inoutObj.top += off;
inoutObj.bottom += off;
}
}
}
// 水平剪裁
if ((gravity&DISPLAY_CLIP_HORIZONTAL) != 0) {
// 窗口left小于屏幕left,把屏幕left赋值给窗口
if (inoutObj.left < display.left) inoutObj.left = display.left;
// 窗口right大于屏幕right,把屏幕right赋值给窗口
if (inoutObj.right > display.right) inoutObj.right = display.right;
} else {
int off = 0;
if (inoutObj.left < display.left) off = display.left-inoutObj.left;
else if (inoutObj.right > display.right) off = display.right-inoutObj.right;
if (off != 0) {
if (inoutObj.width() > (display.right-display.left)) {
inoutObj.left = display.left;
inoutObj.right = display.right;
} else {
inoutObj.left += off;
inoutObj.right += off;
}
}
}
}
```
这个方法里面主要是根据屏幕的裁剪方式来对窗口进行裁剪。首先这里可以看到如果是垂直裁剪的话,如果窗口的top和bottom超过屏幕,那么就使用屏幕的top和bottom作为窗口的top和bottom。如果没有设置垂直裁剪的话,如果窗口的top和bottom起码有一处超过屏幕的话,这时候如果窗口的高度是大于屏幕的话,那么不管怎么调整窗口肯定也是超过屏幕的,所以直接把屏幕的top和bottom赋值给窗口就可以了。否则窗口高度不大于屏幕的话,把窗口调整到屏幕范围内就可以了。这就是垂直裁剪时候的处理。
接着就是处理水平裁剪了,逻辑还是一样的。如果打开了水平裁剪的标志,那么窗口的left和right只要超过了屏幕的大小,就会把屏幕的left和right赋值给窗口。否则如果没有打开水平裁剪的标志,如果窗口的left和right起码有一处是超过屏幕的,如果窗口的宽度是大于屏幕的,那么无论怎么调整窗口还是会超出屏幕,所以就直接把屏幕的left和right赋值给窗口,否则窗口宽度不大于屏幕,就把窗口调整到屏幕范围内。
# 测量布局其他窗口
至此窗口在整个屏幕范围内也就调整好了。我们从窗口布局说了这么多,还记得是从哪里一路走到这里的吗?我们是初始化了窗口的几个区域后,然后调用导航栏和状态栏的测量和布局方法后,最终一路走到这里的,现在我们回到DisplayContent的performLayout方法,我们再简单看一下代码:
```java
void performLayout(boolean initial, boolean updateInputWindows) {
............
mService.mPolicy.beginLayoutLw(isDefaultDisplay, dw, dh, mRotation,
getConfiguration().uiMode);
............
// 获取内容区域,赋给mContentRect
mService.mPolicy.getContentRectLw(mContentRect);
// 第一次遍历每个没有父窗口的窗口,比如根窗口,对他们进行测量布局
forAllWindows(mPerformLayout, true /* traverseTopToBottom */);
.........
// 对已经有父窗口的进行测量布局
forAllWindows(mPerformLayoutAttached, true /* traverseTopToBottom */);
.............
}
```
以上方法就是我们之前分析的代码,我们前面分析的一系列代码都是从上面的beginLayoutLw这个方法调用开始的,经过这个方法都导航栏和状态栏的测量和布局就完成了,同时一些窗口区域的范围也确定了,所以上面代码中我们看到会从WindowManagerPolicy中调用getContentRectLw方法把内容区域赋值给DisplayContent的mContentRect保存起来,接着我们之前说过会遍历所有窗口对其他窗口进行测量和布局。这里会分两种类型窗口进行处理,没有父窗口的和有父窗口的,这里我们首先看下对没有父窗口的窗口进行测量布局,会调用Lambda表达式进行处理,我们看下这个表达式:
```java
// 遍历每个没有attach窗口,比如根窗口,调用layoutWindowLw进行测量,布局
private final Consumer<WindowState> mPerformLayout = w -> {
// 如果这个窗口在锁屏界面下面或者是不显示的
final boolean gone = (mTmpWindow != null && mService.mPolicy.canBeHiddenByKeyguardLw(w))
|| w.isGoneForLayoutLw();
// 如果当前遍历到的窗口非隐藏,没有被测量过,需要被布局
if (!gone || !w.mHaveFrame || w.mLayoutNeeded
|| ((w.isConfigChanged() || w.setReportResizeHints())
&& !w.isGoneForLayoutLw() &&
((w.mAttrs.privateFlags & PRIVATE_FLAG_KEYGUARD) != 0 ||
(w.mHasSurface && w.mAppToken != null &&
w.mAppToken.layoutConfigChanges)))) {
// 如果没有绑定父窗口的,比如根窗口,进行测量布局
if (!w.mLayoutAttached) {
if (mTmpInitial) {
//Slog.i(TAG, "Window " + this + " clearing mContentChanged - initial");
w.mContentChanged = false;
}
// 屏保
if (w.mAttrs.type == TYPE_DREAM) {
// Don't layout windows behind a dream, so that if it does stuff like hide
// the status bar we won't get a bad transition when it goes away.
mTmpWindow = w;
}
w.mLayoutNeeded = false;
// 获取缩放因子
w.prelayout();
// 是否是第一次被layout
final boolean firstLayout = !w.isLaidOut();
// 对该窗口各个区域进行计算,最终得出窗口的大小
mService.mPolicy.layoutWindowLw(w, null);
// 赋值layout序号
w.mLayoutSeq = mService.mLayoutSeq;
// 如果是第一次layout
if (firstLayout) {
// 更新下WindowState中保存的各个区域的insets
w.updateLastInsetValues();
}
// Window frames may have changed. Update dim layer with the new bounds.
final Task task = w.getTask();
// 更新下Dimlayer层
if (task != null) {
mDimLayerController.updateDimLayer(task);
}
}
}
};
```
这个表达式也不算很长,首先这里需要测量布局的是那些会显示的窗口,如果窗口都不显示了,那么现在也没必要花费时间对他进行测量布局,所以开始会先判断这个窗口是否是可见的。如果这个窗口是可见的,那么同时他还符合比如没有进行过测量布局,或者有请求需要对他进行测量布局,或者窗口的Configuration有变化等等,那么就需要对他重新进行测量布局了。
这个表达式我们说了是测量那些没有父窗口的,也就是这个窗口的WindowState的mLayoutAttached是false的,接着会获取窗口的缩放因子,在前面测量布局导航栏和状态栏的时候我们也看到在兼容模式下会对窗口有缩放的处理,这里就是获取缩放因子的,调用的是WindowState的prelayout方法,我们看一眼这个方法:
```java
void prelayout() {
if (mEnforceSizeCompat) {
mGlobalScale = getDisplayContent().mCompatibleScreenScale;
mInvGlobalScale = 1 / mGlobalScale;
} else {
mGlobalScale = mInvGlobalScale = 1;
}
}
```
可以看到缩放因子是在DisplayContent的mCompatibleScreenScale中的,这个是在初始化屏幕时候赋值的,我们回到前面表达式。前面我们有分析到过,WMS测量每个窗口的时候,都有一个序列号值,这里会通过窗口的是否有序列号来检查是否是第一次被测量,如果是第一次被测量,在测量完后,会更新这个窗口的各个区域,比如内容区域,可见区域,stable区域,过扫描区域等。这里测量窗口的方法是PhoneWindowManager的layoutWindowLw,这个方法非常的长,我们稍后在看。经过测量后,会更下这个窗口在WMS中测量的序列号,这样下次再进行测量的时候就知道不是第一次测量了。最后还会更新下和窗口有关的Dim层,Dim层是一层阴影,比如我们常见对话框弹出的后面会有一层阴影层,就是Dim层控制的,这里我们了解一下就好,不多深入。这里的核心方法就是layoutWindowLw方法,他是测量窗口的方法,注意这里调用这个方法的时候,第二个参数传的是null,表示没有父窗口,如果测量一个有父窗口的时候,第二个参数传的就是父窗口的WindowState。
上面的lambda表达式是处理的没有父窗口的窗口,这里我们遗留了核心的测量方法layoutWindowLw,我们稍后来回来看这个方法,现在我们再来看一下测量有父窗口的lambda表达式:
```java
// 对已经attach到父窗口的进行测量布局
private final Consumer<WindowState> mPerformLayoutAttached = w -> {
// 如果attach到这个父窗口的
if (w.mLayoutAttached) {
// 不可见的窗口不用测量,return
if (mTmpWindow != null && mService.mPolicy.canBeHiddenByKeyguardLw(w)) {
return;
}
// 如果是可见的同时是有调用过layout的,或者没测量过,或者需要进行layout
if ((w.mViewVisibility != GONE && w.mRelayoutCalled) || !w.mHaveFrame
|| w.mLayoutNeeded) {
if (mTmpInitial) {
//Slog.i(TAG, "Window " + this + " clearing mContentChanged - initial");
w.mContentChanged = false;
}
w.mLayoutNeeded = false;
// 适配的缩放因子
w.prelayout();
// 对窗口各个区域进行计算,最终得出窗口的大小
// 简单说就是初始化windowState中各个区域的值
mService.mPolicy.layoutWindowLw(w, w.getParentWindow());
// 赋值layout序号
w.mLayoutSeq = mService.mLayoutSeq;
}
} else if (w.mAttrs.type == TYPE_DREAM) {
// Don't layout windows behind a dream, so that if it does stuff like hide the
// status bar we won't get a bad transition when it goes away.
mTmpWindow = mTmpWindow2;
}
};
```
这个表达式的逻辑和前面测量没父窗口表达式类似。首先还是如果该窗口是不可见的,就不用测量了,直接return。否则如果是可见的同时有调用过请求重新测量布局,或者重新没有测量布局过,或者请求了测量布局,那么就会继续测量。这里测量前同样也是会先调用WindowState的prelayout方法来获取下缩放因子,之后之类也调用了WindowManagerPolicy的layoutWindowLw方法来测量,这里我们看到调用这个方法的时候第二个参数传入了当前这个窗口的父窗口,测量完后同样也是更新下在WMS中记录的测量窗口序列号,这样对有父窗口的窗口的测量就完成了。从上面2个lambda表达式我们看到,最核心的方法都是WindowManagerPolicy中的layoutWindowLw方法,那么下面我们就来看下这个方法是怎样测量窗口的。
# 测量布局窗口layoutWindowLw方法
```java
public void layoutWindowLw(WindowState win, WindowState attached) {
// 如果是状态栏或者导航栏,应该已经测量过了,所以返回。
// 这个状态栏如果可以接受输入事件,那么就不会return,重新测量布局
if ((win == mStatusBar && !canReceiveInput(win)) || win == mNavigationBar) {
return;
}
final WindowManager.LayoutParams attrs = win.getAttrs();
final boolean isDefaultDisplay = win.isDefaultDisplay();
// 如果是默认屏幕,当前要计算的这个是输入法目标窗口,输入法窗口非空
final boolean needsToOffsetInputMethodTarget = isDefaultDisplay &&
(win == mLastInputMethodTargetWindow && mLastInputMethodWindow != null);
// 调整这个窗口的内容区域,因为是从上到下遍历的,所以如果出现这里输入法非空的情况,说明当前
// 这个输入法目标窗口出现在了输入法上面,所以需要调整下,不能把输入法窗口给遮挡了
if (needsToOffsetInputMethodTarget) {
if (DEBUG_LAYOUT) Slog.i(TAG, "Offset ime target window by the last ime window state");
// 输入法目标窗口在Z轴层序上在输入法窗口的上面了,所以要确保当前这个目标窗口不能挡住输入法
offsetInputMethodWindowLw(mLastInputMethodWindow);
}
// 获取窗口的flag
final int fl = PolicyControl.getWindowFlags(win, attrs);
final int pfl = attrs.privateFlags;
// 获取软键盘模式
final int sim = attrs.softInputMode;
// 获取systemUI的显示模式
final int sysUiFl = PolicyControl.getSystemUiVisibility(win, null);
final Rect pf = mTmpParentFrame; // 父窗口
final Rect df = mTmpDisplayFrame; // 屏幕
final Rect of = mTmpOverscanFrame; // 过扫描区域的四周
final Rect cf = mTmpContentFrame; // 内容区域
final Rect vf = mTmpVisibleFrame; // 可见区域
final Rect dcf = mTmpDecorFrame; // 装饰区域,内部排除状态栏,导航栏,输入法等
final Rect sf = mTmpStableFrame; // stable区域,内容永远不包含状态栏和导航栏,输入法等
Rect osf = null;
dcf.setEmpty();
// 是否有导航栏
final boolean hasNavBar = (isDefaultDisplay && mHasNavigationBar
&& mNavigationBar != null && mNavigationBar.isVisibleLw());
// 软键盘模式
final int adjust = sim & SOFT_INPUT_MASK_ADJUST;
if (isDefaultDisplay) {
// 默认屏幕stable区域就是stable的设置
sf.set(mStableLeft, mStableTop, mStableRight, mStableBottom);
} else {
// 非默认屏幕,就是全屏
sf.set(mOverscanLeft, mOverscanTop, mOverscanRight, mOverscanBottom);
}
...............
}
```
这个方法非常的长,我们先来看这里的第一段。首先这里会判断这个窗口是不是状态栏和导航栏,由于前面已经测量过这两个窗口了,所以这里遇到这两个窗口就会return。这里要注意的是一般状态栏是不会接受输入事件,但是如果状态栏可以接受输入事件,那么这里就会重新对这个窗口测量,下面代码我们可以看到这一点。
我们遍历所有的窗口都是从上往下遍历的,所以一般来说对于输入法窗口的话会出现在最上面,但是如果遇到一个输入法目标窗口出现在了输入法窗口的上面的情况,那么就需要调整,要确保目标窗口不能挡住输入法,所以上面方法中我们可以看到如果出现这种情况会调用offsetInputMethodWindowLw方法进行调整,我们跟进看下这个方法:
```java
// 调整当前输入法目标窗口的内容和可见区域底部要在输入法窗口的上面
// 这样才不会挡住输入法窗口
// 参数是输入法窗口
private void offsetInputMethodWindowLw(WindowState win) {
// 获取输入法内容区域的顶部
int top = Math.max(win.getDisplayFrameLw().top, win.getContentFrameLw().top);
// 加上给定的输入法内容区域的衬边top
top += win.getGivenContentInsetsLw().top;
// 输入法上面的窗口不能挡住输入法,所以要把底部安排在输入法的上面
if (mContentBottom > top) {
mContentBottom = top;
}
if (mVoiceContentBottom > top) {
mVoiceContentBottom = top;
}
// 输入法窗口可视区域的顶部
top = win.getVisibleFrameLw().top;
// 加上给定的输入法窗口可视区域的衬边top
top += win.getGivenVisibleInsetsLw().top;
// 输入法上面的窗口不能挡住输入法,所以需要把可视区域安排在输入法窗口的上面
if (mCurBottom > top) {
mCurBottom = top;
}
}
```
这个方法整体逻辑也很简单,首先获取输入法窗口的内容区域和可视区域的top,如果有衬边的话,还会加上衬边的高度,然后判断目前窗口的内容区域和可视区域的bottom是否在输入法窗口的top下面,如果是的话,说明把输入法窗口给遮挡了,所以这里会调整把内容区域和可视区域安排在输入法窗口的上面,这样就不影响了。一般这种情况比较少见,我们这里了解有这种情况就可以了,我们回到前面方法。
上面处理好了输入法窗口上面的窗口后,接着获取下这个窗口相关的一些参数,比如flag,软键盘模式,systemUI的显示模式,也就是状态栏和导航栏显示模式以及是否有显示导航栏。然后准备好所有相关的显示区域后,准备开始窗口显示区域的设置了。这里可以看到stable显示区域的话,如果是默认屏幕的话就是之前计算的stable区域,也就是状态栏和导航栏包围的区域,否则是过扫描区包围的区域。这样的话,基本上准备工作都做好了,下面开始正式的测量和布局工作了。
```java
if (!isDefaultDisplay) {
// 有父窗口
if (attached != null) {
// If this window is attached to another, our display
// frame is the same as the one we are attached to.
// 那么设置这个测量窗口的几个显示区域,会根据父窗口来设置
setAttachedWindowFrames(win, fl, adjust, attached, true, pf, df, of, cf, vf);
} else {
// Give the window full screen.
// 非默认屏幕,没有父窗口,几个区域的值,基本就是设置为全屏幕
pf.left = df.left = of.left = cf.left = mOverscanScreenLeft;
pf.top = df.top = of.top = cf.top = mOverscanScreenTop;
pf.right = df.right = of.right = cf.right
= mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom
= mOverscanScreenTop + mOverscanScreenHeight;
}
} else if (attrs.type == TYPE_INPUT_METHOD) { // 到这里默认屏幕,如果是输入法窗口
// 几个区域的左上右都设置为输入法区域
pf.left = df.left = of.left = cf.left = vf.left = mDockLeft;
pf.top = df.top = of.top = cf.top = vf.top = mDockTop;
pf.right = df.right = of.right = cf.right = vf.right = mDockRight;
// IM dock windows layout below the nav bar...
// 父窗口,屏幕,包含过扫描的区域,底部包含导航栏
pf.bottom = df.bottom = of.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
// ...with content insets above the nav bar
// 输入法内容区域,可见区域底部不包含导航栏
cf.bottom = vf.bottom = mStableBottom;
// 如果状态里非空,最后一次焦点窗口是状态里,同时状态栏可以接受输入事件
if (mStatusBar != null && mFocusedWindow == mStatusBar && canReceiveInput(mStatusBar)) {
// The status bar forces the navigation bar while it's visible. Make sure the IME
// avoids the navigation bar in that case.
// 如果导航栏在右边
if (mNavigationBarPosition == NAV_BAR_RIGHT) {
// 各个区域的右边都是mStableRight,也就是排除了导航栏的右边
pf.right = df.right = of.right = cf.right = vf.right = mStableRight;
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
// 如果导航栏在左边,各个区域的左边就是mStableLeft,也就是排除了导航栏的左边
pf.left = df.left = of.left = cf.left = vf.left = mStableLeft;
}
}
// IM dock windows always go to the bottom of the screen.
// 这里是输入法,下面对齐
attrs.gravity = Gravity.BOTTOM;
// 获取输入窗口的z轴层序
mDockLayer = win.getSurfaceLayer();
} else if (attrs.type == TYPE_VOICE_INTERACTION) { // 语音交互窗口
pf.left = df.left = of.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
cf.left = mDockLeft;
cf.top = mDockTop;
cf.right = mDockRight;
cf.bottom = mDockBottom;
} else {
cf.left = mContentLeft;
cf.top = mContentTop;
cf.right = mContentRight;
cf.bottom = mContentBottom;
}
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
vf.left = mCurLeft;
vf.top = mCurTop;
vf.right = mCurRight;
vf.bottom = mCurBottom;
} else {
vf.set(cf);
}
} else if (attrs.type == TYPE_WALLPAPER) { // 壁纸窗口测量布局
layoutWallpaper(win, pf, df, of, cf);
} else if (win == mStatusBar) { // 状态栏
// 父窗口,屏幕和过扫描区域,都设置为不包含过扫描的那些区域
pf.left = df.left = of.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = mUnrestrictedScreenWidth + mUnrestrictedScreenLeft;
pf.bottom = df.bottom = of.bottom = mUnrestrictedScreenHeight + mUnrestrictedScreenTop;
// 内容区域和可视区域设置为Stable区域,即不包含状态栏和导航栏区域
cf.left = vf.left = mStableLeft;
cf.top = vf.top = mStableTop;
cf.right = vf.right = mStableRight;
vf.bottom = mStableBottom; // 可视区域的底部是stable底部
// 如果软键盘调整的话
if (adjust == SOFT_INPUT_ADJUST_RESIZE) {
// 需要根据软键盘调整的话,状态栏内容区域不能在内容区域下面
cf.bottom = mContentBottom;
} else { // 如果不调整输入法弹出时候的界面的话
// 不需要根据软键盘调整的话,那就和输入法底部一样就可以
cf.bottom = mDockBottom;
// 可视区域不能低于内容区域
vf.bottom = mContentBottom;
}
} else {
.............
}
...........
```
这段代码开始测量窗口了,其实前面我们分析了那么多,所谓测量主要就是设置那几个不同区域,什么内容区域,可见区域,stable区域等等,这里会根据不同类型的窗口做不同的处理,首先如果是非默认屏幕,如果是一个子窗口会调用setAttachedWindowFrames方法来根据他的父窗口设置相关几个区域,如果不是子窗口的话,这里就使用Overscan包围区域来设置相关的几个区域,也就是用过扫描包围的区域设置给内容区域,装饰区域等几个区域。这里我们主要来看一下setAttachedWindowFrames这个方法,这个方法是利用父窗口给子窗口测量,后面还会看到测量的时候会调用这个方法,这里就先来看下这个方法。
```java
// 设置参数win是参数attached的子窗口,这个方法根据attached父窗口计算子窗口的几个区域的值
void setAttachedWindowFrames(WindowState win, int fl, int adjust, WindowState attached,
boolean insetDecors, Rect pf, Rect df, Rect of, Rect cf, Rect vf) {
// 子窗口的输入法上面,父窗口在输入法下面,这个情况有问题,所以就用输入法的区域值设置给子窗口
if (win.getSurfaceLayer() > mDockLayer && attached.getSurfaceLayer() < mDockLayer) {
df.left = of.left = cf.left = vf.left = mDockLeft;
df.top = of.top = cf.top = vf.top = mDockTop;
df.right = of.right = cf.right = vf.right = mDockRight;
df.bottom = of.bottom = cf.bottom = vf.bottom = mDockBottom;
} else {
// 如果不需要对软键盘进行界面调整
if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
// 如果子窗口设置了FLAG_LAYOUT_ATTACHED_IN_DECOR这个flag,表示子窗口的内容区域希望在父窗口的DecorView中
// 否则就用过扫描区域
cf.set((fl & FLAG_LAYOUT_ATTACHED_IN_DECOR) != 0
? attached.getContentFrameLw() : attached.getOverscanFrameLw());
} else {
// 设置为父窗口的内容区域
cf.set(attached.getContentFrameLw());
if (attached.isVoiceInteraction()) { // 声音窗口处理
if (cf.left < mVoiceContentLeft) cf.left = mVoiceContentLeft;
if (cf.top < mVoiceContentTop) cf.top = mVoiceContentTop;
if (cf.right > mVoiceContentRight) cf.right = mVoiceContentRight;
if (cf.bottom > mVoiceContentBottom) cf.bottom = mVoiceContentBottom;
} else if (attached.getSurfaceLayer() < mDockLayer) {
// 进入到这里说明该窗口父窗口在输入法窗口下面,子窗口也在输入法窗口下面(否则在方法最开始的if就处理了)
// 同时该子窗口需要根据输入法调整大小,而父窗口可能是不需要调整大小的,所以取父窗口和整个内容区域
// 的交集,mContentTop在前面初始化和导航栏,状态栏测量时候已经初始化过了,基本和Deck区域一样
if (cf.left < mContentLeft) cf.left = mContentLeft;
if (cf.top < mContentTop) cf.top = mContentTop;
if (cf.right > mContentRight) cf.right = mContentRight;
if (cf.bottom > mContentBottom) cf.bottom = mContentBottom;
}
}
// 设置屏幕区域为父窗口的屏幕区域或者自己cf
df.set(insetDecors ? attached.getDisplayFrameLw() : cf);
// 设置过扫描区域为父窗口的过扫描区域
of.set(insetDecors ? attached.getOverscanFrameLw() : cf);
// 设置可见区域为父窗口的可见区域
vf.set(attached.getVisibleFrameLw());
}
// 如果没有FLAG_LAYOUT_IN_SCREEN,即没有忽略状态栏和导航栏,那么父窗口就用attached的显示范围,即mFrame
// 否则父窗口用屏幕区域
pf.set((fl & FLAG_LAYOUT_IN_SCREEN) == 0
? attached.getFrameLw() : df);
}
```
首先这里会遇到一个特殊的情况,父窗口在最下面,中间是输入法窗,而需要计算的窗口在最上面,这种情况一般来说也比较少见,这里的处理会用输入法窗口的区域赋值给当前这个窗口,这样当前窗口会直接覆盖在输入法窗口上面,这里就不考虑父窗口的情况了。
如果是一个正常的子窗口一般会显示在父窗口的上方,这里主要是设置内容区域,可见区域会和父窗口是一样的,因为子窗口要显示在父窗口可见的范围内。这里会先判断下这个窗口是否有设置了软键盘调整,如果没有设置的话,说明不会对这个窗口进行调整,那么会看看他有没有设置FLAG_LAYOUT_ATTACHED_IN_DECOR这个flag,这个flag表示这个窗口希望显示在他的装饰区域里面,装饰区域就是不包含状态栏和导航等的区域,如果有设置这个flag,那么会把父窗口的内容区域设置给子窗口的内容区域,那么子窗口的内容区域就和父窗口一样了。否则没有设置这个flag的话,那么就会把父窗口的过扫描区域赋值给子窗口,这样子窗口的显示的区域就更大一些了。
如果子窗口设置了SOFT_INPUT_ADJUST_RESIZE这个flag,说明子窗口是可以调整的,默认的话子窗口的内容区域还是等于父窗口的内容区域,但是还会有一些特殊处理,首先如果是音量窗口的话会判断下,如果音量窗口的内容区域小于父窗口的区域,那么会把内容区域设置为音量窗口。另外,如果父窗口是显示在输入法窗口下面的话,那么当前子窗口目前也是显示在输入法窗口下面的,因为这个方法开头的if已经处理的子窗口显示在上面的情况了,那么目前这个子窗口又设置了SOFT_INPUT_ADJUST_RESIZE这个flag,所以需要调整他们内容区域,由于父窗口可能是没有添加SOFT_INPUT_ADJUST_RESIZE这个flag的,所以父窗口的内容区域对于子窗口来说就是不正确的,所以这里会取当前系统测量中的内容区域和父窗口的进行比对,当前系统的内容区域在整个测量初始化以及状态栏和导航栏的测量中已经保证不会超过输入法的内容区域外,所以这里取他们之间较小的来设置给当前窗口的内容区域。
最后屏幕显示区域和过扫描区域如果要求和父窗口一样的,那么就使用父窗口的,否则就使用当前内容区域的,最后可视区域会使用父窗口的,不管父窗口有没有根据输入法调整大小,可视区域肯定是不会被输入法挡住的,所以设置成和父窗口一样就可以了。最后如果父窗口设置了FLAG_LAYOUT_IN_SCREEN这个flag,表示该窗口的父窗口是全屏区域,那么会把屏幕区域赋值给他的父窗口,否则就取他的父窗口大小赋值给他的父窗口区域。这样一个子窗口的计算就完成了,子窗口主要会根据父窗口的一些情况来做处理,当前我们看到的是非默认屏幕中调用了这个方法,后面默认屏幕中计算子窗口也会调用这个方法,我们下面会看到,我们回到前面layoutWindowLw方法。
下面我们分别会看到几个不同类型的窗口的大小计算。首先是输入法窗口,输入法窗口的各个区域大小基本就是使用之前测量的Dock区域,这里不一样的是内容区域和可见区域的底部是使用的stable区域,同样在左右横屏的时候可以看到,他的各个区域的left或者right也是使用的stable区域,所以他是排除导航栏的。最后这个窗口的对齐方式是底部对齐,这个也很好理解,输入法都是从底部显示的。
之后的音量窗口和壁纸窗口也都有各自的测量方式,这里就不再对每一个展开说了。但是最后我们再来看一下这个还有个对状态栏的处理,在layoutWindowLw这个方法的开头我们已经看到如果在正常情况下,会跳过状态栏和导航栏的处理,但是如果状态栏是可以接受输入事件的时候,会在对状态栏进行处理,这里就是对可接受输入事件情况下的状态栏进行处理,这里可以看到状态栏的各个区域默认还是使用mUnrestrictedScreenXXX区域来设置,也即使包括状态栏和导航栏,而对于bottom来说这里的设置根据是否设置了SOFT_INPUT_ADJUST_RESIZE这个flag来决定,即是否可以调节窗口大小,如果设置了这个flag,那么窗口的内容区域bottom设置为之前初始化时候的内容区域的bottom,可视区域bottom等于stable区域的bottom,如果没有设置SOFT_INPUT_ADJUST_RESIZE这个flag,那么窗口的内容区域的bottom被设置为Dock区域的bottom,可视区域的bottom被设置为初始化内容区域的bottom。这里前面输入法窗口设置的时候,已经确保输入法窗口会显示在状态栏的下面,所以这里这样设置正常情况下应该不会导致状态栏被输入法遮挡,是不是状态栏的高度很大的时候会有特殊的情况,这个场景就不是太清楚了,有比较了解这块的同学可以指教一下。总之这里的状态栏是可以接受输入事件的,所以这里是对输入法窗口进行调整的处理。好了,上面这一段方法主要是对一些特殊窗口的处理,接着开始就是对一些常规的情况进行处理了,我们接着看下面的代码:
```java
...........
// 装饰区域就是设置排除系统区域的部分
dcf.left = mSystemLeft;
dcf.top = mSystemTop;
dcf.right = mSystemRight;
dcf.bottom = mSystemBottom;
// 是否保存前一个窗口的透明状态
final boolean inheritTranslucentDecor = (attrs.privateFlags
& WindowManager.LayoutParams.PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR) != 0;
// 是否是Activity
final boolean isAppWindow =
attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW &&
attrs.type <= WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
// 是否是一个最顶部可以覆盖住所有窗口的窗口,比如声音窗口
final boolean topAtRest =
win == mTopFullscreenOpaqueWindowState && !win.isAnimatingLw();
// 如果是个Activity同时不是保持前一个窗口透明状态的也不是最顶部全屏的窗口时候
if (isAppWindow && !inheritTranslucentDecor && !topAtRest) {
// 系统非全屏模式
if ((sysUiFl & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0
// 父窗口非全屏
&& (fl & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0
// 父窗口非透明状态栏
&& (fl & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) == 0
// 状态栏背景可绘制
&& (fl & WindowManager.LayoutParams.
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
// 强制状态栏背景
&& (pfl & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND) == 0) {
// Ensure policy decor includes status bar
// 上面的flag都排除掉了不显示状态栏或者自定义状态栏背景,所以装饰栏顶部还是stable区域
// 装饰区域顶部 = stable顶部
dcf.top = mStableTop;
}
// 这里也不设置隐藏导航栏的flag,所以装饰区域底部和右边也是stable区域
if ((fl & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) == 0
&& (sysUiFl & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0
&& (fl & WindowManager.LayoutParams.
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0) {
// Ensure policy decor includes navigation bar
dcf.bottom = mStableBottom;
dcf.right = mStableRight;
}
}
// FLAG_LAYOUT_IN_SCREEN表示要全屏,所以pf,df,of这个最外面的窗口要大一点
// FLAG_LAYOUT_INSET_DECOR表示内容区域cf,vf在装饰区里面
if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
== (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
// 有父窗口
if (attached != null) {
// If this window is attached to another, our display
// frame is the same as the one we are attached to.
// 根据父窗口来设置几个区域
setAttachedWindowFrames(win, fl, adjust, attached, true, pf, df, of, cf, vf);
} else {
if (attrs.type == TYPE_STATUS_BAR_PANEL
|| attrs.type == TYPE_STATUS_BAR_SUB_PANEL) {
// 从状态栏下拉后的窗口
pf.left = df.left = of.left = hasNavBar
? mDockLeft : mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = hasNavBar
? mRestrictedScreenLeft+mRestrictedScreenWidth
: mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = hasNavBar
? mRestrictedScreenTop+mRestrictedScreenHeight
: mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
// 父窗口允许拓展到过扫描区域,同时窗口类型是Actvity和子窗口
} else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
// 父窗口,屏幕区域,过扫描区域都是设置包含过扫描区域的
pf.left = df.left = of.left = mOverscanScreenLeft;
pf.top = df.top = of.top = mOverscanScreenTop;
pf.right = df.right = of.right = mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = mOverscanScreenTop
+ mOverscanScreenHeight;
} else if (canHideNavigationBar() // 可以隐藏导航栏
// 导航栏被隐藏了
&& (sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
// 窗口类型是Activity和子窗口
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
// 父窗口和屏幕区域设置包含过扫描区域
pf.left = df.left = mOverscanScreenLeft;
pf.top = df.top = mOverscanScreenTop;
pf.right = df.right = mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = mOverscanScreenTop + mOverscanScreenHeight;
of.left = mUnrestrictedScreenLeft;
of.top = mUnrestrictedScreenTop;
of.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
of.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
} else {
pf.left = df.left = mRestrictedOverscanScreenLeft;
pf.top = df.top = mRestrictedOverscanScreenTop;
pf.right = df.right = mRestrictedOverscanScreenLeft
+ mRestrictedOverscanScreenWidth;
pf.bottom = df.bottom = mRestrictedOverscanScreenTop
+ mRestrictedOverscanScreenHeight;
of.left = mUnrestrictedScreenLeft;
of.top = mUnrestrictedScreenTop;
of.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
of.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
}
// 父窗口非全屏
if ((fl & FLAG_FULLSCREEN) == 0) {
// 声音窗扣,设置内容区域
if (win.isVoiceInteraction()) {
cf.left = mVoiceContentLeft;
cf.top = mVoiceContentTop;
cf.right = mVoiceContentRight;
cf.bottom = mVoiceContentBottom;
} else {
// 非调整软键盘模式,那就是输入法区域
if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
cf.left = mDockLeft;
cf.top = mDockTop;
cf.right = mDockRight;
cf.bottom = mDockBottom;
} else {
// 调整软键盘模式
cf.left = mContentLeft;
cf.top = mContentTop;
cf.right = mContentRight;
cf.bottom = mContentBottom;
}
}
} else { // 全屏模式
// 这里内容区域基本不包括状态栏和导航栏,过扫描区域
cf.left = mRestrictedScreenLeft;
cf.top = mRestrictedScreenTop;
cf.right = mRestrictedScreenLeft + mRestrictedScreenWidth;
cf.bottom = mRestrictedScreenTop + mRestrictedScreenHeight;
}
// 如果是SYSTEM_UI_FLAG_LAYOUT_STABLE,即不占用状态栏和导航栏
// 用stable来更新内容区域contentFrame
applyStableConstraints(sysUiFl, fl, cf);
// 如果软键盘不是什么都不做
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
// 可见区域设置为不包含状态栏,导航栏和输入法的区域
vf.left = mCurLeft;
vf.top = mCurTop;
vf.right = mCurRight;
vf.bottom = mCurBottom;
} else {
// 这里什么对于输入法什么都不做处理,所以可见区域等于内容区域
vf.set(cf);
}
}
} else{
..........
}
...........
```
这段代码首先设置装饰栏区域,这里默认设置的就是system的区域,system的区域在初始化的是已经被设置在了排除掉系统UI的区域里,这里系统UI主要是指状态栏和导航栏的区域,如果状态栏和导航栏显示着的话,那么system区域会排除掉他们,如果状态栏和导航栏不显示的话,那么system区域就会包含状态栏和导航栏显示的这些区域。装饰栏也是指排除那些systemUI后的区域,其实这里赋值后也可以确定排除systemUI了,但是这里还会通过查看状态栏和导航栏是否有显示,如果有显示的话,重新用stable区域赋值给装饰栏区域。
设置完装饰区后,如果当前这个窗口的flag设置了FLAG_LAYOUT_IN_SCREEN和FLAG_LAYOUT_INSET_DECOR这两个值,FLAG_LAYOUT_IN_SCREEN这个值表示窗口希望在全屏显示,FLAG_LAYOUT_INSET_DECOR这个值表示窗口的内容区域和可视区域在装饰栏的区域,所以上面接着的一段代码就是在这个条件下处理。
首先attached非空,表示有父窗口,同样会调用setAttachedWindowFrames这个方法来处理子窗口,这个方法前面我们已经介绍过了,这里就不多说了。非子窗口的话,下面还会对几种情况的窗口分别处理,主要是处理他们的父窗口区域,屏幕显示区域,过扫描区域,因为这几个区域对于窗口来说是确定最大的显示范围,下面几个窗口的区域需要调整一下最大的显示范围,但是对于内容区域和可见区域还是需要按照正常的来设置。
首先TYPE_STATUS_BAR_PANEL和TYPE_STATUS_BAR_SUB_PANEL是表示状态栏窗口下拉后出现的面板,这种窗口会单独处理他们的各个区域。之后如果窗口flag中设置了FLAG_LAYOUT_IN_OVERSCAN这个值,表示这个窗口显示区域最大可以拓展到过扫描区域内,所以这里会把父窗口,屏幕显示区域,过扫描区域设置为前面初始化的过扫描区域的值mOverscanScreenXXX。之后如果导航栏是隐藏的也会设置各个区域的值。最后父窗口区域和屏幕显示区域主要是按照mOverscanScreeXXX区域来设置,过扫描区域按照mUnrestrictedScreenXXX来设置。
上面这些主要是处理了父窗口区域,屏幕显示区域和过扫描区域,接着会处理内容区域和可见区域。首先还是看这个窗口的flag是否设置了全屏的值FLAG_FULLSCREEN,如果没有设置,说明是并非要在全屏中显示。那么如果窗口是音量窗口,会把音量窗口内容区域设置给这个窗口的内容区域。不是内容区域的话,会根据是否设置了需要根据键盘来调整窗口大小的值SOFT_INPUT_ADJUST_RESIZE来处理,如果设置了需要根据键盘来调整,那么内容区域就是输入法区域Dock区域,否则就是之前初始化的内容区域mContent区域。如果这个窗口设置了全屏显示的flag,那么他的内容区域就是mUnrestrictedScreen区域的值,也就是在导航栏和状态栏之内的区域。
内容区域还和一个systemUI相关的flag有关,SYSTEM_UI_FLAG_LAYOUT_STABLE这个flag表示这个窗口不会占用状态栏和导航栏,我们知道一旦切换显示状态的时候,如果状态栏和导航栏有变化,界面会刷新重新测量布局和绘制,这样界面就会抖动一下,这个flag表示的是最坏的情况下界面的布局,也就是不占用状态栏和导航栏,这样不管是否打开了状态栏和导航栏,当前显示的界面都会在上下空出来一块,但是也确实比较难看,但是也是提供的一种需求。但是设置了这个flag的时候,会调用applyStableConstraints方法来更新内容窗口的大小,我们马上就会跟进这个方法。我们接着往下看最后会处理可视区域的大小,这个就比较简单,如果软键盘调整窗口大小的值SOFT_INPUT_ADJUST_NOTHING被设置了,那么内容区域就是当前显示区域mCurXXX,这个值也是在初始化和测量导航栏和状态栏后更新的,在输入法窗口显示的情况下,这个值代表了输入法可见区域的top,在前面我们分析offsetInputMethodWindowLw方法时候,这个值会被更新,所以可见区域在输入法窗口显示的情况下就是在输入法窗的上面,如果没有设置SOFT_INPUT_ADJUST_NOTHING的话,那么可见区域就是内容区域。到这里在窗口最大可以在全屏显示下的各个区域就设置好了,我们回头看下上面说的applyStableConstraints这个方法,他会更新下当设置了SYSTEM_UI_FLAG_LAYOUT_STABLE这个flag时候的内容区域:
```java
// 如果是SYSTEM_UI_FLAG_LAYOUT_STABLE,即表示不占用状态栏和导航栏
// 用stable来更新内容区域contentFrame
private void applyStableConstraints(int sysui, int fl, Rect r) {
// 如果窗口flag表示不占用状态栏和导航栏,那么根据stable来设置内容区域
if ((sysui & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) {
// 如果父窗口是全屏,用stableFull来更新
if ((fl & FLAG_FULLSCREEN) != 0) {
if (r.left < mStableFullscreenLeft) r.left = mStableFullscreenLeft;
if (r.top < mStableFullscreenTop) r.top = mStableFullscreenTop;
if (r.right > mStableFullscreenRight) r.right = mStableFullscreenRight;
if (r.bottom > mStableFullscreenBottom) r.bottom = mStableFullscreenBottom;
} else {
//父窗口是非全屏的,用stable来更新
if (r.left < mStableLeft) r.left = mStableLeft;
if (r.top < mStableTop) r.top = mStableTop;
if (r.right > mStableRight) r.right = mStableRight;
if (r.bottom > mStableBottom) r.bottom = mStableBottom;
}
}
}
```
这个方法主要就是用来处理SYSTEM_UI_FLAG_LAYOUT_STABLE这个flag的,这个flag的作用前面也说过了,就是是当前窗口固定在stable区域里面,不会随着systemUI的改变而改变,那么既然是固定那么肯定有关固定的值,这里会根据是否设置了FLAG_FULLSCREEN这个flag来确定,也就是是否是全屏显示,如果是全屏显示的话内容区域是设置为mStableFullscreenXXX这个区域的,mStableFullscreenXXX和mStableXXX的区别就是mStableFullscreenXXX是包含状态栏的。而当导航栏显示的时候是不包含导航栏的,不显示的时候会延伸到导航栏区域,而mStableXXX是永远不包含的状态栏和导航栏的,这个方法也就是分别根据这2种情况来设置内容区域。理解了这里的含义,上面这个方法也无需多说了,相信大家也都能看得懂。
到这窗口设置了全屏的flag的情况这里第一步就都处理完了,我们接着后面看代码:
```java
..........
else if ((fl & FLAG_LAYOUT_IN_SCREEN) != 0 || (sysUiFl
& (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
// 到这里父窗口在stableFull下进行,或者内容区域会延伸到状态栏或者导航栏下面
if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle() +
"): IN_SCREEN");
// A window that has requested to fill the entire screen just
// gets everything, period.
// 窗口是状态栏下拉菜单,或者音量
if (attrs.type == TYPE_STATUS_BAR_PANEL
|| attrs.type == TYPE_STATUS_BAR_SUB_PANEL
|| attrs.type == TYPE_VOLUME_OVERLAY) {
pf.left = df.left = of.left = cf.left = hasNavBar
? mDockLeft : mUnrestrictedScreenLeft;
pf.top = df.top = of.top = cf.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = cf.right = hasNavBar
? mRestrictedScreenLeft+mRestrictedScreenWidth
: mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = hasNavBar
? mRestrictedScreenTop+mRestrictedScreenHeight
: mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
if (DEBUG_LAYOUT) Slog.v(TAG, String.format(
"Laying out IN_SCREEN status bar window: (%d,%d - %d,%d)",
pf.left, pf.top, pf.right, pf.bottom));
} else if (attrs.type == TYPE_NAVIGATION_BAR
|| attrs.type == TYPE_NAVIGATION_BAR_PANEL) {
// 这里的导航栏是第二个导航栏,下面mUnrestrictedScreenLeft是除了过扫描区域外的全屏
// The navigation bar has Real Ultimate Power.
pf.left = df.left = of.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = mUnrestrictedScreenLeft
+ mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = mUnrestrictedScreenTop
+ mUnrestrictedScreenHeight;
if (DEBUG_LAYOUT) Slog.v(TAG, String.format(
"Laying out navigation bar window: (%d,%d - %d,%d)",
pf.left, pf.top, pf.right, pf.bottom));
} else if ((attrs.type == TYPE_SECURE_SYSTEM_OVERLAY
|| attrs.type == TYPE_BOOT_PROGRESS
|| attrs.type == TYPE_SCREENSHOT)
&& ((fl & FLAG_FULLSCREEN) != 0)) {
// 全屏下的截屏,系统启动进度条等
// Fullscreen secure system overlays get what they ask for. Screenshot region
// selection overlay should also expand to full screen.
pf.left = df.left = of.left = cf.left = mOverscanScreenLeft;
pf.top = df.top = of.top = cf.top = mOverscanScreenTop;
pf.right = df.right = of.right = cf.right = mOverscanScreenLeft
+ mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mOverscanScreenTop
+ mOverscanScreenHeight;
} else if (attrs.type == TYPE_BOOT_PROGRESS) {
// 启动进度条
// Boot progress screen always covers entire display.
pf.left = df.left = of.left = cf.left = mOverscanScreenLeft;
pf.top = df.top = of.top = cf.top = mOverscanScreenTop;
pf.right = df.right = of.right = cf.right = mOverscanScreenLeft
+ mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mOverscanScreenTop
+ mOverscanScreenHeight;
} else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
// 这个分支是指父窗口会使用过扫描区域的情况下,当前窗口是应用窗口或者子窗口
pf.left = df.left = of.left = cf.left = mOverscanScreenLeft;
pf.top = df.top = of.top = cf.top = mOverscanScreenTop;
pf.right = df.right = of.right = cf.right
= mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom
= mOverscanScreenTop + mOverscanScreenHeight;
} else if (canHideNavigationBar()
&& (sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
&& (attrs.type == TYPE_STATUS_BAR
|| attrs.type == TYPE_TOAST
|| attrs.type == TYPE_DOCK_DIVIDER
|| attrs.type == TYPE_VOICE_INTERACTION_STARTING
|| (attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW))) {
// 可以隐藏导航栏且子窗口可以延伸到导航栏下面的情况下,上面这些类型的窗口
pf.left = df.left = of.left = cf.left = mUnrestrictedScreenLeft;
pf.top = df.top = of.top = cf.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = cf.right = mUnrestrictedScreenLeft
+ mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mUnrestrictedScreenTop
+ mUnrestrictedScreenHeight;
} else if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
// 全屏的情况下
pf.left = df.left = of.left = mRestrictedScreenLeft;
pf.top = df.top = of.top = mRestrictedScreenTop;
pf.right = df.right = of.right = mRestrictedScreenLeft + mRestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = mRestrictedScreenTop
+ mRestrictedScreenHeight;
// 内容区域根据软键盘调整是否打开,如果没打开内容区域就是Dock区域
if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
cf.left = mDockLeft;
cf.top = mDockTop;
cf.right = mDockRight;
cf.bottom = mDockBottom;
} else {
// 打开的调节软键盘的flag,那么内容区域需要可见
cf.left = mContentLeft;
cf.top = mContentTop;
cf.right = mContentRight;
cf.bottom = mContentBottom;
}
} else {
// 非全屏情况下,不包括状态栏和导航栏区域
pf.left = df.left = of.left = cf.left = mRestrictedScreenLeft;
pf.top = df.top = of.top = cf.top = mRestrictedScreenTop;
pf.right = df.right = of.right = cf.right = mRestrictedScreenLeft
+ mRestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mRestrictedScreenTop
+ mRestrictedScreenHeight;
}
// 设置stable区域,根据全屏或者非全屏,会设置stable或者stableFull
applyStableConstraints(sysUiFl, fl, cf);
// 没有打开SOFT_INPUT_ADJUST_NOTHING
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
vf.left = mCurLeft;
vf.top = mCurTop;
vf.right = mCurRight;
vf.bottom = mCurBottom;
} else {
vf.set(cf);
}
} else if (attached != null) {
// 到这里表示不是全屏,即在装饰区内,比如状态栏和导航栏里面,同时是有父窗口的
if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle() +
"): attached to " + attached);
// A child window should be placed inside of the same visible
// frame that its parent had.
// 设置这个窗口的区域
setAttachedWindowFrames(win, fl, adjust, attached, false, pf, df, of, cf, vf);
}
...........
```
这段代码看似很长,其实都是对具体场景的具体处理,我们也没必要一行一行的去分析,我们主要看下有哪些场景,遇到重要的我们进去看一下。首先进入这里的场景同样会是那些需要全屏的或者但是可能状态栏还是显示的情况下,界面会延伸到状态栏或者导航栏下面等这些场景做不同处理。首先还是对状态栏下拉菜单窗口的一些区域进行设置,这里具体过程也不多说,都是大同小异的。之后如果是导航栏窗口,由于也是全屏,所以导航栏的各个区域会设置为mUnrestrictedXXX的区域,也就是包含状态栏和导航栏的区域。之后还会处理像截屏窗口,系统启动时候的进度条窗口等等,都是设置他们的各个区域。如果是应用窗口或者子窗口同时flag设置了FLAG_LAYOUT_IN_OVERSCAN标志,这个标志表示显示区域可以扩展到过扫描区域以内,所以会把各个区域都设置为mOverscanScreenXXX这个过扫描区域以内的部分,除了可视区域会在最后设置。之后如果遇到在隐藏导航栏情况下,状态栏窗口,Toast,分屏分界线,语音交互窗口以及应用窗口或者子窗口的时候,也要重新设置下各个区域。
之后会遇到设置了SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN这个flag,这个flag表示虽然状态栏是显示的,但是窗口会延伸到状态栏下面,对于这种情况,会设置父窗口区域,屏幕区域和过扫描区域为mRestrictedScreenXXX,这里mRestrictedXXX是表示包含状态栏但是不包含导航栏的区域,而内容区域需要根据是否设置了软键盘的SOFT_INPUT_ADJUST_RESIZE值,如果设置了那么内容区域是Dock区域,否则就是mContent区域。最后其余情况下都会设置各个区域为mRestrictedXXX区域。
上面处理了那么多,最后和前面一样,也会调用applyStableConstraints方法根据是否设置的SYSTEM_UI_FLAG_LAYOUT_STABLE这个flag来更新下内容区域,这个flag和方法之前也分析过,是表示是否内容区域不随systemUI变化的值。之后会设置可视区域,如果窗口设置了SOFT_INPUT_ADJUST_NOTHING这个flag,说明没有设置软键盘的显示调整模式,那么可视区域就等于内容区域,否则设置了的话就等于mCurXXX区域,mCurXXX区域前面也说过了,就等于当前窗口的可视区域。到这里和全屏有关的窗口测量设置就完成了。之后开始设置非全屏的情况,首选如果有attach的窗口的话,会调用setAttachedWindowFrames这个方法来计算这个窗口的各个区域大小,这个方法我们前面也分析过了,这里也不多说。
接着下面主要是处理非全屏下的窗口情况,我们继续看代码:
```java
...........
else {
// 到这里表示不是全屏,即在装饰区里面的,同时是个根窗口
// 状态栏下拉菜单或者音量窗口
if (attrs.type == TYPE_STATUS_BAR_PANEL || attrs.type == TYPE_VOLUME_OVERLAY) {
pf.left = df.left = of.left = cf.left = mRestrictedScreenLeft;
pf.top = df.top = of.top = cf.top = mRestrictedScreenTop;
pf.right = df.right = of.right = cf.right = mRestrictedScreenLeft
+ mRestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = cf.bottom = mRestrictedScreenTop
+ mRestrictedScreenHeight;
} else if (attrs.type == TYPE_TOAST || attrs.type == TYPE_SYSTEM_ALERT) {
// toast和系统提示
pf.left = df.left = of.left = cf.left = mStableLeft;
pf.top = df.top = of.top = cf.top = mStableTop;
pf.right = df.right = of.right = cf.right = mStableRight;
pf.bottom = df.bottom = of.bottom = cf.bottom = mStableBottom;
} else {
// 进入这里表示一个正常的非全屏窗口
// 父窗口区域就是内容区域
pf.left = mContentLeft;
pf.top = mContentTop;
pf.right = mContentRight;
pf.bottom = mContentBottom;
if (win.isVoiceInteraction()) { // 语音交互窗口区域
df.left = of.left = cf.left = mVoiceContentLeft;
df.top = of.top = cf.top = mVoiceContentTop;
df.right = of.right = cf.right = mVoiceContentRight;
df.bottom = of.bottom = cf.bottom = mVoiceContentBottom;
} else if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
// 没有打开软键盘调整 ,那么允许被输入法挡住,所以设置Dock区域就行
df.left = of.left = cf.left = mDockLeft;
df.top = of.top = cf.top = mDockTop;
df.right = of.right = cf.right = mDockRight;
df.bottom = of.bottom = cf.bottom = mDockBottom;
} else {
// 到这里说明输入法调整被打开,如果当前是输入法目标窗口的话,该方法开始
// 的时候已经把mContent和mCur都设置在输入法窗口上面了,所以下面会用mContent来设置
df.left = of.left = cf.left = mContentLeft;
df.top = of.top = cf.top = mContentTop;
df.right = of.right = cf.right = mContentRight;
df.bottom = of.bottom = cf.bottom = mContentBottom;
}
// 如果不需要对输入法做什么,那么就设置mCur给vf,否则就用cf设置
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
vf.left = mCurLeft;
vf.top = mCurTop;
vf.right = mCurRight;
vf.bottom = mCurBottom;
} else {
vf.set(cf);
}
}
}
............
```
上面这段方法主要就是处理非全屏下窗口各个区域的测量,和之前的测量一样,这里也分为对不同类型窗口的测量,可以看到这里会对状态栏下拉菜单,音量窗口,Toast,系统警告等做单独的处理,处理过程也就不细说了,有兴趣的同学可以自己自己看下。最后就是对其他的非全屏的窗口进行测量,一般我们遇到的比较多的也就是这种,这种情况下,父窗口会被设置为当前初始化以及导航栏和状态栏测量后的内容区域,其他的区域如果是语言交互窗口的话有他自己的区域会单独设置,剩下的就会根据是否有窗口设置了软键盘调整模式,如果没有设置,那么显示区域,过扫描区域,内容区域会被设置为Dock区域,否则这些区域会被设置为mContent区域。设置了以上这些区域后,最后也就是设置可视区域了,和之前的一样,如果不需要调整输入法显示模式的话,就设置可视区域就等于内容区域,无所谓是否被输入法遮挡,否则就会设置为mCurXXX这个区域,这个区域也就是可视区域。好了,到这里大部分情况下的测量也就完成了,后面还有一些情况需要处理一下,我们继续看后面的代码:
```java
............
// 如果允许窗口显示在屏幕外面同时非系统错误窗口,且非多窗口情况下
if ((fl & FLAG_LAYOUT_NO_LIMITS) != 0 && attrs.type != TYPE_SYSTEM_ERROR
&& !win.isInMultiWindowMode()) {
// 设置屏幕df区域-10000到10000
df.left = df.top = -10000;
df.right = df.bottom = 10000;
// 非壁纸
if (attrs.type != TYPE_WALLPAPER) {
of.left = of.top = cf.left = cf.top = vf.left = vf.top = -10000;
of.right = of.bottom = cf.right = cf.bottom = vf.right = vf.bottom = 10000;
}
}
// 刘海屏设置
// 是否需要使用刘海区域
inal boolean useOutsets = shouldUseOutsets(attrs, fl);
if (isDefaultDisplay && useOutsets) {
osf = mTmpOutsetFrame;
osf.set(cf.left, cf.top, cf.right, cf.bottom);
int outset = ScreenShapeHelper.getWindowOutsetBottomPx(mContext.getResources());
if (outset > 0) {
int rotation = mDisplayRotation;
if (rotation == Surface.ROTATION_0) {
osf.bottom += outset;
} else if (rotation == Surface.ROTATION_90) {
osf.right += outset;
} else if (rotation == Surface.ROTATION_180) {
osf.top -= outset;
} else if (rotation == Surface.ROTATION_270) {
osf.left -= outset;
}
}
}
win.computeFrameLw(pf, df, of, cf, vf, dcf, sf, osf);
if (attrs.type == TYPE_INPUT_METHOD && win.isVisibleLw()
&& !win.getGivenInsetsPendingLw()) {
setLastInputMethodWindowLw(null, null);
// 设置内容区域和可见区域,为后面的窗口
offsetInputMethodWindowLw(win);
}
// 如果是语音交互窗口
if (attrs.type == TYPE_VOICE_INTERACTION && win.isVisibleLw()
&& !win.getGivenInsetsPendingLw()) {
offsetVoiceInputWindowLw(win);
}
```
以上就是最后一部分代码了,我们终于看到了终点,我们来看一下最后这点代码。首先如果窗口flag设置了FLAG_LAYOUT_NO_LIMITS这个值,这个值表明对这个窗口的大小没有显示,甚至可以显示到屏幕外面,所以这里会把所有区域的上下左右设置为从-10000到10000这么大的值,就是允许窗口大小不受限制。但是窗口类型不能是系统错误弹窗或者是多窗口模式,因为系统错误弹出一般是比较严重的错误,需要告知用户,如果显示在了屏幕外就没有意义了,所以这里做了限制。另外多窗口模式一般是分屏的情况,这种情况是比较特殊的显示,我猜测是对多窗口还允许不限制屏幕大小的话会引起很多不必要的bug,而且实际用户也没这么小众的需求,这种没有太大实际意义的处理会造成额外很多问题的产生,所以也不允许在多窗口模式下不限制窗口大小。这里还有一点要注意的是,如果窗口是壁纸窗口,除了屏幕显示区域外,其余的区域不会设置为-10000到10000,因为壁纸也是一个比较特殊的窗口,对于用户来说打开手机第一眼看到了就是屏幕壁纸,所以对这个窗口还是希望保留在正常的显示区域范围内。
处理好了无限制大小的窗口后,接着还会刘海屏的处理,这里首先调用方法shouldUseOutsets来判断是否需要使用刘海区域,如果需要使用到刘海区域的话,会对刘海区域进行处理。这里对刘海区域的处理我们不看细节了,主要看下shouldUseOutsets这个方法,怎么判断是否使用到刘海区域的。
```java
// 是否需要使用刘海区域
private boolean shouldUseOutsets(WindowManager.LayoutParams attrs, int fl) {
// 如果是壁纸,或者flag设置为全屏,或者显示区域会延延伸到过扫描区域
return attrs.type == TYPE_WALLPAPER || (fl & (WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN)) != 0;
}
```
这里判断也很容易,如果是壁纸窗口,或者窗口flag设置了FLAG_FULLSCREEN,即全屏显示,或者窗口参数中设置了FLAG_LAYOUT_IN_OVERSCAN,表示显示区域会延伸到过扫描区域。以上这些情况下,显示区域都会覆盖到刘海区域,所以这些情况下会需要窗口相关区域对刘海进行处理。刘海区域的获取是通过ScreenShapeHelper类的getWindowOutsetBottomPx方法获取的,我们看一下这个方法:
```java
public static int getWindowOutsetBottomPx(Resources resources) {
if (Build.IS_EMULATOR) {
return SystemProperties.getInt(ViewRootImpl.PROPERTY_EMULATOR_WIN_OUTSET_BOTTOM_PX, 0);
} else {
return resources.getInteger(com.android.internal.R.integer.config_windowOutsetBottom);
}
}
```
这个方法我们可以看到如果设备不是模拟器的话,会从资源文件config_windowOutsetBottom属性中获取,所以一般刘海的区域都会从系统配置文件中获取,我们这里了解一下就好,我们也不深入了解刘海,如果是开发这方面的同学,可以深入在研究下,我们回到前面方法。
之后我们看到会调用WindowState的computeFrameLw方法来最终计算整个窗口的大小了,这个方法我们在前面也是分析过了,这里之前计算出来的所有区域会作为参数传入到这个方法中,这里的这些参数主要是给了每个窗口各个区域指定了一个大的范围,这个大范围是保证了不同窗口见能够互不影响的显示给用户,而具体窗口中各个区域显示的内容还需要结合每个窗口所在进程提供的数据最终确定窗口的大小,所以computeFrameLw这个方法中会最终计算出每个代表每个窗口的WindowState对象中保存的窗口大小值,具体大家可以回到前面看看computeFrameLw这个方法,这个方法也是非常重要的一个方法。
接着我们看到还会处理下输入法窗口,一般来说输入法窗口是显示在最上面的窗口,由于这里遍历是从后往前遍历的,所以在正常情况下会首先遍历到输入法窗口,这里遍历到输入法窗口后会调用offsetInputMethodWindowLw方法设置当前整个系统中保存的初始化内容区域mContentXXX,这个方法之前我们已经分析过了会让系统初始化内容区域mContentXXX保持在输入法窗口的上面,这样就像前面方法中遇到的那样,如果其他窗口设置了软键盘调整的flag值SOFT_INPUT_ADJUST_RESIZE,那么那个窗口的内容区域就会被设置为mContentXXX,同时可视区域也会被设置为mCurXXX,这样可以保证窗口显示在输入法窗口上面被用户看到,否则输入法窗口下面的窗口的内容区域会被设置为mDock区域,可视区域也被设置为内容区域一样,这样窗口就会有部分被挡住。所以这里每次遇到输入法窗口后主要就是确保整个系统的mContent区域和mCur可视区域在输入法窗口上面,具体可以看下前面对于offsetInputMethodWindowLw方法的分析。这里除了调用offsetInputMethodWindowLw外,还会调用setLastInputMethodWindowLw方法,这个方法我们看下:
```java
@Override
public void setLastInputMethodWindowLw(WindowState ime, WindowState target) {
mLastInputMethodWindow = ime;
mLastInputMethodTargetWindow = target;
}
```
这里主要是把mLastInputMethodWindow和mLastInputMethodTargetWindow赋值,我们上面分析的流程这里都会被赋予null,表示输入法相关的测量已经处理过了,所以后面窗口不会在进行上面和输入法有关的系统中的mContent和mCur区域的调整了,好了,我们回到前面layoutWindowLw方法。
最后我们看到如果遇到是语言交互窗口的话,还会调用offsetVoiceInputWindowLw这个方法去处理语音交互窗口相关的区域,这个方法我们就不跟进了,有兴趣研究这块的同学可以自己去看下。
至此,整个的窗口测量和布局流程就都完成了,整个流程确实是非常的复杂,这里还仅仅是对窗口的测量,要正确显示还需要对里面的具体的View进行测量,对View模块比较熟悉的同学肯定肯定,除了测量布局外还有绘制,绘制又会根据我们前面分析的Z轴的层序来进行,所以所有的流程都是一部接着一步的,整个Android的显示系统确实设计的非常的复杂,不过我们经过WMS的这么多分析,多少会对这过程中的各个细节有些熟悉。WMS对于Android的显示系统来说,虽然也有对窗口的测量和布局,但是他的主要的职责其实是对窗口的管理,比如窗口间的Z轴层序的设置,对于窗口间,尤其是会有相互影响的不同窗口的各个区域的布局排列,比如输入法窗口和其他窗口,WMS都需要做合理的安排。说了这么多,我们对应WMS的分析也暂时告一段落了,虽然我们零零总总说了这么多,但是里面很多方法还可以进一步跟进去深究,我们暂时就先对WMS部分的分析告一段落了,后面有机会再对更深入的内容来分析。这里文章我们就说到这里,后面的模块分析我们再见。
WMS(六)之窗口的测量和布局