View的繪制流程(二)
每一個視圖的繪制過程都必須經(jīng)歷三個最主要的階段,即onMeasure()、onLayout()和onDraw()
Layout
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
子View的Measure的過程結(jié)束后抹沪,View Tree上的所有View的大小都已經(jīng)確定洋闽。接下來就是Layout的過程。這個過程就是用于給視圖進(jìn)行布局的休涤,也就是確定視圖的位置咱圆。ViewRootImpl的performTraversals()方法會在measure結(jié)束后繼續(xù)執(zhí)行,并調(diào)用View的layout()方法來執(zhí)行此過程功氨,如上述代碼(其中mView就是DecorView序苏,mView.getMeasureWidth/Height()返回的數(shù)值就是Measure過程計算出來的)。
layout的主要作用:根據(jù)子視圖的大小以及布局參數(shù)將View樹放到合適的位置上疑故。
注意:Android中的每個空間都會在界面中占得一塊矩形區(qū)域杠览。我們通過Measure過程計算出來的大小就是這塊矩形的大小。
由于mView就是DecorView纵势,本質(zhì)上就是一個FrameLayout踱阿,所以首先調(diào)用的就是ViewGroup的layout()方法,傳入的參數(shù)都是0钦铁,0软舌,measuredWidth, measuredHeight(位置從左上角開始,將整個DecorView完整顯示牛曹,measuredWidth/Height都是通過ViewRootImpl計算得到的佛点,一般是屏幕大小)
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
這里大致看一下代碼,如果此時對ViewGroup中的子View操作時(增加或者刪除),那么就調(diào)用View.java中的layout()超营;如果此時正在操作鸳玩,那么設(shè)置flag(mLayoutCalledWhileSuppressed)為true,表明需要等操作完成再調(diào)用requestLayout()(重新遍歷View Tree演闭,調(diào)用onMeasure()和onLayout())不跟。
由于這個方法有final修飾詞,所以無法覆寫米碰,所有ViewGroup的子類都會調(diào)用這個方法作為Layout過程的第一步窝革。
public final void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//設(shè)置View位于父視圖的坐標(biāo)軸
boolean changed = setFrame(l, t, r, b);
//判斷View的位置是否發(fā)生過變化,看有必要進(jìn)行重新layout嗎
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
//調(diào)用onLayout(changed, l, t, r, b); 函數(shù)
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
.....
}
當(dāng)ViewGroup中對其子視圖的操作都完成了吕座,調(diào)用View.layout()虐译。這邊理解幾個關(guān)鍵方法:setFrame()和onLayout()。
首先會調(diào)用setFrame()方法來判斷視圖的大小是否發(fā)生過變化吴趴,以確定有沒有必要對當(dāng)前的視圖進(jìn)行重繪漆诽,同時還會在這里把傳遞過來的四個參數(shù)分別賦值給mLeft、mTop史侣、mRight和mBottom這幾個變量拴泌。這幾個值構(gòu)成的矩形區(qū)域就是該View顯示的位置,這里的具體位置都是相對與父視圖的位置惊橱。
以后我們在代碼中調(diào)用View子類的getTop/Right/Bottom/Left()返回的值都是這里設(shè)置的值
接著我們回調(diào)ViewGroup.onlayout()方法蚪腐,但是在View.java中發(fā)現(xiàn)這是一個空方法,一般情況下我們不需要覆寫View.java的該方法税朴。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
對于ViewGroup 來說回季,唯一的差別就是ViewGroup中多了關(guān)鍵字abstract的修飾,要求其子類必須重載onLayout函數(shù)正林。
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
這里我的理解泡一,ViewGroup中的onLayout()方法就是用于確定視圖在布局中的位置,而這個操作應(yīng)該有ViewGroup來完成觅廓,同時不同的ViewGroup子類都用不同的放置自身子View的算法鼻忠。所以自定義ViewGroup時必須實現(xiàn)這個方法。
舉個例子:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
for(int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if(child.getVisibility() != View.GONE) {
child.layout(1, i * mScreenHeight, r, (i + 1) * mScreenHeight)
}
}
}
上述代碼只是簡單的實現(xiàn)杈绸,并沒有什么實際意義帖蔓。首先我們遍歷該View的所有子View,在遍歷的同時瞳脓,我們通過調(diào)用child.layout()來設(shè)置child矩形相對于該View的位置塑娇,即child要顯示的位置。這里傳遞給child.layout()的參數(shù)應(yīng)該結(jié)合布局文件中的設(shè)置的屬性(android:gravity...)和child的Measure過程得到的矩形大小值劫侧,來確定child.layout()的參數(shù)埋酬。
傳入layout()方法的參數(shù)可以自定義哨啃,但是最好能夠?qū)hild的矩形完整顯示出來,也就是按照Measure過程得到的尺寸來確定這四個參數(shù)写妥。當(dāng)然我們可以安全不用顧忌Measure過程計算出來的child矩形大小拳球,自己自定義這四個參數(shù),顯示child矩形的一部分或者大于child的大小耳标。這種情況下醇坝,當(dāng)完成child.onlayout()方法后可以通過getWidth/Height()方法來獲取視圖的寬高度和Measure過程結(jié)束后調(diào)用getMeasureWidth/Height()得到的視圖寬高度不一致(getWidth()返回的值是child的右左邊減去左坐標(biāo),getMeasureWidth()是child.setMeasuredDimension()設(shè)置的)次坡。最好能夠保持它們的一致,是編碼的好習(xí)慣画畅。
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}