[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树的底层,并减少嵌套。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值