🌳 趣味故事:Android View 树的「量房大作战」🏗️

145 阅读3分钟

想象你是一个装修队长(ViewRootImpl),要测量整栋房子(View 树)的尺寸。房子由多个房间(ViewGroup)和家具(View)组成,每个房间还可能包含小房间和家具。你需要带着神奇的卷尺(MeasureSpec)完成任务!


📐 核心角色介绍

  1. 装修队长ViewRootImpl(测量发起者)

  2. 神奇卷尺MeasureSpec(32位整数 = 2位模式 + 30位尺寸)

    • EXACTLY(精确模式):业主指定确切尺寸
    • AT_MOST(最大模式):尺寸不能超过某个值
    • UNSPECIFIED(随意模式):想多大就多大
  3. 房间ViewGroup(如 LinearLayout

  4. 家具View(如 TextView


📏 测量四部曲(源码级流程)

🚀 Step 1:队长发起测量

java

// ViewRootImpl.java
public void performTraversals() {
    int widthMeasureSpec = getRootMeasureSpec(width, lp.width); // 业主给的卷尺要求
    int heightMeasureSpec = getRootMeasureSpec(height, lp.height);
    
    mView.measure(widthMeasureSpec, heightMeasureSpec); // 从顶层View开始测量
}

🔁 Step 2:房间测量自己和孩子(ViewGroup流程)

java

// ViewGroup.java (以FrameLayout为例)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    // 1. 测量每个孩子(递归开始)
    for (View child : children) {
        if (child.isGone()) continue; // 跳过被藏起的家具
        
        // 关键魔法:根据房间尺寸和孩子需求生成新卷尺
        int childWidthSpec = getChildMeasureSpec(..., child.getLayoutParams().width);
        int childHeightSpec = getChildMeasureSpec(..., child.getLayoutParams().height);
        
        child.measure(childWidthSpec, childHeightSpec); // 把卷尺交给孩子
    }
    
    // 2. 根据所有孩子的尺寸计算房间大小
    int totalWidth = calculateSize(children); // 考虑padding/margin等
    int totalHeight = calculateSize(children);
    
    // 3. 保存自己的尺寸
    setMeasuredDimension(resolveSize(totalWidth, widthMeasureSpec), 
                         resolveSize(totalHeight, heightMeasureSpec));
}

🔍 魔法卷尺生成器(核心算法)

java

// ViewGroup.java
public static int getChildMeasureSpec(int parentSpec, int padding, int childDimension) {
    switch (childDimension) {
        case LayoutParams.MATCH_PARENT: // 孩子想和房间一样大
            if (parentMode == MeasureSpec.EXACTLY) {
                return MeasureSpec.makeMeasureSpec(parentSize, EXACTLY); // 精确继承
            } else {
                return MeasureSpec.makeMeasureSpec(parentSize, AT_MOST); // 最多这么大
            }
            
        case LayoutParams.WRAP_CONTENT: // 孩子自己决定尺寸
            return MeasureSpec.makeMeasureSpec(parentSize, AT_MOST); // 但不能超过房间
            
        default: // 孩子指定了具体数值(如100dp)
            return MeasureSpec.makeMeasureSpec(childDimension, EXACTLY); // 按孩子要求
    }
}

🛋️ Step 3:家具自我测量(View流程)

java

// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 解析卷尺要求
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
    // 2. 计算理想尺寸(考虑背景图/文字内容等)
    int desiredWidth = calculateDesiredSize(); 
    
    // 3. 根据卷尺模式确定最终尺寸
    int finalWidth;
    switch (widthMode) {
        case MeasureSpec.EXACTLY: // 必须用卷尺尺寸
            finalWidth = widthSize;
            break;
        case MeasureSpec.AT_MOST: // 不能超过卷尺尺寸
            finalWidth = Math.min(desiredWidth, widthSize);
            break;
        case MeasureSpec.UNSPECIFIED: // 自由发挥
            finalWidth = desiredWidth;
            break;
    }
    
    // 4. 保存测量结果(高度同理)
    setMeasuredDimension(finalWidth, finalHeight);
}

🌲 View树测量全景图

deepseek_mermaid_20250630_af3a77.png

💡 关键设计思想

  1. 责任链模式:测量请求从根节点层层下发

  2. 尺寸协商原则

    • 父View用MeasureSpec传递约束条件
    • 子View通过setMeasuredDimension()回传结果
  3. 性能优化

    • 避免重复测量(measure()调用可能触发多次)
    • 使用measureCache缓存测量结果(Android 9+)

� 常见踩坑点

  1. 测量循环:当子View尺寸依赖父View尺寸时,可能需要多次测量

    java

    // 自定义ViewGroup中可能需要
    protected void onMeasure(...) {
        measureChildren(...);      // 第一轮测量
        calculateSelfSize();       // 计算自身尺寸
        remeasureCertainChild(...);// 根据结果重新测量特定子View
    }
    
  2. wrap_content失效:自定义View未处理AT_MOST模式

    java

    // 错误示范(只考虑EXACTLY模式)
    protected void onMeasure(...) {
        setMeasuredDimension(100, 100); // 永远返回固定值
    }
    
    // 正确做法
    protected void onMeasure(...) {
        int contentWidth = calculateContentWidth();
        int finalWidth = resolveSize(contentWidth, widthMeasureSpec);
        // ...同理处理高度
    }
    

🚀 总结:测量过程本质是尺寸协商

卷尺(MeasureSpec)  在View树中自上而下传递约束,
测量结果(measuredDimension)  自下而上汇总,
每个View都是聪明的谈判专家,
业主要求自身需求间找到平衡点!🤝

通过这个趣味比喻,结合源码关键流程,相信你已经理解Android View树的测量机制啦!试着写个自定义View实践下吧~