Android View体系是构建用户界面的基石,内容庞杂但逻辑清晰。为了帮你建立起完整的知识框架,我将从架构全景、核心职责、三大工作流程、事件分发、自定义实践五个维度进行拆解。
一、架构全景:从Activity到ViewRoot
要理解View体系,首先需要厘清它在大框架中的位置。Activity不是视图,而是视图的控制层;真正绘制和处理事件的是View。
1.1 核心组件关系
每个Activity内部持有一个Window对象(唯一实现是PhoneWindow),PhoneWindow中包含了顶层视图DecorView。当我们调用setContentView时,实际是将自定义布局添加到DecorView的android.R.id.content区域。
层级链:
Activity → Window → DecorView → ContentParent → Your Layout
1.2 ViewRoot的角色
ViewRoot不是View,而是Handler的实现类,它建立了DecorView与窗口系统Server端的桥梁。绘制的总入口是ViewRootImpl.performTraversals(),该方法依次触发measure、layout、draw三大流程。
关键认知: View树是单线程的,所有UI操作必须在主线程执行。跨线程刷新需使用
postInvalidate()。
二、View体系核心组成
2.1 View与ViewGroup的本质区别
| 维度 | View | ViewGroup |
|---|---|---|
| 继承 | 直接继承Object | 继承自View |
| 职能 | 绘制自己、处理事件 | 容器、管理子View |
| 绘制 | 重写onDraw() |
不重写onDraw(),重写dispatchDraw() |
| 布局 | onLayout()无操作 |
onLayout()为abstract,必须实现 |
| 子类 | TextView, ImageView | LinearLayout, RecyclerView |
关键代码印证:
ViewGroup实现了ViewManager接口,因此具备addView、removeView能力;同时实现ViewParent接口,负责焦点、滚动等控制。
2.2 树形结构(组合模式)
Android采用组合模式设计视图层级:
- 叶节点:普通View(不可包含子View)
- 树枝节点:ViewGroup(可包含子ViewGroup或View)
这种设计使得绘制和事件分发的递归操作成为可能------从根节点开始深度遍历。
三、三大工作流程(必考/必会)
整个绘制流程由ViewRootImpl.performTraversals()驱动,依次调用performMeasure → performLayout → performDraw。
3.1 Measure:测量尺寸
入口方法: measure(int widthMeasureSpec, int heightMeasureSpec)(final,不可重写)。
核心重写: onMeasure(int widthMeasureSpec, int heightMeasureSpec)。
3.1.1 MeasureSpec 解密
MeasureSpec是一个64位压缩值 (实际代码中为32位int):高2位模式 + 低30位尺寸,通过位运算提高性能。
| 模式 | 取值 | 含义 |
|---|---|---|
| EXACTLY | 2 bits 11 | 精确值(match_parent / 100dp),父View强制指定 |
| AT_MOST | 2 bits 10 | 最大值(wrap_content),不能超过SpecSize |
| UNSPECIFIED | 2 bits 00 | 无限制(ScrollView嵌套、RecyclerView),想多大就多大 |
子View的MeasureSpec生成规则:
由父View的MeasureSpec + 子View的LayoutParams共同决定。例如:父View是EXACTLY,子View是wrap_content → 子View变为AT_MOST,size=父View可用剩余空间。
3.1.2 自定义View测量要点
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
// wrap_content:需要自己计算一个默认大小
setMeasuredDimension(mDefaultWidth, mDefaultHeight);
} else {
// EXACTLY 直接使用
setMeasuredDimension(widthSize, heightSize);
}
}
必须调用setMeasuredDimension(),否则会报错。
3.2 Layout:分配位置
入口方法: layout(int l, int t, int r, int b)(final)。
核心重写: onLayout(boolean changed, int left, int top, int right, int bottom)。
3.2.1 位置参数
View的位置由相对于父View的左上角坐标定义:
getLeft()= 左边缘距父容器左侧距离getTop()= 上边缘距父容器顶部距离getRight()=getLeft() + getWidth()getBottom()=getTop() + getHeight()
坑点提醒:
getWidth()与getMeasuredWidth()可能不同。前者是layout后最终尺寸,后者是measure阶段测量尺寸。理论上layout不应改变尺寸,但开发者可以强行改。
3.2.2 ViewGroup必须实现onLayout
java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(x, y, x + child.getMeasuredWidth(), y + child.getMeasuredHeight());
x += child.getMeasuredWidth(); // 水平排列示例
}
}
若不遍历调用child.layout(),子View将不可见。
3.3 Draw:绘制内容
入口方法: draw(Canvas canvas)(final,定义绘制骨架)。
核心重写: onDraw(Canvas canvas)(View绘制自身)、dispatchDraw(Canvas canvas)(ViewGroup绘制子View)。
3.3.1 draw方法内部执行顺序
- 绘制背景(
drawBackground()) - 绘制自身(调用
onDraw()) - 绘制子View(调用
dispatchDraw())→ 内部drawChild()→ 调用子View的draw() - 绘制渐变框(边缘效果)
- 绘制滚动条
重要规律:
ViewGroup通常不重写onDraw (即使重写也可能不显示,需设置setWillNotDraw(true)优化),而重写dispatchDraw可干涉子View绘制顺序或添加装饰。
四、事件分发机制(交互核心)
虽然搜索结果中仅部分提及,但这是View体系的关键闭环。
4.1 三大方法
dispatchTouchEvent(MotionEvent ev):事件分发入口,决定是否传递。onInterceptTouchEvent(MotionEvent ev):拦截判断(仅ViewGroup有)。onTouchEvent(MotionEvent ev):事件处理。
4.2 传递顺序(Activity→Window→ViewGroup→View)
伪代码逻辑:
java
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev); // 拦截了,自己处理
} else {
for (child in children) {
if (child.dispatchTouchEvent(ev)) {
consume = true;
break; // 子View消费了,终止传递
}
}
}
return consume;
}
经典结论:
- 事件由外向内传递,再由内向外回溯(责任链模式)。
onTouchListener优先级高于onTouchEvent。OnClickListener在onTouchEvent的ACTION_UP触发。
五、自定义View实践
5.1 分类与选择策略
| 场景 | 继承方式 | 必须重写的方法 |
|---|---|---|
| 全新绘制 | View | onMeasure() + onDraw() |
| 组合控件 | ViewGroup | onMeasure() + onLayout() |
| 扩展控件 | TextView/Button等 | onDraw() 或 仅增加功能 |
| 特殊布局 | ViewGroup | onMeasure() + onLayout() |
5.2 自定义属性流程
- res/values/attrs.xml 定义
<declare-styleable> - 构造函数中
context.obtainStyledAttributes()获取TypedArray - 解析属性值,最后
recycle()
5.3 构造函数解析
java
// 代码new时调用
public MyView(Context context) { this(context, null); }
// XML解析时调用
public MyView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }
// style指定时调用
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { ... }
// API 21+ 样式资源指定
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { ... }
必须实现前两个,后两个按需。
六、性能优化与常见陷阱
6.1 刷新机制对比
| 方法 | 触发场景 | 后果 |
|---|---|---|
invalidate() |
外观不变(文字/颜色) | 仅重绘draw流程 |
postInvalidate() |
非UI线程调用 | 同上,Handler切线程 |
requestLayout() |
尺寸/位置变化 | measure+layout+draw |
forceLayout() |
标识需要重新measure | 下次layout生效 |
原理: invalidate会从当前View向上标记直到ViewRoot,最终触发performTraversals(),但仅执行draw阶段。
6.2 常见误区
- 误以为View有margin:View只有padding,margin是ViewGroup.LayoutParams的属性。
- 直接调用measure() :严禁开发者主动调用measure,应使用
requestLayout由系统触发。 - setTranslationX与setX混淆 :
setX设置绝对坐标,setTranslationX设置相对于原始位置的偏移,即X = left + translationX。 - findViewById性能:从根开始DFS遍历树,避免在循环中调用。
总结
Android View体系可归纳为一树、三流、一分发:
- 一棵树:组合模式构建的视图树
- 三大流程:measure(量)、layout(位)、draw(绘)
- 一套分发:事件自上而下、自下而上的责任链