search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

你需要知道的Android View的布局

上一篇我們分析Android View的測量。我們先回顧一下,View的測量,在ViewRootImpl#performTraverals方法下,先進行對DecorView根布局測量獲取MeasureSpec,然後開始執行測量performMeasure,通過View#measure找到對應View的核心onMeasure,如果是ViewGroup,先遞歸子View,將父View的MeasureSpec和子View的LayoutParams作為參數而進行測量,然後逐層返回,不斷保存ViewGroup的測量寬高。

好了,我們短短回顧后,回到ViewRootImpl#performTraverals方法:

private void performTraversals { ...if (!mStopped) { int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } if (didLayout) { performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... } if (!cancelDraw && !newSurface) { if (!skipDraw || mReportNextDraw) { if (mPendingTransitions != null && mPendingTransitions.size > 0) { for (int i = 0; i < mPendingTransitions.size; ++i) { mPendingTransitions.get(i).startChangingAnimations; } mPendingTransitions.clear; } performDraw; } } ... }

源碼非常清晰,繼續我們的分析performLayout。Let』s go!

privatevoidperformLayout(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 { host.layout(0, 0, host.getMeasuredWidth, host.getMeasuredHeight); ... } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; }

源碼挺清晰易懂,我們著重看到host.layout,host在上面mView賦值,那就是說host是指向DecorView對象的,方法所帶的參數分別是0,0,host.getMeasuredWidth,host.getMeasuredHeight,分別代表著View的左上右下四個位置。之前發分析所知DecorView是FrameLayout子類,FrameLayout是ViewGroup子類,而我們在ViewGroup#layout方法中看到是用final修飾的,那就是說host.layout調用的就是ViewGroup#layout,我們看一下該方法的源碼:

@Overridepublicfinalvoidlayout(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; } }

我們首先利用變數的命名推測,再結合源碼的註釋來分析,看一下mTransition對象,我們看到是LayoutTransition類的對象,註釋寫著用於處理ViewGroup增加和刪除子視圖的動畫效果,那就是layout方法一開始可能是判斷一些參數來處理動畫的過渡效果的,不影響整體的代碼邏輯,我們可以直接看super.layout(l, t, r, b);,那就是說調用的是View#layout方法,並將左上右下四個參數傳遞過去。

publicclassViewimplements ···{

···

publicvoidlayout

(

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;

boolean

changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//設置相對於父布局的位置//判斷View的位置是否發生過變化,看有沒必要進行重新layoutif

(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo;

if

(li !=

null

&& li.mOnLayoutChangeListeners !=

null

) { ArrayListlistenersCopy = (ArrayList)li.mOnLayoutChangeListeners.clone;

int

numListeners = listenersCopy.size;

for

(

int

i =

0

; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(

this

, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; } }

一開始的判斷,我們從他們全局變數的註釋來理解,說的大概是在測量方法被跳過時,需要在layout前再次調用measure測量方法。接著是isLayoutModeOptical,這裡面的註釋是這個ViewGroup的布局是否在視角範圍里,setOpticalFrame裡面的實現方法經過一些判斷計算,同樣調用回setFrame(l, t, r, b)方法。

protectedbooleansetFrame(int left, int top, int right, int bottom) { boolean changed = false; ··· if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bitint drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); ··· return changed; }

這方法開始我們可以跳過,主要是對mLeft 、mTop、mRight、mBottom賦值,我們稍微看一下方法註釋中對left,top,right,bottom解析是各位置的點,且是相對於父布局的,那就是說現在賦值后可以確定了View自己在父布局的位置了。另外我們在類方法中查詢getLeft等其他三個點,看看他們返回值對用mLeft等對應值的,這個點我們後面再說,我們繼續往下分析。

在setFrame之後我們終於可以看到onLayout,點進去查看View#onLayout方法:

publicclassViewimplements···{ ··· protectedvoidonLayout(boolean changed, int left, int top, int right, int bottom) { } ··· } publicabstractclassViewGroupextendsViewimplements·· ··· @OverrideprotectedabstractvoidonLayout(booleanchanged, intl, intt, intr, intb); ···

從上面源碼我們看到View#onLayout與ViewGroup#onLayout都是實現了一個空方法。但是ViewGroup是一個抽象方法,那就是說繼承ViewGroup的子類必須重寫onLayout方法。因為上篇我們分析View的測量同樣是不同的ViewGroup都有不同的onMeasure,既然測量都不同了,onLayout布局方法就肯定不同了,我們按照上篇的邏輯,依舊對FrameLayout(DecorView)的onLayout來分析:

@OverrideprotectedvoidonLayout(boolean changed, int left, int top, int right, int bottom) { layoutChildren(left, top, right, bottom, false/* no force left gravity */); } void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { finalint count = getChildCount; // parentLeft由父容器的padding和Foreground決定finalint parentLeft = getPaddingLeftWithForeground; finalint parentRight = right - left - getPaddingRightWithForeground; finalint parentTop = getPaddingTopWithForeground; finalint parentBottom = bottom - top - getPaddingBottomWithForeground; for (int i = 0; i < count; i++) { final View child = getChildAt(i); // 不為GONEif (child.getVisibility != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams; //獲取子View的測量寬高finalint width = child.getMeasuredWidth; finalint height = child.getMeasuredHeight; int childLeft; int childTop; int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; } finalint layoutDirection = getLayoutDirection; finalint absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); finalint verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; // 當子View設置水平方向layout_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; } // 當子View設置豎直方向layout_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; } //對子元素進行布局 child.layout(childLeft, childTop, childLeft + width, childTop + height); } } }

FrameLayout#onLayout方法直接調用layoutChildren方法,裡面的實現方法雖然有點長,但是比較好理解,無非加點空間想象力上去就無壓力了。

我們梳理一下:首先是得到父布局的左上右下的Padding值,然後遍歷子布局,通過子View的layout_gravity屬性、子View的LayoutParams屬性、父布局的Padding值來確定子View的左上右下參數,然後調用child.layout方法,把布局流程從父容器傳遞到子元素。

上面我們已經分析過View#layout方法,是一個空方法,主要作用是我們使用的子View重寫該方法,例如TextView、CustomView自定義View等等。不同的View不同的布局方式。大家有興趣可以看看他們的實現過程。

View#getWidth、View#getMeasureWidth

我們在分析View#setFrame分析到這個問題,我們在今篇View的布局可知,在View#setFrame執行里對
mLeft、mRight、mTop、mBottom,從命名方式帶m,我們可以知道這是一個全局變數,在View的布局時賦值的。

而View#getMeasureWidth就要回到我們上一篇View的測量,在View#onMeasure方法中會調用View#setMeasuredDimension方法,在這方式的實現子View設置自身寬高的,這方法里有View#setMeasuredDimensionRaw方法,我們看一下它的源碼:

privatevoidsetMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }

簡單來說就是對mMeasuredWidth與mMeasuredHeight賦值,所以在View#getMeasureWidth方法里返回的值,,是我們進行測量后的值mMeasuredWidth。

他們的值基本情況下是一致的,那麼不一致時什麼時候呢?看回我們本篇中的FrameLayout#onLayout,最後是不是調用了childView#layout方法,FrameLayout我們不可修改,但是在我們CustomView自定義View,重寫onLayout的時候是可以按照我們的特殊要求修改的,例如修改為:childView.layout(0,0,100,100);那麼View#getWidth、View#getMeasureWidth返回的值就會不一致,有興趣的同學可以自己去驗證一下。

所以他們的值在不特殊修改的情況下返回時一樣的,但是他們的意義是完全不同的,一個在測量過程、一個在布局過程。大家要稍微留意。

View的布局流程就已經全部分析完了。我們總結一下:布局流程相對簡單一些,上一篇View的測量,我們可以得到View的寬和高,ViewGroup的layout布局,調用layout方法,確定在父布局的位置,在onLayout方法中遍歷其子View,調用子View的layout方法並根據子View大小、View的LayoutParams值、父View對子 View位置的限制作為參數傳入,完成布局;而View的測量利用測量出來的寬和高來計算齣子View相對於父View的位置參數,完成布局。在下篇,我們將會講述最後一步,View的繪製。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦