- 文章獨(dú)家授權(quán)公眾號:碼個蛋
- 更多分享:http://www.cherylgood.cn
前言
大家好!本次我們將繼續(xù)學(xué)習(xí)Android之自定義View的死亡三部曲中的第二部:排兵布陣
我們在上一篇Android之自定義View的死亡三部曲之(Measure)中分析了死亡三部曲的第一部,也是三部中最復(fù)雜的一步:View的測量芒划,想知道View的測量相關(guān)知識可以點(diǎn)進(jìn)去查看哦!
通過第一部View的測量衙吩,我們就能拿到View的三圍數(shù)據(jù)了(View的寬高)望抽。
那么接下來我們要做的當(dāng)然就是對測量好的View進(jìn)行布局了。
-
Ok塞耕,說干就干,這次嘴瓤,我們同樣是從ViewRootImpl的performTraversals方法開始扫外,還記得我們的performTraversals方法體內(nèi)部都有哪些內(nèi)容么莉钙?我們再粘貼一下代碼吧。
private void performTraversals() { ... if (!mStopped) { //1筛谚、獲取頂層布局的childWidthMeasureSpec int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); //2磁玉、獲取頂層布局的childHeightMeasureSpec int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //3、測量開始測量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } if (didLayout) { //4驾讲、執(zhí)行布局方法 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... } if (!cancelDraw && !newSurface) { ... //5蚊伞、開始繪制了哦 performDraw(); } } ... }
我們上次分析測量是以performMeasure為入口進(jìn)行分析的,那么本次分析到布局吮铭,當(dāng)然是從performLayout作為起點(diǎn)了时迫。
-
Ok,那么我們就直接看performLayout方法體內(nèi)部的源碼吧
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { Log.v(TAG, "Laying out " + host + " to (" + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); try { //1谓晌、調(diào)用了host.layout host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); mInLayout = false; ..... } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; }
我們可以看到掠拳,在1處,直接調(diào)用了host.layout進(jìn)行布局扎谎,而host是什么東東呢碳想?其實host就是我們的DecorView,還記得我們之前分析View的誕生之謎的時候毁靶,在創(chuàng)建ViewRootImpl時胧奔,直接把DecorView賦值給mView了。
那么也就是說其實是調(diào)用了DecorView的layout方法预吆。我們再看下其傳遞的參數(shù)分別是0龙填,0,host.getMeasuredWidth()拐叉,host.getMeasuredHeight()
而這四個參數(shù)按順利所代碼的含義分別是left岩遗,top,right凤瘦,bottom宿礁,也就是左、上蔬芥、右梆靖、下
left、top當(dāng)然是0了笔诵,為什么呢返吻?難道你想手機(jī)屏幕顯示一個畫面是,左邊和頂部不是剛好貼合的么乎婿?顯然不會希望這樣测僵,簡直丑死啦。
寬就是我們DecorView測量后的寬度谢翎,高就是DecorView測量后的高度
-
Ok捍靠,所有的控件當(dāng)時都是繼承自View了沐旨,那么我們看下View的layout方法
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //1、isLayoutModeOptical(mParent)判斷是傳統(tǒng)模式還是視覺模式剂公,不懂的小伙伴可以百度一下哦 //然后對不同模式分別調(diào)用對象的方法希俩,作用是設(shè)置View的四個點(diǎn) boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //2、直接調(diào)用onLayout方法進(jìn)行布局 onLayout(changed, l, t, r, b); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList listenersCopy = (ArrayList)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { //3纲辽、如果設(shè)置了OnLayoutChangeListener,在layout之后就會回調(diào)告訴你了哦 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
在1中針對不同的layoutMode調(diào)用了不同的方法璃搜,我們來看下一班的layoutMode模式下調(diào)用setFrame方法時拖吼,內(nèi)部做了什么操作呢,
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
//1这吻、如果有一個值發(fā)生了改變吊档,那么就需要重新調(diào)用onLayout方法了,后面會分析到
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
//2唾糯、保存舊的寬和高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
//計算新的寬和高
int newWidth = right - left;
int newHeight = bottom - top;
//3怠硼、判斷寬高是否有分生變化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//Invalidate our old position
//4、如果大小變化了移怯,在已繪制了的情況下就請求重新繪制
invalidate(sizeChanged);
//5香璃、存儲新的值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
//6、大小變化時進(jìn)行處理
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
//7舟误、如果此時View是可見狀態(tài)下葡秒,立即執(zhí)行繪制操作
invalidate(sizeChanged);
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
- 可以看到changed的值只與四個點(diǎn)是否發(fā)生了變化有關(guān)。
- 同時嵌溢,我們還發(fā)現(xiàn)眯牧,如果你想獲得某個view的top、left赖草、right学少、bottom的值,在layout之后就可以拿到了秧骑。
- 而從View.layout方法的2位置處我們知道版确,在執(zhí)行了setFrame之后調(diào)用的是onLayout方法,所以也就是說腿堤,我們可以在onLayout方法中獲得四個位置點(diǎn)的值
- View類的成員變量mLeft阀坏、mRight、mTop和mBottom分別用來描述當(dāng)前視圖的左右上下四條邊與其父視圖的左右上下四條邊的距離笆檀,如果它們的值與參數(shù)left忌堂、right、top和bottom的值不相等酗洒,那么就說明當(dāng)前視圖的大小或者位置發(fā)生變化了士修。這時候View類的成員函數(shù)setFrame就會將參數(shù)left枷遂、right、top和bottom的值分別記錄在成員變量mLeft棋嘲、mRight酒唉、mTop和mBottom中。
- 然后我們很開心的點(diǎn)開了View.onLayout方法沸移,發(fā)現(xiàn)痪伦,居然是空的!~~空的雹锣!
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
} - 沒錯网沾,就是空的,一般該方法是用來確認(rèn)childView的位置的蕊爵,比如FrameLayout會調(diào)用onLayout方法告知childView辉哥,你可以可以開始布局了哦。然后childView就會調(diào)用自身的layout方法完成自身的布局工作攒射,如果childView中還包含有childView醋旦,就會一直調(diào)用下去。
- 我們先來梳理下流程:
1会放、performTraversals內(nèi)部調(diào)用performLayout開始執(zhí)行布局工作
2饲齐、performLayout內(nèi)部會調(diào)用layout開始進(jìn)行布局
3、layout中會調(diào)用setFrame確定mTop鸦概,mLeft箩张,mRight,mBottom的值以及判斷是個點(diǎn)的值是否發(fā)生了變化
4窗市、最后調(diào)用onLayout方法通知下面的childView進(jìn)行布局操作 - ok先慷,那么我們就分析下FrameLayout的onLayout方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
- 從上面可以看到內(nèi)部只是調(diào)用了layoutChildren方法,layoutChildren才是具體的實現(xiàn)
- 我們繼續(xù)看下layoutChildren里面的代碼:
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
//1咨察、獲得子view的熟練
final int count = getChildCount();
//2论熙、獲得父view左面位置,getPaddingLeftWithForeground獲得的是對應(yīng)的內(nèi)邊距
final int parentLeft = getPaddingLeftWithForeground();
//3摄狱、獲得父view右邊位置
final int parentRight = right - left - getPaddingRightWithForeground();
//4脓诡、獲得父view頂部位置
final int parentTop = getPaddingTopWithForeground();
//4、獲得父view底部位置
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
//5媒役、遍歷子view
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//6祝谚、針對不同的水平方向Gravity做處理
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
//6、針對不同的垂直方向Gravity做處理
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//7酣衷、調(diào)用child的layout方法交惯,對child進(jìn)行布局,前面我們分析了
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
- 知識點(diǎn)梳理:
1、獲取父View的內(nèi)邊距padding的值
2席爽、遍歷子View意荤,處理子View的layout_gravity屬性、根據(jù)View測量后的寬和高只锻、父View的padding值玖像、來確定子View的布局參數(shù),
3齐饮、調(diào)用child.layout方法捐寥,對子View進(jìn)行布局
對childView進(jìn)行布局
- 從上面的分析我們的可以知道,如果子view屬于FrameLayout這種布局類的View祖驱,里面就會重復(fù)上面流程上真,如果不是,最終就會調(diào)用到View.onLayout,而這個方法是一個空的實現(xiàn)羹膳,所以我們在自定義View時,需要重新onLayout實現(xiàn)布局的操作
總結(jié):
- 布局流程主要的操作就是確定View的四個點(diǎn)的數(shù)值根竿,相對于之前的測量陵像,是不是要簡單一些呢?