想象你是一个装修队长(ViewRootImpl),要测量整栋房子(View 树)的尺寸。房子由多个房间(ViewGroup)和家具(View)组成,每个房间还可能包含小房间和家具。你需要带着神奇的卷尺(MeasureSpec)完成任务!
📐 核心角色介绍
-
装修队长:
ViewRootImpl(测量发起者) -
神奇卷尺:
MeasureSpec(32位整数 = 2位模式 + 30位尺寸)EXACTLY(精确模式):业主指定确切尺寸AT_MOST(最大模式):尺寸不能超过某个值UNSPECIFIED(随意模式):想多大就多大
-
房间:
ViewGroup(如LinearLayout) -
家具:
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树测量全景图
💡 关键设计思想
-
责任链模式:测量请求从根节点层层下发
-
尺寸协商原则:
- 父View用
MeasureSpec传递约束条件 - 子View通过
setMeasuredDimension()回传结果
- 父View用
-
性能优化:
- 避免重复测量(
measure()调用可能触发多次) - 使用
measureCache缓存测量结果(Android 9+)
- 避免重复测量(
� 常见踩坑点
-
测量循环:当子View尺寸依赖父View尺寸时,可能需要多次测量
java
// 自定义ViewGroup中可能需要 protected void onMeasure(...) { measureChildren(...); // 第一轮测量 calculateSelfSize(); // 计算自身尺寸 remeasureCertainChild(...);// 根据结果重新测量特定子View } -
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实践下吧~