一、視圖狀態(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()
中創建對象