[Android開發]LinearLayout與RelativeLayout異同深入探討

Android初級工程師或者校招的面試過程中,很容易被問到LinearLayout與RelativeLayout區別,這是一個基礎問題,由此可以引出例如ViewGroup和View繪製流程等問題,可以看出應聘者的掌握程度。

一般可以這麼回答回答:

LinearLayout爲線性佈局:

1.分爲垂直佈局(vertical)和水平佈局(horizontal)兩種(android:orientation屬性)

2.可以通過設置控件的android:layout_weight屬性以控制各個控件在佈局中的相對大小,線性佈局會根據該控件layout_weight值與其所處佈局中所有控件layout_weight值之和的比值爲該控件分配佔用的區域。如果layout_weight指爲0,控件會按原大小顯示,不會被拉伸;對於其餘layout_weight屬性值大於0的控件,系統將會減去layout_weight屬性值爲0的控件的寬度或者高度,再用剩餘的寬度或高度按相應的比例來分配每一個控件顯示的寬度或高度。

3.android:baselineAligned,默認爲true,即基準線對其。另外還有android:baselineAlignedChildIndex用於指定以哪個child爲基準。


RelativeLayout爲相對佈局:

1.允許子元素指定它們相對於其父元素或兄弟元素的位置,例如android:layout_centerHrizontal、android:layout_below、android:layout_marginBottom等屬性。


吧啦吧啦回答這麼多已經接近及格線了,但是在這個不裝X就遭雷劈的年代,這麼就結束肯定不會讓面試官滿意

首先,我們應該先利用這個問題秀一下標準的美式口語:

A RelativeLayout is a very powerful utility for designing a user interface because it can eliminate nested view groups and keep your layout hierarchy flat, which improves performance. If you find yourself using several nested LinearLayout groups, you may be able to replace them with a single RelativeLayout

這可是google爹說的,不會錯~

其實就是說Relativelayout性能更好,更靈活。因爲使用LinearLayout 容易產生多層嵌套的佈局結構,而因爲Relativelayout的靈活性的優點,可以降低佈局的嵌套層級,優化性能,因此當嵌套多時推薦使用RelativeLayout。


秀完口語面試官已經被你的口語震的一愣一愣的了,此時應該趁熱打鐵,用深沉的語氣對其進行一段分(che)析(dan):


RelativeLayout與LinearLayout都繼承於ViewGroup,而ViewGroup實現了android.view.ViewParent和android.view.ViewManager接口,賦予了其裝載控件和管理子控件的能力。例如ViewParent中的requestLayout()與ViewManager中的addView(View view, ViewGroup.LayoutParams params)


ViewGroup的作用是組織和管理它的子View,即對子View進行佈局,讓子View繪製自身並對它們的大小、邊距進行約束等。ViewGroup管理View的基本過程是onMeasure()->onLayout(),RelativeLayout與LinearLayout對子View繪製與佈局的區別就大部分就在這兩個函數中


首先是LinearLayout:


LinearLayout的onMeasure:

onMeasure的作用是遍歷所有子View,對其大小進行測量。

LinearLayout會根據mOrientation來分別調用measureVertical或者measureHorizontal。以水平佈局爲例,在LinearLayout中的measureHorizontal函數中,有幾個關鍵變量:

1.全局變量mTotalLength:用於統計所有View的寬度和;

2.widthSize:LinearLayout的寬度,初始值爲mTotalLength;

3.totalWeight:所有View的weight和;

4.mWeightSum:對應android:weightSum即weight之和最大值;


總體來說:遍歷所有View,跳過爲null的和屬性爲View.GONE的(View.GONE與View.INVISIBLE的區別),加上分割線寬度mDividerWidth與左右margin,計算所有View的childWidth之和mTotalLength,統計所有View的weight和totalWeight,並且對子view進行測量。

具體探討這個過程:首先如果父LinearLayout的屬性爲MeasureSpec.EXACTLY且子View的寬度爲0,weight大於0,則當前無法計算子控件的寬度,mTotalLength加上左右Margin暫時不統計子View的寬度,若android:baselineAligned爲true,對子View進行第一次measure,size爲0,false則置skippedMeasure爲true,然後等待遍歷完畢之後過程進行重新測量分配:

(代碼一)

<span style="font-size:12px;">	// Optimization: don't bother measuring children who are going to use
	// leftover space. These views will get measured again down below if
	// there is any leftover space.
	if (isExactly) {
	    mTotalLength += lp.leftMargin + lp.rightMargin;
	} else {//這條分支不會走
	    final int totalLength = mTotalLength;
	    mTotalLength = Math.max(totalLength, totalLength +
	            lp.leftMargin + lp.rightMargin);
	}
	
	// Baseline alignment requires to measure widgets to obtain the
	// baseline offset (in particular for TextViews). The following
	// defeats the optimization mentioned above. Allow the child to
	// use as much space as it wants because we can shrink things
	// later (and re-measure).
	if (baselineAligned) {
	    final int freeSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
	    child.measure(freeSpec, freeSpec);
	} else {
	    skippedMeasure = true;
	}</span>


否則(比如LinearLayout是WRAP_CONTEN或者子View寬度不爲0),根據前面所有view的總寬度與總weight測量該View,並獲取childWidth,統計到mTotalLength當中

(代碼二)

<span style="font-size:12px;">	int oldWidth = Integer.MIN_VALUE;

	if (lp.width == 0 && lp.weight > 0) {//如果滿足這個條件,讓佈局儘量的小
	// widthMode is either UNSPECIFIED or AT_MOST, and this
	// child
	// wanted to stretch to fill available space. Translate that to
	// WRAP_CONTENT so that it does not end up with a width of 0
		oldWidth = 0;
		lp.width = LayoutParams.WRAP_CONTENT;
	}

	// Determine how big this child would like to be. If this or
	// previous children have given a weight, then we allow it to
	// use all available space (and we will shrink things later
	// if needed).
	measureChildBeforeLayout(child, i, widthMeasureSpec,
			totalWeight == 0 ? mTotalLength : 0,
			heightMeasureSpec, 0);

	if (oldWidth != Integer.MIN_VALUE) {
		lp.width = oldWidth;
	}

	final int childWidth = child.getMeasuredWidth();
	if (isExactly) {
		mTotalLength += childWidth + lp.leftMargin + lp.rightMargin +
				getNextLocationOffset(child);
	} else {
		final int totalLength = mTotalLength;
		mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin +
			   lp.rightMargin + getNextLocationOffset(child));
	}

	if (useLargestChild) {//若android:measureWithLargestChild爲true
		largestChildWidth = Math.max(childWidth, largestChildWidth);
	}</span>

在遍歷過程中,還會涉及到:

1.android:baselineAligned,在遍歷過程中通過maxAscent[]與maxDescent[]兩個數組控制/計算;

2.android:measureWithLargestChild(遍歷過程中統計最寬的子View,記住最寬的寬度,若該變量爲true且LinearLayout的寬度爲指定值(WRAP_CONTENT對應模式爲AT_MOST)或未指定值UNSPECIFIED時,則重新統計寬度,每個子View的寬度爲最大子View的寬度,可見不能輕易使用這個屬性,會一定程度影響性能;

3.paddingLeft/Right的影響。

統計所有子View寬度完畢後,初始widthSize爲mTotalLength,並與suggested minimum (測量background寬度)比較,取最大值。然後調用resolveSizeAndState(int size, int measureSpec, int childMeasuredState)獲得合適的尺寸。

<span style="font-size:12px;">	int widthSize = mTotalLength;
	
	// Check against our minimum width
	widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
	
	// Reconcile our calculated size with the widthMeasureSpec
	int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
	widthSize = widthSizeAndState & MEASURED_SIZE_MASK;</span>

接下來會對layout_weight進行處理:

用widthSize - mTotalLength獲取差delta,若delta不爲0且存在子View的weight屬性大於0的情況則按照weight分配空間,權重大的分配的多,小的分配的少

(代碼三)

<span style="font-size:12px;">	if (childExtra > 0) {
		// Child said it could absorb extra space -- give him his share
		int share = (int) (childExtra * delta / weightSum);
		weightSum -= childExtra;
		delta -= share;

		final int childHeightMeasureSpec = getChildMeasureSpec(
				heightMeasureSpec,
				mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
				lp.height);

		// TODO: Use a field like lp.isMeasured to figure out if this
		// child has been previously measured
		if ((lp.width != 0) || (widthMode != MeasureSpec.EXACTLY)) {
			// child was measured once already above ... base new measurement
			// on stored values
			int childWidth = child.getMeasuredWidth() + share;
			if (childWidth < 0) {
				childWidth = 0;
			}

			child.measure(
				MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
				childHeightMeasureSpec);
		} else {
			// child was skipped in the loop above. Measure for this first time here
			child.measure(MeasureSpec.makeMeasureSpec(
					share > 0 ? share : 0, MeasureSpec.EXACTLY),
					childHeightMeasureSpec);
		}

		// Child may now not fit in horizontal dimension.
		childState = combineMeasuredStates(childState,
				child.getMeasuredState() & MEASURED_STATE_MASK);
	}</span>


光看代碼暈的不行,還是從例子中分析吧

例如最簡單的3個View採用橫向佈局如下:

<span style="font-size:12px;">	<LinearLayout 
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:orientation="horizontal">
	    <View 
	        android:layout_weight="1"
	      	android:background="@android:color/holo_blue_bright"
	        android:layout_height="50sp"
	        android:layout_width="0dp"/>
	    <View 
	        android:layout_weight="1"
	      	android:background="@android:color/holo_green_light"
	        android:layout_height="40sp"
	        android:layout_width="0dp"/>
	    <View 
	        android:layout_weight="1"
	      	android:background="@android:color/holo_red_light"
	        android:layout_height="30sp"
	        android:layout_width="0dp"/>
	</LinearLayout></span>

效果圖:



時,代碼會走代碼一與三,在1中,因爲margin與width均爲0,所以mTotalLength一直不增加,到了代碼三時,delta不爲0且totalWeight大於0,進入if分支。對剩餘空間進行分配,同時通過child.measure將分配空的空間交給child負責(1263行)。

如果我們將第三個View的width從0dp改爲其他非0,不改動weight,則會進入代碼一中的else分支,調用measureChildBeforeLayout(child, i, widthMeasureSpec,totalWeight == 0 ? mTotalLength : 0,heightMeasureSpec, 0);,因爲totalWeight !=0,所以此時分配的寬度爲android:layout_width中寬度,mTotalLength加上該長度。進入代碼三後,因爲第三個View仍有權重,值爲1,佈局仍會對剩餘的空間進行分配,所以,如果width寬度小於一定值,仍會被分配爲3個一樣的寬度,若大於一定值,則會超過平均長度,前兩者的長度根據剩餘長度調整。如果此時將weight去掉,則設置多少就是多少長度。

另一種情況,若將寬度均改成match_parent,當weight均爲1時,效果不變,當weight改成121時,則效果如下:


發現第二個View消失了?將第二個View的weight改爲小於其他兩個View權重和,則又出現了。

從源碼分析,當xml中設置如下時,delta爲負,所以通過int share = (int) (childExtra * delta / weightSum)根據權重計算分配空間時,權重越大的分配的越多,因爲分配的是負寬度,所以相應變小,因此,第二個View則消失了

可見,weight並不是單純是根據權重來平均分配空間,正確的解釋應該是:(再次秀英語)

Indicates how much of the extra space in the LinearLayout will be allocated to the view associated with these LayoutParams. Specify 0 if the view should not be stretched. Otherwise the extra pixels will be pro-rated among all views whose weight is greater than 0.
即設置後的控件的寬度爲原控件寬度加上剩餘寬度(可能爲負)的佔比。

從上面可以看出,在設置weight的情況下,LinearLayout對所有的子View進行了兩次measure,而不設置weight則進行1次measure。因此,我們需要儘量避免層級的嵌套,來減少measure的調用。而LinearLayout因爲容易造成層級的疊加,因此,能在一個RelativeLayout中繪製的,儘量不適用LinearLayout。


onMeasure完成之後是onLayout

相比於onMeasure,onLayout中的內容則簡單的多,遍歷所有不爲View.Gone的非null的View,根據Gravity和baselineAligned計算位置,調用每個子View的layout方法即可,這裏暫且不再贅述。


接下來是RelativeLayout

與LinearLayout相同,RelativeLayout同樣繼承於ViewGroup,同樣需要經過onMeasure與onLayout。


首先是onMeasure:

當第一次執行onMeasure或者執行requestLayout後,需要調用sortChildren方法,根據添加順序對所有的子View進行排序,橫着一次(mSortedHorizontalChildren),豎着一次(mSortedVerticalChildren),然後對兩個序列進行檢查,通過依賴圖靜態類DependencyGraph中的getSortedViews方法根據依賴關係進行排序。

之後在onMeasure中,對子View進行遍歷,即對兩個序列進行分別遍歷,首先是mSortedHorizontalChildren,

第一步獲取RelativeLayout.LayoutParams,RelativeLayout.LayoutParams是RelativeLayout的內部類,用數組mRules保存着相對佈局的id,大小VERB_COUNT爲22,不同的位置代表不同的屬性,例如mRules[0]爲leftOf。

之後是3個方法:

<span style="font-size:12px;">	for (int i = 0; i < count; i++) {
		View child = views[i];
		if (child.getVisibility() != GONE) {
			LayoutParams params = (LayoutParams) child.getLayoutParams();
			int[] rules = params.getRules(layoutDirection);

			applyHorizontalSizeRules(params, myWidth, rules);
			measureChildHorizontal(child, params, myWidth, myHeight);

			if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
				offsetHorizontalAxis = true;
			}
		}
	}</span>

1.applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules)

該方法用於根據依賴的控件的LayoutParams以及其LEFT_OF、RIGHT_OF等屬性計算該控件的橫向位置及mLeft與mRight

2.measureChildHorizontal(View child, LayoutParams params, int myWidth, int myHeight)

該方法用於橫向測量子View,此時的高度只是暫時值。

3.positionChildHorizontal(View child, LayoutParams params, int myWidth,boolean wrapContent)

該方法用於橫向擺放子View,並且如果父RelativeLayout的寬度是WRAP_CONTENT,會在此時對寬高進行修正。


橫向完畢後,會再次對垂直排列的View序列進行上述操作,步驟大致相同,在此次對子View進行measure時就會正確的測量。

之後的操作就是對父RelativeLayout的寬高等屬性進行再次修正


接着就是onLayout:

遍歷child,根據onMeasure中的計算結果依次layout即可

<span style="font-size:12px;">    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }</span>

經過一通分(che)析(dan),可以看出在使用weight情況下LinearLayout與RelativeLayout的效率差別並不大,在某些情況下LinearLayout可能優於RelativeLayout。所以關鍵在於我們的設計,在界面設計過程中應該儘量減少層級,減少measure操作,儘量將RelativeLayout和LinearLayout置於View樹的底層,並減少嵌套。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章