View核心原理
核心心法:一棵树的"生长"三部曲
想象一下,整个 Android 界面就是一棵 View 树,而 DecorView 是树根。这棵树的生长,是从 ViewRootImpl 这个"园丁"那里开始的。
ViewRootImpl 里的 performTraversals 方法,就是园丁的"工作指令"。它一执行,就会严格按照 measure、layout、draw 的顺序,驱动整棵树的生长。
这个顺序是单向的、严格的。measure 决定了每个节点"想占多大",layout 决定了每个节点"最终被放在哪",draw 就是"把它画出来"。
第一关:MeasureSpec —— 父子间的"尺寸谈判"
measure 过程的核心就是 MeasureSpec。它不是一个简单的数字,而是一个 32 位的 int,封装了模式和大小。这背后是系统为了减少对象创建、提升性能的精心设计。
理解 MeasureSpec 的关键,在于理解它是一场父子容器间的"尺寸谈判":
- 谈判的筹码:父容器的
MeasureSpec(它的空间限制) + 子 View 的LayoutParams(它想要的大小)。 - 谈判的结果:一个决定了子 View 最终可以是多少的
MeasureSpec。
系统通过 ViewGroup 的 getChildMeasureSpec 方法,用一套非常精密的规则来进行这场谈判。下面这张"谈判规则表"是理解一切的核心:
子 View 的 LayoutParams |
父容器 MeasureSpec 是 EXACTLY (精确) | 父容器 MeasureSpec 是 AT_MOST (最大) |
|---|---|---|
| 固定值 (ex: 100dp) | 子 View 为 EXACTLY,大小就是 100dp |
子 View 为 EXACTLY,大小还是 100dp |
match_parent |
子 View 为 EXACTLY,大小是父容器剩余空间 |
子 View 为 AT_MOST,最大不超过父容器剩余空间 |
wrap_content |
子 View 为 AT_MOST,最大不超过父容器剩余空间 |
子 View 为 AT_MOST,最大不超过父容器剩余空间 |
拷问:为什么自定义 View 要重写 onMeasure 处理 wrap_content?
从表中可以看到,当 wrap_content 时,子 View 的模式永远是 AT_MOST,而其大小 SpecSize 就是父容器剩余空间。如果直接继承 View 但不重写 onMeasure,系统默认的 getDefaultSize 方法会直接返回这个 SpecSize。这就导致 wrap_content 的效果和 match_parent 一模一样,填满了整个父容器。所以,你必须在 onMeasure 中,对 AT_MOST 模式做一个特殊处理,给它一个默认的内部大小。
第二关:Measure 的递归 —— 整棵树的"自顶向下"测量
measure 是整棵树从根节点 DecorView 开始,一层层递归向下的过程。
- 顶层触发:
ViewRootImpl.performMeasure调用View.measure。 - 核心方法:
measure方法是final的,你不能重写,它内部会回调onMeasure。这是模板方法模式的经典应用。 - 分摊任务:
ViewGroup没有重写onMeasure,但它提供了measureChildren、measureChildWithMargins这样的方法,遍历所有子 View,并把自己的MeasureSpec和子 View 的LayoutParams作为参数,计算出子 View 的MeasureSpec,然后调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)。这样,测量流程就传递下去了。 - 到达叶子:直到递归到叶子节点(一个普通的
View),它的onMeasure会使用传入的MeasureSpec计算并设置自己的测量宽高,然后递归结束,逐层返回。
特别注意:测量宽高(getMeasuredWidth/Height)和最终宽高(getWidth/Height)在绝大多数情况下是相等的,但赋值时机不同。测量宽高在 measure 阶段确定,最终宽高在 layout 阶段通过 setFrame 确定。在 onLayout 中获取是最可靠的。
第三关:Layout —— 父亲的"位置分配"
layout 过程相对简单,是一个自顶向下的"位置分配"过程。
View.layout方法被调用,它用传入的四个顶点坐标(l, t, r, b)调用setFrame来确定自己的位置。getWidth()就是r - l,getHeight()就是b - t。- 然后,
layout方法内部会调用onLayout。对于ViewGroup,onLayout是抽象方法,你必须实现它,在这个方法里,循环遍历所有子 View,调用child.layout(childLeft, childTop, childRight, childBottom)为每个子 View 分配具体的屏幕位置。这样,layout 流程就传递下去了。
第四关:Draw —— 一块画布的"接力绘制"
draw 过程的传递和上两个不太一样,它是在 draw 方法内部通过 dispatchDraw 实现的。
View.draw 方法的绘制顺序是严格定义的,就像一个绘画清单:
- drawBackground:绘制背景。
- onDraw:绘制自身内容。这是自定义 View 时最常重写的方法。
- dispatchDraw:绘制子 View。
ViewGroup重写了此方法,会遍历调用子 View 的draw方法,完成绘制接力。View的这个方法是空的。 - onDrawForeground:绘制前景(如滚动条、装饰)。
性能优化点:如果一个 ViewGroup 本身不需要绘制任何内容(比如纯布局),我们可以通过 setWillNotDraw(true) 来告诉系统,这样系统会跳过 onDraw 步骤,直接去 dispatchDraw,从而提升性能。但如果你的 ViewGroup 需要绘制背景或分割线,你必须手动将它设为 false。
第五关:解决"拿不到宽高"的终极理解
为什么在 onCreate、onResume 里拿到的宽高是 0?因为这里是同步的生命周期回调,而 View 的 measure/layout 是异步的,它们还没有被执行。
你提到的四种解决方案,本质都是将获取宽高这个动作,延迟到 View 已经完成测量和布局之后:
-
onWindowFocusChanged:当 Activity 获得焦点时,View 肯定已经绘制完毕。 -
view.post(Runnable):利用消息队列机制,将任务放到消息队列末尾,当它执行时,前面的 measure/layout 消息已经处理完了。 -
ViewTreeObserver.OnGlobalLayoutListener:注册一个监听器,当 View 树布局状态发生变化时回调,此时必然已测量完毕。 - 手动
view.measure:强行触发测量。但这需要你根据 View 的LayoutParams手动构造出合适的MeasureSpec,是一种比较"硬核"且不推荐的方法,因为容易产生不一致。
理论说得再多,不如直接上代码。我带你手写两个例子,把核心知识点全用代码串起来:一个自定义 View(处理 wrap_content),一个自定义 ViewGroup(流布局,展示 measure 和 layout 的完整流程)。
第六关、自定义 View:一个带圆角的标签
这个例子重点解决第四章反复强调的坑:直接继承 View 时,必须手动处理 wrap_content,否则效果和 match_parent 一样。
public class RoundTagView extends View {
private Paint mPaint;
private int mColor;
private String mText;
private float mTextSize;
private Rect mTextBounds; // 用于测量文字宽高
public RoundTagView(Context context) {
this(context, null);
}
public RoundTagView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mColor = Color.parseColor("#FF6B6B");
mText = "标签";
mTextSize = sp2px(14);
mTextBounds = new Rect();
}
/**
* 核心!处理 wrap_content 必须重写 onMeasure
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 1. 先计算文字实际占用的宽高
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
int textWidth = mTextBounds.width();
int textHeight = mTextBounds.height();
// 2. 加上内边距,得到控件真正需要的"理想尺寸"
int paddingHorizontal = getPaddingLeft() + getPaddingRight() + dp2px(16);
int paddingVertical = getPaddingTop() + getPaddingBottom() + dp2px(8);
int desiredWidth = textWidth + paddingHorizontal;
int desiredHeight = textHeight + paddingVertical;
// 3. 根据不同的测量模式,计算最终尺寸
int finalWidth, finalHeight;
if (widthMode == MeasureSpec.EXACTLY) {
// match_parent 或 固定值,直接使用父容器给的尺寸
finalWidth = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
// wrap_content:取理想尺寸和父容器最大可用尺寸中的较小值
finalWidth = Math.min(desiredWidth, widthSize);
} else {
// UNSPECIFIED:直接用理想尺寸
finalWidth = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
finalHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
finalHeight = Math.min(desiredHeight, heightSize);
} else {
finalHeight = desiredHeight;
}
// 4. 必须调用 setMeasuredDimension 保存测量结果
setMeasuredDimension(finalWidth, finalHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// 绘制圆角背景
mPaint.setColor(mColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRoundRect(0, 0, width, height, dp2px(20), dp2px(20), mPaint);
// 绘制文字居中
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(mTextSize);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float textX = (width - mTextBounds.width()) / 2f;
float textY = height / 2f - (fontMetrics.ascent + fontMetrics.descent) / 2f;
canvas.drawText(mText, textX, textY, mPaint);
}
// 提供外部设置文字的方法
public void setTagText(String text) {
this.mText = text;
requestLayout(); // 文字变了要重新测量
invalidate(); // 并重新绘制
}
private int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, getResources().getDisplayMetrics());
}
private float sp2px(float sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, getResources().getDisplayMetrics());
}
}
代码要点解析:
onMeasure中分三步走:先算理想尺寸 → 再判断模式 → 最后setMeasuredDimensionAT_MOST时取Math.min(desired, parentSize),这正是让wrap_content生效的关键- 改变内容后调用
requestLayout()+invalidate()重新走一遍流程
第七关、自定义 ViewGroup:流布局(FlowLayout)
这个例子展示 ViewGroup 最核心的职责:在 onMeasure 中测量所有子 View,在 onLayout 中为它们分配位置。
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 测量阶段:遍历所有子 View,计算流式布局需要的总宽高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 先测量所有子 View(这一步必须做,否则子 View 的 measuredWidth 是 0)
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int lineWidth = 0; // 当前行已占用的宽度
int lineHeight = 0; // 当前行的高度
int totalHeight = 0; // 所有行的总高度
int totalWidth = 0; // 所有行中的最大宽度
int childCount = getChildCount();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// 可用的最大宽度(去掉 padding)
int availableWidth = widthSize - paddingLeft - paddingRight;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + childWidth > availableWidth) {
// 放不下了,换行
totalWidth = Math.max(totalWidth, lineWidth);
totalHeight += lineHeight;
// 重置新行
lineWidth = childWidth;
lineHeight = childHeight;
} else {
// 当前行还能放,继续往后排
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 如果是最后一个子 View,别忘了把这行的高度加上
if (i == childCount - 1) {
totalWidth = Math.max(totalWidth, lineWidth);
totalHeight += lineHeight;
}
}
// 加上 padding
totalWidth += paddingLeft + paddingRight;
totalHeight += paddingTop + paddingBottom;
// 根据测量模式确定最终尺寸
int finalWidth = (widthMode == MeasureSpec.EXACTLY) ? widthSize : totalWidth;
int finalHeight = (heightMode == MeasureSpec.EXACTLY) ? heightSize : totalHeight;
setMeasuredDimension(finalWidth, finalHeight);
}
/**
* 布局阶段:给每个子 View 分配屏幕上的实际位置
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int availableWidth = getWidth() - paddingLeft - getPaddingRight();
int lineWidth = 0;
int lineHeight = 0;
int topOffset = paddingTop; // 当前行的 top 坐标
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + childWidth > availableWidth) {
// 换行:更新 topOffset,重置 lineWidth
topOffset += lineHeight;
lineWidth = 0;
lineHeight = 0;
}
// 计算子 View 的四个坐标
int childLeft = paddingLeft + lineWidth + lp.leftMargin;
int childTop = topOffset + lp.topMargin;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
}
/**
* 必须重写,让子 View 的 margin 生效
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
}
代码要点解析:
measureChildren是ViewGroup提供的方法,内部会为每个子 View 生成MeasureSpec并调用child.measure(),这是测量传递的关键onMeasure中做的是"换行逻辑":累加宽度直到放不下就换行,最后算出总宽高onLayout中必须手动调用child.layout(l, t, r, b)确定每个子 View 的位置,否则子 View 不会显示- 重写
generateLayoutParams为MarginLayoutParams,这样在 XML 中设置的margin才会生效
第八关、在 XML 中使用
<!-- 流布局示例 -->
<com.example.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android"
android:background="#E8F5E9"
android:padding="8dp"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View的工作原理"
android:background="#FFF3E0"
android:padding="8dp"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自定义View"
android:background="#E3F2FD"
android:padding="8dp"
android:layout_margin="4dp"/>
<!-- 更多标签... -->
</com.example.FlowLayout>
更多推荐

所有评论(0)