視圖從初始化到完全展示到螢幕之上,這段時間裡,還有許多工作要做;總體而言,這些工作可用分為三大步驟;而這三大步驟便是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繪制的主要工作,是以對繪制流程介紹得十分簡單。