一、视图状态(View States)
1. 五种核心状态
状态 | 作用 | 修改方法 | 特点 |
---|---|---|---|
enabled | 视图是否响应交互 | setEnabled(boolean) | 禁用状态下不响应onTouch事件 |
focused | 视图是否获得焦点 | requestFocus() | 需同时满足focusable和focusableInTouchMode |
window_focused | 视图所在窗口是否在前台 | 系统自动维护 | 应用无法直接修改 |
selected | 视图是否被选中 | setSelected(boolean) | 同一界面允许多个视图同时选中 |
pressed | 视图是否被按下 | setPressed(boolean) | 通常由系统自动设置点击状态 |
2. 状态变更响应流程
关键源码解析:
状态变更入口
// View.java protected void drawableStateChanged() {Drawable d = mBackground;if (d != null && d.isStateful()) {d.setState(getDrawableState()); // 传递新状态给Drawable} }
状态匹配原理
// StateListDrawable.java protected boolean onStateChange(int[] stateSet) {int idx = findStateIndex(stateSet); // 匹配selector中对应的itemreturn selectDrawable(idx); // 切换Drawable }
触发重绘
// StateListDrawable.java public boolean selectDrawable(int idx) {// ...更新DrawableinvalidateSelf(); // 关键重绘触发 }
二、重绘机制(View Invalidation)
1. 两种重绘方式对比
方法 | 触发流程 | 应用场景 | 性能影响 |
---|---|---|---|
invalidate() | 仅重走draw() 流程 | 内容变化但尺寸不变(如文字/颜色) | 低开销,局部刷新 |
requestLayout() | 完整measure-layout-draw | 视图尺寸/结构变化(如添加子View) | 高开销,全局重新布局 |
2. invalidate() 核心流程
源码关键路径:
// 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. 性能优化要点
减少重绘范围
// 只刷新局部区域 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:三级优化策略:
减少区域:
// 只刷新变化区域 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()
中可检查线程:
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()
中创建对象