OverView
Android ListView 是Android中常用的長(zhǎng)列表組件扣猫,其繼承層次如下:
用法
通常在業(yè)務(wù)代碼中使用ListView的常用姿勢(shì)是:
- 創(chuàng)建1個(gè)ListView
- 創(chuàng)建1個(gè)BaseAdapter的子類(lèi)养叛,實(shí)現(xiàn)getCount/getItem/getItemId/getView這4個(gè)方法洲押,有時(shí)候還會(huì)實(shí)現(xiàn)getItemViewType/getViewTypeCount方法來(lái)滿(mǎn)足有多種ItemView樣式的需求
- 將BaseAdatper的子類(lèi)實(shí)例通過(guò)ListView的setAdapter()方法,設(shè)置給ListView實(shí)例
常用優(yōu)化
通常的ListView在View的復(fù)用上有2種優(yōu)化:
- public View getView(int position, View convertView, ViewGroup parent)這個(gè)方法在實(shí)現(xiàn)是,首先判斷一下傳入的convertView是否為null,不為null即可復(fù)用断楷,無(wú)需調(diào)用inflate或者new來(lái)新創(chuàng)建1個(gè)View
- 可以通過(guò)ViewHoloder的方法,將convertView的子View直接存一個(gè)引用在ViewHolder中崭别,然后將ViewHolder通過(guò)convertView的setTag方法存儲(chǔ)在convertView上冬筒;這種做法的好處在于,通過(guò)對(duì)子View的直接引用訪問(wèn)茅主,避免了findViewById的耗時(shí)操作
源碼淺析
ListView的源碼比較長(zhǎng)舞痰,暫時(shí)先把精力放在理解Adapter的6個(gè)方法(getViewTypeCount/getItemViewType/getCount/getItem/getItemId/getView)被調(diào)用的時(shí)機(jī)上,更詳細(xì)的源碼分析文章已經(jīng)很多了诀姚,比較經(jīng)典的有郭霖前輩的https://blog.csdn.net/sinyu890807/article/details/44996879
int getViewTypeCount()
在源碼中搜索getViewTypeCount()被引用的位置响牛,得到的和ListView相關(guān)的結(jié)果是在setAdapter(ListAdapter adapter)方法中有1行:
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
這個(gè)mRecycler成員是RecycleBin類(lèi)型,RecycleBin的定義在AbsListView中赫段,其作用顧名思義呀打,就是起到1個(gè)回收的垃圾箱作用,其setViewTypeCount(int viewTypeCount)方法的實(shí)現(xiàn)為:
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//noinspection unchecked
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
RecycleBin回收廢棄View的實(shí)現(xiàn)是通過(guò)其scrapViews數(shù)組實(shí)現(xiàn)的糯笙,而傳入的viewTypeCount決定了這個(gè)數(shù)組的長(zhǎng)度贬丛,注意scrapViews的每一個(gè)成員是一個(gè)ArrayList<View>;在這里我的理解是给涕,viewTypeCount決定了有多少種View會(huì)被回收豺憔,而每1個(gè)被回收的View會(huì)根據(jù)viewType進(jìn)入到對(duì)應(yīng)的ArrayList<View>中去额获,方便在復(fù)用時(shí)從正確的類(lèi)型中取出對(duì)應(yīng)的View來(lái)進(jìn)行復(fù)用。
int getItemViewType(int position)
在源碼中搜索該函數(shù)焕阿,有好幾處調(diào)用的地方咪啡,但多數(shù)調(diào)用都是得到viewType之后設(shè)置到AbsListView.LayoutParams的viewType屬性上使用首启,這種使用并不是非常重要暮屡,真正關(guān)鍵的調(diào)用在AbsListView的getScrapView(position)函數(shù)中:
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
getScrapView(int position)函數(shù)的作用在于,從廢棄的View中獲取一個(gè)View毅桃,準(zhǔn)備復(fù)用褒纲,從實(shí)現(xiàn)上可以看出,getItemViewType的作用在于钥飞,得到正確的ViewType莺掠,從而從對(duì)應(yīng)的mScrapViews數(shù)組中取出1個(gè)ScrapView
int getCount()
getCount()的調(diào)用在源碼中的搜索結(jié)果就實(shí)在是太多了,粗略瀏覽的一下读宙,把覺(jué)得比較關(guān)鍵的點(diǎn)記錄下來(lái)
- 首先是ListView的setAdatper()函數(shù)中有這么一句:
mItemCount = mAdapter.getCount();
這個(gè)mItemCount是ListView的祖先類(lèi)AdapterView的成員彻秆,設(shè)置到這上面之后就能更加方便地使用了
- 然后是ListView的layoutChildren()函數(shù)中有這么一段:
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
在layoutChildren()過(guò)程中需要檢查mItemCount和mAdapter.getCount()是否一致,如果不一致结闸,證明數(shù)據(jù)源被改變了卻沒(méi)有調(diào)用notifyDataSetChanged()通知觀察方
Object getItem(int position)
getItem(int position)方法主要被調(diào)用的地方在AdapterView的getItemAtPosition(int position)函數(shù)中:
public Object getItemAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? null : adapter.getItem(position);
}
除此之外唇兑,在源碼中再?zèng)]找到getItem(int position)的相關(guān)調(diào)用,這也可以理解桦锄,因?yàn)間etItem(int position)更主要的使用場(chǎng)景是我們?cè)跇I(yè)務(wù)代碼中調(diào)用扎附,通過(guò)該方法,能夠從Adapter里拿出數(shù)據(jù)項(xiàng)结耀,而不需要直接跟數(shù)據(jù)源接觸留夜。
long getItemId(int position)
getItemId(int postion)函數(shù)在源碼中搜索,ListView中的調(diào)用已經(jīng)被標(biāo)注為@Deprecate图甜,其余主要的調(diào)用都在AbsListView中碍粥,選擇其中一處來(lái)看下這個(gè)方法的作用
public void setItemChecked(int position, boolean value) {
if (mChoiceMode == CHOICE_MODE_NONE) {
return;
}
// Start selection mode if needed. We don't need to if we're unchecking something.
if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
if (mMultiChoiceModeCallback == null ||
!mMultiChoiceModeCallback.hasWrappedCallback()) {
throw new IllegalStateException("AbsListView: attempted to start selection mode " +
"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
"supplied. Call setMultiChoiceModeListener to set a callback.");
}
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
}
final boolean itemCheckChanged;
if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
boolean oldValue = mCheckStates.get(position);
mCheckStates.put(position, value);
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
if (value) {
mCheckedIdStates.put(mAdapter.getItemId(position), position);
} else {
mCheckedIdStates.delete(mAdapter.getItemId(position));
}
}
itemCheckChanged = oldValue != value;
if (itemCheckChanged) {
if (value) {
mCheckedItemCount++;
} else {
mCheckedItemCount--;
}
}
if (mChoiceActionMode != null) {
final long id = mAdapter.getItemId(position);
mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
position, id, value);
}
} else {
……
}
// this may end up selecting the value we just cleared but this way
// we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
if (value) {
mCheckStates.put(position, true);
if (updateIds) {
mCheckedIdStates.put(mAdapter.getItemId(position), position);
}
mCheckedItemCount = 1;
} else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
mCheckedItemCount = 0;
}
}
// Do not generate a data change while we are in the layout phase or data has not changed
if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) {
mDataChanged = true;
rememberSyncState();
requestLayout();
}
}
可以看到,主要是通過(guò)該方法黑毅,獲得對(duì)應(yīng)位置的id后嚼摩,能夠作為一個(gè)索引,用于增刪改查等快速操作博肋。
View getView(int position, View convertView, ViewGroup parent)
最后Adapter中最重要的方法低斋,getView方法的作用是返回某一項(xiàng)對(duì)應(yīng)的ItemView,在源碼中關(guān)鍵的調(diào)用是AbsListView中的obtainView()方法中的調(diào)用:
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
AbsListView的obtainView函數(shù)的作用就是構(gòu)建出某一position對(duì)應(yīng)的View匪凡,首先會(huì)從mRecycler中取出一個(gè)對(duì)應(yīng)的廢棄View膊畴,這個(gè)廢棄View就是傳入Adapter的getView()方法中的convertView,這里也就解釋了為什么需要判空——在首次布局時(shí)病游,實(shí)際上是還沒(méi)有廢棄的View可用的唇跨,而后面布局時(shí)就有廢棄的View可復(fù)用稠通,無(wú)需重新構(gòu)建了。