Android 中View的绘制机制源代码分析 二

时间:2023-03-09 01:20:46
Android 中View的绘制机制源代码分析 二

尊重原创:http://blog.csdn.net/yuanzeyao/article/details/46842891

本篇文章接着上篇文章的内容来继续讨论View的绘制机制,上篇文章中我们主要解说了View的measure过程。今天我们就来学习ViewGroup的measure过程。因为ViewGroup仅仅是一个抽象类,所以我们须要以一个详细的布局来分析measure过程,正如我上篇文章说的。我打算使用LinearLayout为例解说measure过程,假设你还没有读过上篇文章。那么建议你先浏览一下上篇文章吧:Android中View的绘制机制源代码分析 一

在进行今天的主题之前,我来给大家分享一下我近期看到而且非常喜欢的两句话吧:

1、把生命浪费在美好的事物上

2、应该有一份不以此为生的职业

喜欢第一句话的原因是因为里面包括了一种乐观的生活态度,仅仅要一件事情你在进行的过程中可以给你带来快乐。那么我们就值得花时间做,喜欢第二句话的原因是作为程序猿这个职业以后转型的问题也是值得我们考虑的,相信大家也都听说过程序猿是吃青春饭的职业,当你到35-40岁已经年老色衰的时候。你不得不考虑转型了。有部分转型为管理人才,有些人全然转型,干着和IT毫无关系的职业,所以我们是不是如今就要想想我们有没有一份不以此为生的职业呢?好吧  扯淡就扯到这里吧,以下我们步入正题。

我们来分析今天的第一个问题:你对layout_weight属性知多少?

相信大多数同学会说这个属性就是标明一个View在父View中所占的权重(比例)。这个解释对吗?我们暂且不做评论。我们使用两个样例来验证一下:

example 1:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="2"
android:text="New Text"
android:background="#998877"
android:id="@+id/textView"
/> <TextView
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="4"
android:text="New Text"
android:background="#334455"
android:id="@+id/textView2"
/>
</LinearLayout>

效果图例如以下:

Android 中View的绘制机制源代码分析 二

example 2:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="2"
android:text="New Text"
android:background="#998877"
android:id="@+id/textView"
/> <TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="4"
android:text="New Text"
android:background="#334455"
android:id="@+id/textView2"
/>
</LinearLayout>

效果图例如以下:

Android 中View的绘制机制源代码分析 二

在第一张图片中,上面的TextView的weidht是2,以下的TextView的weight是4。所以上面的TextView的高度是以下TextView高度的一半,注意此时两个TextView的layout_height都是0dip。再看以下的一张图,相同上面的TextView和以下TextView的weight各自是2和4,唯一不同的是它们的layout_height变为了match_parent,此时上面的高度确实以下的两倍

所以从第一张图片看来。layout_weight好像是代表比例的,可是从第二张图片看。刚好是相反的。我们今天就带着这个疑问開始分析LinearLayout的measure源代码吧

LinearLayout的measuer调用的是View中的measure方法,从上篇文章中我们知道measure会调用onMeasure方法,所以直接从LinearLayout的onMeasure開始分析:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}

看了源代码是不是认为so easy!,在onMeasure主要依据当前的LinearLayout是横向还是纵向。分别调用measureVertical方法和measureHorizontal方法,这里我们以纵向为例,看看measureVertical代码。因为代码比較长。我们分段分析:

Section one:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//用来存储全部的子View使用的高度
mTotalLength = 0;
int maxWidth = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
//全部View的weight的和
float totalWeight = 0;
//获得子View的个数
final int count = getVirtualChildCount();
//widthMeasureSpec和heightMeasureSpec就是父View传递进来的,这里拿到父View的mode
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

这里定义了几个重要的变量,mTotalLength,用来存储全部子View的高度,count存在子View的个数,widthMode和heightMode用来存储父View的mode(假设对于mode不熟,我以看我前面的一篇文章)。

Section Two:

	//遍历全部的子View,获取全部子View的总高度,并对每一个子View进行measure操作
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i); if (child == null) {
//假设child 是Null,则mTotalLength加0
mTotalLength += measureNullChild(i);
continue;
} if (child.getVisibility() == View.GONE) {
//假设child不可见,则跳过
i += getChildrenSkipCount(child, i);
continue;
}
//拿到child的LayoutParams
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//将weight的值加到totalWeight,weight的值就是xml文件里的layout_weight属性的值
totalWeight += lp.weight; if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
/**
假设父View的mode是EXACTLY,而且height==0 而且lp.weight>0(就是我们上面的样例中的第一张图的情况)
那么就先不measure这个child,直接把topMargin和bottoMargin等属性加到totaoLength中
*/
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
//假设父View不是EXACLTY,那么将子View的height变为WRAP_CONTENT
if (lp.height == 0 && lp.weight > 0) {
// heightMode 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 height of 0
oldHeight = 0;
lp.height = 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, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0); if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
} final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}

这段代码是整个measureVertical最核心的部分了,我已经在代码中增加了对应的凝视。这里我仅仅想说的是为什么child.getLayoutParam可以直接强制转换为LinearLayout.LayoutParams。这个问题我们先保留吧。我打算后面的文章中专门分析一下这个LayoutParams这个对象。

我们发如今measureVertical中调用了一个measureChildBeforeLayout方法。我们先看看它传入的几个參数,我们发现最后一个參数听奇怪的,totalWeight==0?mTotalLength:0。也就是说对于一个View,假设这个View之前的View没有设置过layout_weight属性。那么这个參数等于mTotalLength,假设有设置过,那么传0,我们先进入measureChildBeforeLayout方法看看:

   void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}

事实上就是调用父类ViewGroup的measureChildWidthMargins方法,这种方法我们在前篇文章已经分析过了,这里我们就不分析了,它就是对子View进行measure方法,仅仅只是我们这里须要注意,假设前面有View设置了layout_weight属性,那么这里的totalHeight就是0,在运行完了measureChildBeforeLayout方法后,child的高度就知道了,就将child的高度累加到mTotalHeight中。

Section Three:

//将全部View的高度赋值给heightSize;
int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); //这里对heightSize再次赋值,只是假设LinearLayout是xml文件的根标签。而且设置到Activity的话
//此时heightSize的大小就是屏幕的高度。我们临时就考虑等于屏幕高度的情况,其它情况相似
heightSize = resolveSize(heightSize, heightMeasureSpec); //屏幕的高度还剩下delta。假设对于我们上面第一张图,delta>0,对于第二张图则<0
int delta = heightSize - mTotalLength;
if (delta != 0 && totalWeight > 0.0f) {
//假设设置了weightsum属性。这weightSum等于weightsum的属性。否则等于totalWeight
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0;
//又一次遍历全部的子View
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//假设子View不可见,直接跳过
if (child.getVisibility() == View.GONE) {
continue;
} LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); float childExtra = lp.weight;
//假设设置了weight属性
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
//从delta中分到(weight/weightSum)*delta,注意这里delta可能<0
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share; final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width); // TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
/**
记得heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0吗
这个是Section Two的一个推断条件,也就是说假设走到这里,说明这个View前面已经measure过
如今要将share的值增加到高度上。所以要又一次measure */
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
} child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
/**
因为走到Section Two中走到heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0时,是直接跳过的
所以没有測量过。所以在这里对View进行測量
*/
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
} final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth); boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT; alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth); allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
} // Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
} if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
} maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//全部的孩子View測量完成。为自己设置大小
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);

这段代码主要是来又一次绘制设置有layout_weight属性的子View。首先计算LinearLayout可以提供的高度大小heightSize,正如凝视里面说的,在上面的两个样例中,heightSize都是屏幕的高度,然后通过heightSize和mTotalLenght计算还剩下的高度。然后将这些高度依照weight的比例分配给对应的View。然后调用View的measure方法。我们如今来解释上面的两个样例吧:

第一个样例:两个TextView的高度都是0dip。layout_weight各自是2 和 4,LinearLayout的mode=EXACTLY

从Section Two開始,条件满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 所以在SectionTwo运行完后两个TextView是没有运行measure的,所以mTotalLenght等于0。

进入Section Three,此时heightSize等于屏幕的高度,所以delta=heightSize-mTotalLenght=屏幕高度。

weightSum=2+4=6.在遍历子View的时候,通过计算第一个TextView的高度是:屏幕高度*(2/6),而且delta=delta-屏幕高度*(2/6).weightSum=6-2=4.

因为第一个TextView不满足条件(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY),所以运行else里面的逻辑:

child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));

所以第一个TextView的高度就是屏幕的1/3. 

遍历完第一个TextView之后,遍历第二个TextView。相同的道理第二个 TextView的高度等于delta*(4/4),也就是等于delta的值,事实上也就是 屏幕高度*(4/6)。

第二个样例:两个TextView的高度都是match_parent,layout_weight各自是2和4 ,LinearLayout的mode=EXACTLY

从Section Two開始,条件不满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ,所以运行到了else里面的逻辑

measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);

此时totalWeight明显不等0,所以measureChildBeforeLayout最后一个參数明显是0,所以导致第一个View的高度绘制出来及时heightMeasureSpec的size。也就是屏幕的高度(原因见我上篇文章的分析)。

相同的道理对于第二个TextView測量后高度也是整个屏幕的高度。所以导致这里算出的delta=(-屏幕的高度),也就是说是个负数,进入Section Three,非常明显满足了(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)这个条件,所以运行例如以下代码:

int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
} child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

通过Section Two的分析child.getMeasureHeight等于屏幕高度,share=-屏幕高度*(2/6),也就是说第一个TextView的高度变为 屏幕的高度*(4/6),相同的道理可以得出第二个TextView的高度 屏幕的高度*(2/6)。

对于layout_weight属性的理解应该是这种:在SectionTwo中測量全然部的View后,将delta的值依照weight的比例给对应的 View。假设delta>0,那么那么就是在原来大小上加上对应的值,否则就是减去对应的值。

最后调用setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize) 设置自身的大小。

相信到这里你应该已经对LinearLayout的測量过程有了非常深刻的理解了吧,假设还有认为描写叙述不清楚的地方,欢迎留言讨论...