Window添加DecorView和MyView之"商品房和自建房"

101 阅读5分钟

我们用一个 "盖房子" 的故事来讲透这个问题,全程结合 Android 源码的核心逻辑,保证大家能看懂。

故事背景:小区盖房记

把 Android 系统比作一个 "小区",每个 App 都是小区里的一栋 "楼":

  • Window(窗口) :就是楼的 "地皮",规定了这栋楼能占多大地方(比如不能超出小区边界)。
  • View(视图) :就是楼里的 "房间",我们看到的按钮、文字都是不同的房间。
  • ViewRootImpl:相当于 "施工队长",负责按规矩把房间盖在合适的位置。
  • WindowManager:相当于 "小区物业",负责审批地皮使用、监督施工。
  • LayoutParams:相当于 "施工图纸",规定了房间的大小、位置等信息。

一、两种盖房方式:精装套餐 vs 自建房

小区里有两种盖房方式,对应我们的问题场景:

1. Activity 的 DecorView:开发商的 "精装套餐"

你买了一套开发商(系统)的精装房(Activity),不需要自己设计,开发商会给你一个默认的 "整体户型"(DecorView)。

流程细节:

  • 你签购房合同(调用setContentView)时,开发商会默默做两件事:

    1. 给你配一个 "整体户型图"(DecorView),这个户型图的默认图纸(LayoutParams)是 "占满整个地皮"(MATCH_PARENT)。
    2. 找物业(WindowManager)申请地皮(Window),然后派施工队长(ViewRootImpl)来施工。
  • 施工队长(ViewRootImpl)拿到图纸后,会先问物业:"这地皮实际能盖多大?"(计算窗口可用空间)。
    物业会说:"地皮总面积 100 平,但小区规定要留 20 平做公共区域(状态栏、导航栏),实际能用 80 平。"(对应源码中ViewRootImpl计算mWidthmHeight时减去 Insets 的逻辑)。

  • 施工队长再看图纸:"户型要占满可用空间"(LayoutParams.width = MATCH_PARENT),于是算出房间的 "尺寸要求"(MeasureSpec):"宽度必须是 80 平,不能多不能少"(EXACTLY + 80)。
    (源码对应:ViewRootImpl.performTraversals()中通过getRootMeasureSpec(mWidth, lp.width)生成 MeasureSpec)

  • 最后按这个要求盖房(调用DecorView.measure()),房间就正好占满 80 平。

2. WindowManager 直接加 MyView:自己盖 "自建房"

你不想买精装房,想在小区里自己盖个小房子(MyView),流程如下:

流程细节:

  • 你直接找物业(WindowManager)申请地皮,同时提交自己画的图纸(WindowManager.LayoutParams),比如图纸上写着:"宽度 30 平,高度随便(能放下就行)"(width=300dpheight=WRAP_CONTENT)。
  • 物业审批通过后,同样派施工队长(ViewRootImpl)来施工。
  • 施工队长还是先问物业:"地皮可用空间多少?" 得到同样的答案:"80 平"。
  • 再看你的图纸:"宽度 30 平" → 尺寸要求是 "必须 30 平"(EXACTLY + 30);"高度随便" → 尺寸要求是 "最多 80 平,能小就小"(AT_MOST + 80)。
    (源码逻辑和 DecorView 一样,都是getRootMeasureSpec()方法,只是传入的lp.widthlp.height不同)
  • 最后按这个要求盖房(MyView.measure()),宽度固定 30 平,高度根据内容自动调整(但不会超过 80 平)。

二、核心差异:图纸(LayoutParams)不同,结果可能不同

两种方式的施工流程完全一样(都是施工队长主导,先问地皮大小,再按图纸算尺寸要求),但因为 "图纸" 不同,可能导致最终房间大小不同:

  • 精装房的图纸(DecorView 的 LayoutParams)是系统默认的 "占满可用空间",所以房间会铺满地皮剩下的空间。
  • 自建房的图纸(MyView 的 LayoutParams)是你自己定的,可能是固定大小、自适应大小等,所以房间大小由你决定(但不能超过地皮限制)。

三、没有 "上级房间"(父容器),参数从哪来?

不管是精装房的整体户型(DecorView)还是自建房(MyView),它们都是 "最高级别的房间"(顶级 View),没有上级房间(父容器)。那施工队长怎么知道该盖多大?

答案是:两个关键信息源(源码层面的参数来源):

  1. 图纸(WindowManager.LayoutParams)
    无论是系统给 DecorView 默认生成的,还是你给 MyView 手动设置的,这个图纸里写了:

    • 房间要多大(widthheight:是铺满、自适应还是固定值);
    • 房间的类型(type:是普通房间还是特殊房间,比如悬浮窗)。
      (源码中,ViewRootImpl会持有这个LayoutParams,在performTraversals()中作为计算依据)
  2. 地皮实际可用空间(窗口尺寸 - 公共区域)
    施工队长会从物业(WMS,WindowManagerService)拿到地皮的总大小,再减去小区规定的公共区域(状态栏、导航栏等,通过WindowInsets传递),得到实际能盖房的空间。
    (源码中,ViewRootImpl通过mWidthmHeight存储这个可用空间,这两个值在窗口创建或旋转时由 WMS 更新)

四、源码里的 "施工队长工作手册"

施工队长(ViewRootImpl)的核心工作就是在performTraversals()方法里计算尺寸要求,关键代码如下(简化后):

// ViewRootImpl.java
private void performTraversals() {
    // 1. 从物业拿到地皮可用空间(mWidth、mHeight)
    // (实际是通过WMS计算,减去Insets后的尺寸)
    
    // 2. 拿到图纸(LayoutParams)
    WindowManager.LayoutParams lp = mWindowAttributes;
    
    // 3. 计算顶级View的尺寸要求(MeasureSpec)
    int widthSpec = getRootMeasureSpec(mWidth, lp.width);
    int heightSpec = getRootMeasureSpec(mHeight, lp.height);
    
    // 4. 按要求盖房(触发measure)
    mView.measure(widthSpec, heightSpec);
}

// 计算尺寸要求的核心方法
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 图纸要求"铺满" → 必须等于地皮可用空间
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 图纸要求"自适应" → 最大不能超过地皮可用空间
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 图纸要求"固定大小" → 必须等于指定值
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

不管是 DecorView 还是 MyView,最终都会走到这段代码。区别只在于lp.widthlp.height的值不同(一个是系统默认,一个是开发者设置)。

总结:

顶级 View(DecorView 或 MyView)的测量,就像施工队长按 "图纸"(LayoutParams)和 "地皮可用空间"(窗口尺寸 - 公共区域)算出来的盖房要求,流程完全一样,只是图纸内容不同可能导致最终大小不同。没有父容器没关系,因为施工队长(ViewRootImpl)会直接从物业(系统)拿到所有必要的参数。