最近看到一篇對view繪製講的很好的博客,特此也總結一下我對view繪製的理解。
先對上圖做一波簡單的解釋:Activity內部持有一個PhoneWindow,PhoneWindow纔是真正展示用戶界面的大boss。
PhoneWindow這個類是Framework爲我們提供的Android窗口的具體實現,我們熟悉的Dialog也繼承自它。當調用setContentView()方法設置Activity的用戶界面時,實際上就完成了對所關聯的PhoneWindow的ViewTree(也就是ContentView)的設置。
那麼DecorView是啥?DecorView是Window(抽象類,PhoneWindow實現自Window)的最頂層view,它內部是一個LinearLayout包括TitleView和ContentView兩部分。
ContentView實際上是一個FrameLayout,平時我在Activity中setContentView()也就是想佈局add到ContentView上。這裏走進setContentView()源碼發現了平時我們常用的LayoutInflater.inflate(),它就是真正填充佈局的方法。這裏不多解釋,感興趣的童鞋可以看看源碼。
View的繪製:
1.View的繪製實際是由ViewRootImpl負責,那麼ViewRootImpl是啥?
它是的官方註釋是實現視圖和窗口管理器之間所需的協議。
2.那是完成啥協議呢?
這裏最主要完成的就是視圖的繪製以及各種點擊事件。
3.那麼View的繪製什麼時候開始繪製呢?
是setContentView()的時候嗎?答案是NO,setContentView()方法只是將ViewTree添加到DecorView中,此時並沒有開始繪製。DecorView真正的繪製是在activity.handleResumeActivity方法中DecorView被添加到WindowManager時候,也就是調用到windowManager.addView(decorView)。而在windowManager.addView方法中調用到windowManagerGlobal.addView,開始創建初始化ViewRootImpl,再調用到viewRootImpl.setView,最後是調用到viewRootImpl的performTraversals來進行view的繪製(measure,layout,draw),這個時候View才真正被繪製出來。
經過上面的靈魂三問,我們來看看源碼學習View繪製流程,剛剛提到viewRootImpl.setView()方法,那麼我們就從它看起吧:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
performTraversals();
}
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
//這幾個方法最終都會調用
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
mView.draw(canvas);
從setView()方法我們發現了requestLayout(),走入這個方法可以看出主要判斷是否是當前線程,判斷結束最終會走到performTraversals()方法,這是就出現了關鍵的三個方法measure,layout,draw。
measure(int widthMeasureSpec, int heightMeasureSpec)
measure顧名思義就是來測量view的大小,對於一個頁面的繪製流程會從ViewRoot的performTraversals()方法中開始遞歸依次去測量子視圖,viewGroup中的measureChildren()方法就是來去測量子視圖的大小,之後再返回給viewRoot。我們看下面這張圖:
它是一種樹的遍歷過程,它是根據一些父容器對子容器測量規格以及參數去測量子容器的大小,測量完成之後再返回給父容器。
measure方法中重要參數:
一.ViewGroup.LayoutParams
這個參數大家應該都不陌生,使用最多的場景就是在類中動態設置控件的擺放位置。
可以設置三種參數:
1.固定數值,單位px
2.ViewGroup.LayoutParams.MATCH_PARENT ,意思爲寬度和父view相同
3.ViewGroup.LayoutParams.WRAP_CONTENT,意思爲自適應
二.MeasureSpec
MeasureSpec中文解釋是測量規格,它是一個32位的int值,由前兩位的specSize(記錄大小)和後30位的specMode(記錄規格)共同組成。
specMode有三種模式:
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
EXACTLY:大小是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小
AT_MOST:子視圖最多隻能是specSize中指定範圍內的大小,開發人員應該儘可能小得去設置這個視圖,並且保證不會超過specSize。
UNSPECIFIED:開發人員可以設置任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
在measure()方法中會調用onMeasure()方法最終會調用到setMeasuredDimension()方法,這時候才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。
layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()是確定view的擺放位置的方法,它同measure()也是自上而下遞歸遍歷子視圖的擺放位置的。值得注意的是ViewGroup中的onLayout()方法是一個抽象方法,這就意味着所有ViewGroup的子類都必須重寫這個方法。像LinearLayout、RelativeLayout等佈局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行佈局的。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
例如LinearLayout,它重寫onLayout()方法實現自己內部子控件的擺放規則。
onDraw(Canvas canvas)
當measure和layout結束後就真正的進入了視圖繪製的draw方法。這裏主要說一下兩個容易混淆的方法:
一.invalidate()和postInvalidate()
invalidate()它是用於進行刷新重寫繪製重新調用draw(),但是不會調用layout()和measure()。
invalidate方法和postInvalidate方法都是用於進行View的刷新,invalidate方法應用在UI線程中,而postInvalidate方法應用在非UI線程中,用於將線程切換到UI線程,postInvalidate方法最後調用的也是invalidate方法。
invalidate()觸發時機:
1、直接調用invalidate()方法,請求重新draw(),但只會繪製調用者本身。
2、setSelection()方法 :請求重新draw(),但只會繪製調用者本身。
3、setVisibility()方法 : 當View可視狀態在INVISIBLE轉換VISIBLE時,會間接調用invalidate()方法,繼而繪製該View。
4 、setEnabled()方法 : 請求重新draw(),但不會重新繪製任何視圖包括該調用者本身。
二.requstLayout()
requestLayout會直接遞歸調用父窗口的requestLayout,直到ViewRootImpl,然後觸發peformTraversals,由於mLayoutRequested爲true,會導致onMeasure和onLayout被調用。不一定會觸發OnDraw。
requestLayout觸發onDraw可能是因爲在在layout過程中發現l,t,r,b和以前不一樣,那就會觸發一次invalidate,所以觸發了onDraw,也可能是因爲別的原因導致mDirty非空(比如在跑動畫)
到這裏view的繪製就大致小結完了,希望對大家有幫助,也歡迎提問互相學習。
參考資料: