一、视图状态(View States)
1. 五种核心状态
状态 | 作用 | 修改方法 | 特点 |
---|---|---|---|
enabled |
视图是否响应交互 | setEnabled(boolean) |
禁用状态下不响应onTouch事件 |
focused |
视图是否获得焦点 | requestFocus() |
需同时满足focusable和focusableInTouchMode |
window_focused |
视图所在窗口是否在前台 | 系统自动维护 | 应用无法直接修改 |
selected |
视图是否被选中 | setSelected(boolean) |
同一界面允许多个视图同时选中 |
pressed |
视图是否被按下 | setPressed(boolean) |
通常由系统自动设置点击状态 |
2. 状态变更响应流程

关键源码解析:
-
状态变更入口
java// View.java protected void drawableStateChanged() { Drawable d = mBackground; if (d != null && d.isStateful()) { d.setState(getDrawableState()); // 传递新状态给Drawable } }
-
状态匹配原理
java// StateListDrawable.java protected boolean onStateChange(int[] stateSet) { int idx = findStateIndex(stateSet); // 匹配selector中对应的item return selectDrawable(idx); // 切换Drawable }
-
触发重绘
java// StateListDrawable.java public boolean selectDrawable(int idx) { // ...更新Drawable invalidateSelf(); // 关键重绘触发 }
二、重绘机制(View Invalidation)
1. 两种重绘方式对比
方法 | 触发流程 | 应用场景 | 性能影响 |
---|---|---|---|
invalidate() |
仅重走draw() 流程 |
内容变化但尺寸不变(如文字/颜色) | 低开销,局部刷新 |
requestLayout() |
完整measure-layout-draw |
视图尺寸/结构变化(如添加子View) | 高开销,全局重新布局 |
2. invalidate() 核心流程

源码关键路径:
java
// ViewRootImpl.java
void scheduleTraversals() {
sendEmptyMessage(DO_TRAVERSAL); // 发送异步消息
}
public void handleMessage(Message msg) {
if (msg.what == DO_TRAVERSAL) {
performTraversals(); // 最终入口
}
}
private void performTraversals() {
// 根据标记位决定流程
if (!mLayoutRequested) {
// 仅执行draw流程
performDraw();
}
}
3. 性能优化要点
-
减少重绘范围
java// 只刷新局部区域 public void invalidate(Rect dirty) { // 计算脏区域并传递 }
-
避免过度重绘
-
使用
View.setWillNotDraw(true)
跳过无内容视图 -
合并状态变更(避免连续多次invalidate)
-
三、常见问题
Q1:按下按钮时背景图切换的完整流程?
A:
-
状态变更 :
View.setPressed(true)
更新状态数组 -
通知Drawable :
drawableStateChanged()
调用StateListDrawable.setState()
-
匹配资源 :
StateListDrawable.onStateChange()
查找对应状态图片 -
触发重绘 :
selectDrawable()
→invalidateSelf()
→View.invalidate()
-
绘制执行 :递归至
ViewRootImpl.scheduleTraversals()
→ 下一帧触发draw()
流程
Q2:invalidate() 和 requestLayout() 的本质区别?
A:
-
invalidate():
-
仅设置
DIRTY
标记 → 触发draw()
流程 -
不重新测量/布局 → 适用于内容变化但尺寸不变场景
-
-
requestLayout():
-
设置
FORCE_LAYOUT
标记 → 触发完整measure-layout-draw
-
向父视图递归 → 可能引发全局重新布局
-
Q3:为什么StateListDrawable能自动切换图片?
A :核心机制是状态匹配+重绘触发:
-
在
res/drawable
中定义<selector>
状态映射 -
onStateChange()
用状态数组匹配最佳item下标 -
selectDrawable()
切换当前Drawable并调用invalidateSelf()
Q4:自定义View如何优化重绘性能?
A:三级优化策略:
-
减少区域:
java// 只刷新变化区域 invalidate(dirtyRect);
-
避免过度绘制:
-
覆写
hasOverlappingRendering()
返回false -
使用
canvas.clipRect()
限制绘制区域
-
-
复用资源:
-
预初始化Paint/Path等对象
-
使用
View.setLayerType(LAYER_TYPE_HARDWARE)
启用硬件加速
-
Q5:解释下 scheduleTraversals()
中发送异步消息的意义?是否在主线程执行?
A :
核心是通过异步消息+同步屏障确保UI更新的及时性:
-
异步消息 :
DO_TRAVERSAL
消息被标记为异步类型,优先于普通消息处理 -
同步屏障:
-
在消息队列插入屏障,阻塞后续同步消息
-
仅允许异步的UI更新消息通过
-
-
主线程执行:
-
消息最终由
ViewRootImpl
的Handler
在主线程处理 -
调用
performTraversals()
执行完整的视图树遍历
-
-
设计目的:
-
解决UI更新被业务消息阻塞的问题
-
保证16ms内完成绘制(60Hz刷新率)
-
使用代码证明主线程执行 :
在 performTraversals()
中可检查线程:
java
void performTraversals() {
if (Thread.currentThread() != mThread) {
throw new RuntimeException("Must be on UI thread!");
}
// ...measure/layout/draw...
}
其中 mThread
即 ViewRootImpl
创建时的主线程。
Q6:View.postInvalidate() 和 invalidate() 区别?
A:
维度 | invalidate() |
postInvalidate() |
---|---|---|
调用线程 | 仅UI线程 | 任意线程 |
内部实现 | 直接操作视图树 | 通过Handler转发到UI线程 |
适用场景 | 视图内部状态变更 | 后台线程触发的UI更新 |
四、总结
Q:请解释Android视图状态变更如何触发界面更新?
A :
整个过程分为四个关键阶段:
-
状态变更
-
调用
setPressed()
/setSelected()
等方法改变视图状态 -
更新视图内部的
mDrawableState
状态数组
-
-
Drawable响应
-
触发
drawableStateChanged()
回调 -
StateListDrawable
通过onStateChange()
匹配新状态对应的Drawable资源
-
-
重绘调度
-
调用
invalidateSelf()
→ 触发View.invalidate()
-
通过
ViewParent
链递归至ViewRootImpl
-
通过
scheduleTraversals()
异步调度重绘
-
-
绘制执行
-
下一帧触发
performTraversals()
-
根据标记位仅执行draw流程(measure/layout跳过)
-
调用
View.draw()
→Drawable.draw()
渲染新状态对应的图片
-
性能优化要点:
-
优先使用
invalidate(Rect)
局部刷新 -
复杂动画启用硬件加速(
LAYER_TYPE_HARDWARE
) -
避免在
draw()
中创建对象