【进阶android】ListView源码分析——布局三大方法
视图从初始化到完全展示到屏幕之上,这段时间里,还有许多工作要做;总体而言,这些工作可用分为三大步骤;而这三大步骤便是View类的三大布局方法onMeasure、onLayout以及onDraw,三个方法分别表示对视图进行测量、布局及绘制。
ListView是一个视图,当然也会重写这三个主要的方法;同时,这三个方法也完成了ListView在展示到屏幕之前,所需要完成的绝大多数初始化工作。
一、测量
首先,我们先直接看看ListView的onMeasure()的源码:
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Sets up mListPadding
- //设置mListPadding,并确定是否需要强制滚动到底部
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- int childWidth = 0;
- int childHeight = 0;
- int childState = 0;//UNSPECIFIED模式,子视图的测量模式
- mItemCount = mAdapter == null ? 0 : mAdapter.getCount();//更新mItemCount
- if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
- heightMode == MeasureSpec.UNSPECIFIED)) {//只由子视图自身决定大小
- final View child = obtainView(0, mIsScrap);
- measureScrapChild(child, 0, widthMeasureSpec);//测量子视图
- childWidth = child.getMeasuredWidth();
- childHeight = child.getMeasuredHeight();
- childState = combineMeasuredStates(childState, child.getMeasuredState());
- if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
- ((LayoutParams) child.getLayoutParams()).viewType)) {
- mRecycler.addScrapView(child, -1);
- }
- }
- if (widthMode == MeasureSpec.UNSPECIFIED) {//完全由子视图决定宽度
- widthSize = mListPadding.left + mListPadding.right + childWidth +
- getVerticalScrollbarWidth();
- } else {
- widthSize |= (childState&MEASURED_STATE_MASK);//将子视图的测量模式合成到ListView的测量模式之中
- }
- if (heightMode == MeasureSpec.UNSPECIFIED) {
- heightSize = mListPadding.top + mListPadding.bottom + childHeight +
- getVerticalFadingEdgeLength() * 2;
- }
- if (heightMode == MeasureSpec.AT_MOST) {
- // TODO: after first layout we should maybe start at the first visible position, not 0
- heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);//测量子视图的高度
- }
- setMeasuredDimension(widthSize , heightSize);
- mWidthMeasureSpec = widthMeasureSpec;
- }
onMeasure中的两个入参:widthMeasureSpec和heightMeasureSpec,两个入参表示ListView的父视图能够分配给ListVIew的宽度与高度,以及ListView的父视图指定的测量方式;测量方式一共有三种,分别为UNSPECIFIED方式、EXACTLY方式以及AT_MOST方式。UNSPECIFIED表示父视图完全不约束子视图的宽(高)度;EXACTLY表示子视图的宽(高)度只能由父视图决定;AT_MOST表示子视图的宽(高)度由子视图和父视图共同决定,即在不超过父视图能够提供的宽(高)度的情况下,子视图的宽(高)度想要多大就要多大。
此外,入参widthMeasureSpec不仅仅表示宽度的数值,也包含类宽度的测量方式;该入参是一个32byte的int类型,其中第30、31byte表示测量方式,第0~29byte才是表示宽度的数值。heightMeasureSpec的含义与widthMeasureSpec一致,只不过一个表示高度,一个表示宽度。
在初步了解两个入参的含义后,我们就开始分析onMeasure方法的源代码;总体而言,ListView的onMeasure方法可以分为5个步骤:
1、通过MeasureSpec类的相关方法,初步处理两个入参;即将宽(高)度的数值与测量方式进行分离;分离后的数值表示父视图能够提供的宽(高)度及父视图制定的宽(高)度测量方式。
2、进行判断,确定是否要对ListView的子视图进行测量;判断的标准是,ListView的宽度和高度是否至少有一个完全是由ListView自身决定的,如果是,则表示要进行子视图的测量,因为ListView自身的宽度也有可能被ListView的子视图决定(注意此处ListView的父视图、ListView、ListView的子视图三者之间的影响)。如果要进行子视图测量,则将调用ListView类的measureScrapChild方法,我们将在后续分析这个方法。
3、测量ListView的宽度,如果ListView的父视图所制定的宽度测量方式为UNSPECIFIED,则表示ListView的宽度完全由ListView自身决定,因此ListView的宽度等于childWidth加上ListView本身左右padding及垂直滚动条的宽度。否则其宽度就等于ListView的父视图所提供的宽度。
4、测量ListVIew的高度,如果ListView的父视图所制定的高度测量方式为UNSPECIFIED,则表示ListVIew的高度完全由ListView自身决定,因此ListView的高度等于childHeight加上ListView本身高低padding加上垂直边缘高度的两倍。否则如果ListView的父视图所制定的测量模式为AT_MOST,则其高度将通过调用ListView的measureHeightOfChildren方法来得出。同样我们将在后续分析这个方法。最后一种情况是ListView的父视图所制定的测量模式为EXACTLY,则不做任何处理。
5、调用ListView的setMeasuredDimension方法,设置ListView的测量宽度(包括测量模式)和测量高度(包括测量模式)。
在对ListView的onMeasured()方法进行了一个总体的分析之后,我们接下来看看measureScrapChid方法;从字面上来理解,此方法是用来测量废弃子视图;然而实际上,该方法所测量的子视图并不是废弃的,而是可废弃的;一个子视图可废弃,表示该子视图可能来至于重用视图的废弃堆之中,同时,当该子视图被废弃时也许要重新返回到重用视图的废弃堆之中;关于ListView的重用视图这一块儿,我们将在后续小节之中探讨。
mesureScrapChild方法既然时用来测量可废弃的子视图,那么在调用measureScrapChild方法之前,需要先获取一个可废弃的子视图;在ListView之中,通过其父类AbsListView的obtainView方法来获取一个可废弃的子视图。关于obtainView方法的内部实现,我们也将在后续小节之中继续探讨。
下面回到measureScrapChild方法之中,其源代码如下:
- private void measureScrapChild(View child, int position, int widthMeasureSpec) {
- LayoutParams p = (LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
- child.setLayoutParams(p);
- }
- p.viewType = mAdapter.getItemViewType(position);
- p.forceAdd = true;
- int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- }
- child.measure(childWidthSpec, childHeightSpec);//测量子视图
- }
由于ListView自身属性的决定(是上下滑动),所以其分配给子视图的高度并没有严格的控制,即如果子视图的布局参数中没有具体的指定高度数值,则由子视图自身决定器高度;如果指定了具体的高度数值,则以此数值为高度。
而对子视图的宽度,ListView则有比较严格控制,我们直接看ViewGroup的静态方法getChildMeasureSpec源码:
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
- int specMode = MeasureSpec.getMode(spec);//ListView的父类能够提供的测量方式
- int specSize = MeasureSpec.getSize(spec);//ListView的父类能够提供的尺寸
- int size = Math.max(0, specSize - padding);//ListView的父类能够提供的尺寸
- int resultSize = 0;
- int resultMode = 0;
- switch (specMode) {
- // Parent has imposed an exact size on us父视图强迫一个确切的数额
- case MeasureSpec.EXACTLY:
- if (childDimension >= 0) {
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- // Child wants to be our size. So be it.
- resultSize = size;
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- // Child wants to determine its own size. It can't be
- // bigger than us.
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- // Parent has imposed a maximum size on us
- case MeasureSpec.AT_MOST:
- if (childDimension >= 0) {
- // Child wants a specific size... so be it
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- // Child wants to be our size, but our size is not fixed.
- // Constrain child to not be bigger than us.
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- // Child wants to determine its own size. It can't be
- // bigger than us.
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- // Parent asked to see how big we want to be
- case MeasureSpec.UNSPECIFIED:
- if (childDimension >= 0) {
- // Child wants a specific size... let him have it
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- // Child wants to be our size... find out how big it should
- // be
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- // Child wants to determine its own size.... find out how
- // big it should be
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
该方法一共分了九种情况,我们将这九种情况特定与ListView来解释:
1、如果ListView的父类完全约束ListView的宽度,ListView的子视图布局参数中的宽度为一个具体值;那么ListView子视图的测量宽度为布局参数制定的具体值,测量模式为EXACTLY;
2、如果ListView的父类完全约束ListView的宽度,ListView的子视图布局参数中的宽度为MATCH_PARENT;那么ListView子视图的测量宽度为ListView父类分配给ListView的测量宽度数值,测量模式为EXACTLY;
3、如果ListView的父类完全约束ListView的宽度,ListView的子视图布局参数中的宽度为WRAP_CONTENT;那么ListView子视图的测量宽度为ListView弗雷分配给ListView的测量宽度数值,测量模式为AT_MOST;
4、如果ListView的父类完全不约束ListView的宽度,ListView的子视图布局参数中的宽度为一个具体值;那么ListView子视图的测量宽度为布局参数制定的具体值,测量模式为EXACTLY;
5、如果ListView的父类完全不约束ListView的宽度,ListView的子视图布局参数中的宽度为MATCH_PARENT;那么ListView子视图的测量宽度为0,测量模式为UNSPECIFIED;
6、如果ListView的父类完全不约束ListView的宽度,ListView的子视图布局参数中的宽度为WRAP_CONTENT;那么ListView子视图的测量宽度为0,测量模式为UNSPECIFIED;
7、如果ListView的父类不完全约束ListView的宽度,ListView的子视图布局参数中的宽度为一个具体值;那么ListView子视图的测量宽度为布局参数制定的具体值,测量模式为EXACTLY;
8、如果ListView的父类不完全约束ListView的宽度,ListView的子视图布局参数中的宽度为MATCH_PARENT;那么ListView子视图的测量宽度为ListView父类分配给ListView的测量宽度数值,测量模式为AT_MOST;
9、如果ListView的父类不完全约束ListView的宽度,ListView的子视图布局参数中的宽度为WRAP_CONTENT;那么ListView子视图的测量宽度为ListView父类分配给ListView的测量宽度数值,测量模式为AT_MOST;
至此,我们就完全分析了measureScrapView方法;该方法总体而言,可分为4个步骤:
1、布局参数(LayoutParam)相应的处理;
2、合成父视图(ListView)能够提供给该子视图的宽度测量模式及宽度测量数值,因为ListVIew上下滚动的特性,此步骤调用了ViewGroup的静态方法 getChildMeasureSpec;
3、合成父视图(ListView)能够提供给该子视图的高度测量模式及高度测量数值;
4、根据第2、3点得到的结果调用子视图的measure来进行子视图测量。
接下来我们继续分析ListView的onMeasured()中的另一个方法:measureHeightOfChildren。当ListView的高度的测量模式是AT_MOST时,即由ListView和其父类共同决定时,调用该方法。
源码如下:
- final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
- final int maxHeight, int disallowPartialChildPosition) {
- final ListAdapter adapter = mAdapter;
- if (adapter == null) {
- return mListPadding.top + mListPadding.bottom;
- }
- // Include the padding of the list
- int returnedHeight = mListPadding.top + mListPadding.bottom;//返回高度包括列表的padding
- final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
- // The previous height value that was less than maxHeight and contained
- // no partial children
- int prevHeightWithoutPartialChild = 0;//除了特殊的子视图外的子视图的高度
- int i;
- View child;
- // mItemCount - 1 since endPosition parameter is inclusive
- //在onMeasured方法之中调用该方法时,endPosition参数的值为NO_POSITION
- endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
- final AbsListView.RecycleBin recycleBin = mRecycler;
- final boolean recyle = recycleOnMeasure();//ListView中该方法始终返回true
- final boolean[] isScrap = mIsScrap;
- //在onMeasured方法之中调用该方法时,startPosition参数的值为0
- for (i = startPosition; i <= endPosition; ++i) {
- child = obtainView(i, isScrap);
- measureScrapChild(child, i, widthMeasureSpec);//测量子视图
- if (i > 0) {
- // Count the divider for all but one child
- returnedHeight += dividerHeight;//增加分割线
- }
- // Recycle the view before we possibly return from the method
- if (recyle && recycleBin.shouldRecycleViewType(
- ((LayoutParams) child.getLayoutParams()).viewType)) {
- recycleBin.addScrapView(child, -1);
- }
- returnedHeight += child.getMeasuredHeight();//增加子视图的高度
- if (returnedHeight >= maxHeight) {
- // We went over, figure out which height to return. If returnedHeight > maxHeight,
- // then the i'th position did not fit completely.
- return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
- && (i > disallowPartialChildPosition) // We've past the min pos
- && (prevHeightWithoutPartialChild > 0) // We have a prev height
- && (returnedHeight != maxHeight) // i'th child did not fit completely
- ? prevHeightWithoutPartialChild
- : maxHeight;
- }
- if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
- prevHeightWithoutPartialChild = returnedHeight;
- }
- }
- // At this point, we went through the range of children, and they each
- // completely fit, so return the returnedHeight
- return returnedHeight;
- }
测量所给范围的子视图的高度,返回的高度包括ListView的padding和分割线高度。如果提供了最大高度,当测出的高度达到最大值时,这个测量将停止。该方法在测量时,从入参startPosition开始,到入参endPosition结束,一共会测量(endPosition-startPosition+1)个子视图,而返回的高度值则是这些子视图的高度和加上分割线高度和加上列表的paddingTop、和paddingBottom和。如果返回的高度少于入参maxHeight,则直接返回测量的高度值,如果大于等于maxHeight,则返回maxHeight。
在onMeasured()方法调用measureHeightOfChildren方法时,如果ListView的父视图制定的高度数值,则入参maxHeight就等于ListView的父视图制制定的高度数值,否则入参maxHeight的值则等于ListView中一个子视图的高度加上列表的paddingTop和paddingBottom加上垂直边缘高度的两倍。
对于measureHeightOfChildren笔者还有个疑惑,还请相关高手解答:入参disallowPartialChildPosition只有当大于等于0时才有意义,而笔者观测了一下,整个ListView调用measureHeightOfChildren的地方只有onMeasured()方法,而且在调用时,入参disallowPartialChildPosition的值为-1;所以个人感觉这个参数没有什么意义。如果有高手知道这个参数的具体含义,麻烦告之!谢谢!
至此,ListView的onMeasured()方法便分析完成,下面进行一下总结:
首先判断此ListView的宽度和高度是否至少有一个的测量模式是UNSPECIFIED,UNSPECIFIED表示宽(高)度需要ListView自身来决定,而ListView的自身决定也是受子视图决定的,因此需要在此处调用一次measureScrapView方法,来确定一个子视图的宽度或高度;而measureScrapView之中,因为ListView的特性(可以上下滑动,不可左右滑动),所以会调用ViewGroup的getChildMeasureSpec方法来具体决定ListView子视图的测量宽度(此方法涉及ListView父类、ListView及ListView子类三者的决定)。
接着,会再次判断ListView的的宽度测量模式,如果是UNSPECIFIED模式,则根据第一步测出来的一个子视图的宽度来生成ListView的宽度,否则将ListView父类提供的宽度数值作为宽度具体数值,将根据第一步测出来的一个子视图的宽度测量模式作为宽度测量模式,最后将两者合成作为ListView的最终宽度。
然后,和宽度一样,会再次判断ListView的高度测量模式,如果是UNSPECIFIED模式,与宽度的处理模式类似;如果是AT_MOST模式,即父视图子视图共同决定,则会调用measureHeightOfChildren方法来进行测量高度。而在measureHeightOfChildren方法中会根据两个入参(startPosition,endPosition)来循环测量多个子视图的高度(循环调用measureScrapView方法)。
最后,设置ListView的测量高度和测量宽度。
二、布局
在分析布局方法之前,我们首先介绍一下ListView的布局模式;ListView的布局模式一共有7种:
LAYOUT_NORMAL :有规则的布局,通常是被视图系统主动提供的布局(mStackFromBottom变量);此模式为默认模式。
LAYOUT_FORCE_TOP :以第一个item为基础填充剩下的item,即显示第一个item。
LAYOUT_SET_SELECTION :强制被选的item出现在屏幕之中;
LAYOUT_FORCE_BOTTOM :以最后一个item为基础填充剩下的item,显示最后一个item。
LAYOUT_SPECIFIC :确保被选择的item出现在一个特定的位置,在这个特定位置的基础上构建剩下的视图;顶部位置被mSpecificTop制定。
LAYOUT_SYNC :同步时,需要进行重新布局,其为数据改变的结果。
LAYOUT_MOVE_SELECTION :为使用定位键而进行布局
分析完测量后,我们再来看看ListView的布局方法;ListView之中没有直接重写onLayout方法,而是把这份工作交给了它的父类AbsListView,因此我们直接看AbsListView中的onLayout方法。
其实在分析ListView之中的onMeasured()方法时,我们发现此时的ListView还未添加任何子视图,那么onLayout之中会添加ListView的子视图吗?我们就带着这个问题进入AbsListView类之中的onLayout方法,其源码如下:
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- ...
- layoutChildren();
- ...
- }
该方法主要的代码是,layoutChildren()方法。AbsListView类中的layoutChildren()方法什么都没做,因此我们直接看ListView中的layoutChilren()方法。 ListView中的layoutChildren()方法有251行,所以笔者打算分段分析;在分段之前,对该方法做一个总体的流程总结:
1、确定当前被选择的item及最新被选择的item;
2、如果数据改变了,则调用AbsListView.handleDataChanged函数来处理数据集的改变;
3、更新被选择的item(用mNextSelectedPosition赋值mSelectedPosition);
4、对焦点子视图的相关处理,焦点视图具有临时状态;
5、根据数据集是否改变来判断:如果已经改变,则将子视图添加到废弃视图堆之中;如果没有改变则将当前所有的视图添加到活动视图列表之中;清除上一次布局的所有老视图;
6、根据不同的布局模式来按照不同的方式来填充列表;
7、将回收机制中当前所有活跃的视图列表降级为废弃视图;
8、确定更新选择绘制物矩形(mSelectorRect)的范围;
下面我们根据这八点来分析ListView的layoutChildren()方法。
- ...
- int index = 0;//布局前被现在的item对应的视图在子视图列表中的索引
- int delta = 0;//布局前被选择的item与布局后被选择的item的位置差
- View oldSel = null;//当前选择item对应的视图
- View oldFirst = null;//布局前第一个子视图
- View newSel = null;//布局完成后被选择的item对应的视图
- // Remember stuff we will need down below
- switch (mLayoutMode) {//当前布局模式
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
- // Remember the previously selected view
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
- // Remember the previous first child
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
- // Caution: newSel might be null
- newSel = getChildAt(index + delta);
- }
- ...
这段代码主要确定5个变量的值;根据布局模式的不同,该代码有三种确定变量值的方式;如果当前布局模式为显示第一个item模式、显示最后一个item模式、根据位于特定位置的被选择的item来进行布局的模式、因为同步数据而进行的布局的模式这四种模式,则什么都不做;如果是强制被选的item出现在屏幕之中的布局模式,则只确定被选择的item对应的视图在子视图列表中的索引,已经布局后被选择的item对应的视图;其余的布局模式则是要全部确定这5个变量值。
需要注意的是,如果当前ListView还未有任何子视图,则这5个变量可能为空。
- ...
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
- ...
关于handleDataChanged()方法,我们将在后续分析其源码。我们继续layoutChildren方法的分析
- ...
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw ...//抛出一个异常
- }
- setSelectedPositionInt(mNextSelectedPosition);//更新被选中的item
- ...
接下来是判断mItemCount是否合法,并且更新被选择的item的位置。即将布局后的被选择的item的位置赋值给布局前的被选择的item的位置。也就是说,此处布局前,布局后的被选择的item的位置将变为一致。
- ...
- // Ensure the child containing focus, if any, has transient state.
- // If the list data hasn't changed, or if the adapter has stable
- // IDs, this will maintain focus.
- //如果列表数据未改变,或者适配器有一个稳定的行ID,列表将包含一个焦点
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- focusedChild.setHasTransientState(true);
- }
- ...
如果列表存在一个焦点子视图,则将该子视图的临时状态设置为true;关于临时状态,在后续分析ListView的重用视图时,也会进一步的分析。
- ...
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
- if (dataChanged) {//如果数据发生改变了,则所有的子视图都将重新丢进重用视图池里的废弃视图堆之中
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i), firstPosition+i);
- }
- } else {//如果数据未发生改变,则将当前所有的子视图保存在重用视图池离的活跃视图数组之中,以便重用。
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
- // Clear out old views
- detachAllViewsFromParent();//清除ListView的所有子视图
- recycleBin.removeSkippedScrap();//清除重用视图池中当前此刻需要清除的视图(例如一些具有临时状态的视图)
- ...
此段代码处理了,在真正布局前,对布局前的子视图进行处理(如果此时不是第一次布局);处理的方式分为两种:
数据已改变,将当前所有子视图丢弃到重用视图池中的废弃视图堆中。
数据未改变,将当前所有子视图保存到重用视图池中的活跃视图数组中。
将当前所有的子视图保存完毕后,就将当前所有的子视图从ViewGroup之中清除(重用视图池中的视图还是存在的)。最后清除重用视图池中需要清除的视图。
接下来便涉及到真正的布局处理了:
- ...
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- if (newSel != null) {
- //从布局后被选择的item对应的视图开始布局(填充)
- sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
- } else {
- //从屏幕的中间位置开始布局(填充)
- sel = fillFromMiddle(childrenTop, childrenBottom);
- }
- break;
- case LAYOUT_SYNC:
- //从同步位置开始布局(填充)
- sel = fillSpecific(mSyncPosition, mSpecificTop);
- break;
- case LAYOUT_FORCE_BOTTOM:
- //从下往上开始布局(填充)
- sel = fillUp(mItemCount - 1, childrenBottom);
- adjustViewsUpOrDown();
- break;
- case LAYOUT_FORCE_TOP:
- //从上往下开始布局(填充)
- mFirstPosition = 0;
- sel = fillFromTop(childrenTop);
- adjustViewsUpOrDown();
- break;
- case LAYOUT_SPECIFIC:
- //从制定位置开始布局(填充)
- sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
- break;
- case LAYOUT_MOVE_SELECTION:
- sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
- break;
- default:
- if (childCount == 0) {
- if (!mStackFromBottom) {
- final int position = lookForSelectablePosition(0, true);
- setSelectedPositionInt(position);
- sel = fillFromTop(childrenTop);
- } else {
- final int position = lookForSelectablePosition(mItemCount - 1, false);
- setSelectedPositionInt(position);
- sel = fillUp(mItemCount - 1, childrenBottom);
- }
- } else {
- if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
- sel = fillSpecific(mSelectedPosition,
- oldSel == null ? childrenTop : oldSel.getTop());
- } else if (mFirstPosition < mItemCount) {
- sel = fillSpecific(mFirstPosition,
- oldFirst == null ? childrenTop : oldFirst.getTop());
- } else {
- sel = fillSpecific(0, childrenTop);
- }
- }
- break;
- }
- ...
ListView的填充方式就是承载ListView布局中真正布局的那一部分工作;填充方式在对子视图进行填充时,不仅对子视图进行布局,还将子视图真正的添加到了ListV
iew之中。ListView提供了多种填充方式来应对上文所提的7中布局模式;关于这些填充方式(方法)的具体实现,我们将在下一章具体分析。
此处我们主要分析布局模式为LAYOUT_NORMAL的情况(也就是上述代码里switch中的default分支)。
childCount为0,则表示没有子视图(例如第一次布局时),根据mStackFromBottom来判断当前ListView的填充方法(视图系统主动提供的布局),在XML布局文件中可以用android:stackFromBottom属性来设置该值(true
or false),该值默认false,也就是默认从上到下布局。如果mStackFromBottom为false,表示从上往下,则从第一个item开始向下填充子视图;如果mStackFromBottom为true,表示从上往下,则从最后一个item开始向上填充。同时,此处的开始位置(第一个或者最后一个)将被设置为当前被选择的item。
childCount不为0,则表示之前有布局过,此刻判断当前被选择的item是否合法,如果合法,则以当前位置为开始点,填充子视图;如果当前被选择的item不合法,则判断当前屏幕上第一个视图对应的item所处的位置是否合法,如果合法则以屏幕上第一个视图对应的item所处的位置为开始点,填充子视图;如果以上两个条件都不成立,则以第一个item为开始点,填充子视图。
ListView之中所有的填充方式都涉及到ListView的视图重用功能。由于第5步,会将上一次布局的子视图保存到重用视图池中相关的位置(活跃视图数组或者废弃视图堆或者临时视图数组);因此在填充方法中,会重用所有能够重用的视图。如果填充方法执行结束后,重用视图池中的活跃视图数组还存在视图,则将根据这些视图的状态(是否具有临时状态)分别丢弃到重用视图池中的临时视图数组或者废弃视图堆之中。而这一步只需如下一行代码即可:
- ...
- // Flush any cached views that did not get reused above
- //将所有能缓存的,并没有被重用的视图冲刷掉(放入废弃视图堆之中)
- recycleBin.scrapActiveViews();
- ...
在此之后,layoutChildren方法主要的工作为更新选择器可绘物,执行布局后的滚动,更新下一次布局的被选择item位置等。
至此,ListView之中的布局方法大体分析结束。
三、绘制
ListView中的绘制流程基本上由其父类或者View来完成;对于ListView而言,主要处理列表之上(下)的可绘制物的绘制、列表分割线绘制等;而这方面的工作主要是由ListView中的方法dispatchDraw来完成。
总体而言,ListView的dispatchDraw方法可总结为以下几个步骤:
1、确定是否绘制列表内容之上(下)的可绘制物,是否绘制分割线;
2、确定item数量相关的变量;例如item的总数、页眉(脚)数、第一个可视item的位置等;
3、以从上到下或者从下到上为标准,分为两种情况;这两种情况的流程大致一致:先绘制列表内容之上的可绘制物,再绘制列表内容之中每个item之间的分割线,最后再绘制列表内容之下的可绘制物。
4、调用父类中的dispatchDraw方法来绘制ListView的item对应的视图。
- @Override
- protected void dispatchDraw(Canvas canvas) {
- if (mCachingStarted) {
- mCachingActive = true;
- }
- // Draw the dividers
- final int dividerHeight = mDividerHeight;
- final Drawable overscrollHeader = mOverScrollHeader;//列表之上的绘制物
- final Drawable overscrollFooter = mOverScrollFooter;//列表之下的绘制物
- final boolean drawOverscrollHeader = overscrollHeader != null;//是否绘制列表之上的绘制物
- final boolean drawOverscrollFooter = overscrollFooter != null;//是否绘制列表之下的绘制物
- final boolean drawDividers = dividerHeight > 0 && mDivider != null;//是否绘制分割线
- if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
- // Only modify the top and bottom in the loop, we set the left and right here
- //确定列表的左边、右边范围
- final Rect bounds = mTempRect;
- bounds.left = mPaddingLeft;
- bounds.right = mRight - mLeft - mPaddingRight;
- final int count = getChildCount();
- final int headerCount = mHeaderViewInfos.size();
- final int itemCount = mItemCount;//mItemCount包括页眉、item、页脚三部分总item数
- final int footerLimit = (itemCount - mFooterViewInfos.size());//最后一个item的位置
- final boolean headerDividers = mHeaderDividersEnabled;
- final boolean footerDividers = mFooterDividersEnabled;
- final int first = mFirstPosition;
- final boolean areAllItemsSelectable = mAreAllItemsSelectable;
- final ListAdapter adapter = mAdapter;
- // If the list is opaque *and* the background is not, we want to
- // fill a rect where the dividers would be for non-selectable items
- // If the list is opaque and the background is also opaque, we don't
- // need to draw anything since the background will do it for us
- //如果列表不透明,并且背景也不透明,我们则不需要绘制任何东西,因为背景将为我们做这些
- final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
- if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
- mDividerPaint = new Paint();
- mDividerPaint.setColor(getCacheColorHint());
- }
- final Paint paint = mDividerPaint;
- int effectivePaddingTop = 0;//有效的padding顶部
- int effectivePaddingBottom = 0;//有效的padding底部
- //当FLAG_CLIP_TO_PADDING 和 FLAG_PADDING_NOT_NULL 同时设置时,绘图将剪切掉在内边距区域内的图像
- if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
- effectivePaddingTop = mListPadding.top;
- effectivePaddingBottom = mListPadding.bottom;
- }
- final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
- if (!mStackFromBottom) {//从上到下
- int bottom = 0;
- // Draw top divider or header for overscroll
- //为滚动越界的情况绘制顶部分割线或者页眉视图
- final int scrollY = mScrollY;
- if (count > 0 && scrollY < 0) {//如果为滚动越界的情况
- if (drawOverscrollHeader) {//绘制列表之上的可绘制物
- bounds.bottom = 0;
- bounds.top = scrollY;
- drawOverscrollHeader(canvas, overscrollHeader, bounds);
- } else if (drawDividers) {//绘制分割线
- bounds.bottom = 0;
- bounds.top = -dividerHeight;
- drawDivider(canvas, bounds, -1);
- }
- }
- //绘制item
- for (int i = 0; i < count; i++) {
- final int itemIndex = (first + i);//在mFirstPosition基础上进行绘制
- final boolean isHeader = (itemIndex < headerCount);//此处位置是否位于页眉之中
- final boolean isFooter = (itemIndex >= footerLimit);//此处位置是否位于页脚之中
- if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {//需要绘制分割线的情况
- final View child = getChildAt(i);
- bottom = child.getBottom();
- final boolean isLastItem = (i == (count - 1));//是否是最后一个子视图
- if (drawDividers && (bottom < listBottom)
- && !(drawOverscrollFooter && isLastItem)) {
- final int nextIndex = (itemIndex + 1);
- // Draw dividers between enabled items, headers and/or
- // footers when enabled, and the end of the list.
- if (areAllItemsSelectable
- || ((adapter.isEnabled(itemIndex)|| (headerDividers && isHeader)|| (footerDividers && isFooter))
- && (isLastItem|| adapter.isEnabled(nextIndex)
- || (headerDividers && (nextIndex < headerCount))
- || (footerDividers && (nextIndex >= footerLimit))))) {
- bounds.top = bottom;
- bounds.bottom = bottom + dividerHeight;
- drawDivider(canvas, bounds, i);
- } else if (fillForMissingDividers) {
- bounds.top = bottom;
- bounds.bottom = bottom + dividerHeight;
- canvas.drawRect(bounds, paint);
- }
- }
- }
- }
- final int overFooterBottom = mBottom + mScrollY;
- //绘制列表内容之下的可绘制物
- if (drawOverscrollFooter && first + count == itemCount &&
- overFooterBottom > bottom) {
- bounds.top = bottom;
- bounds.bottom = overFooterBottom;
- drawOverscrollFooter(canvas, overscrollFooter, bounds);
- }
- } else {//从下到上绘制,与从上到下类似
- ...
- }
- }
- // Draw the indicators (these should be drawn above the dividers) and children
- super.dispatchDraw(canvas);
- }
至此我们便将ListView的三大方法大致分析完成;其中主要分析了测量和布局,次要分析了绘制方法。
其中测量主要结合了ListView的父类、ListVIew以及ListView的子类三者共同作用,三种不同的测量模式对应了不同的测量结果;而布局则是根据不同的布局场景对ListView的子视图进行布局,在下一章我们将具体分析这些不同的布局方法;最后,因为ListView并未负责ListView绘制的主要工作,所以对绘制流程介绍得十分简单。

浙公网安备 33010602011771号