該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡可能按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深刻理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑑了其餘的優質博客,在此向各位大神表示感謝,膜拜!!!java
列表展現控件(ListView或者RecyclerView)是咱們在開發過程當中常常要使用到的一種控件。而咱們學習Android開發的時候,ListView也是必須掌握的。那麼本篇咱們來講一下ListView,雖然如今ListView逐漸的被RecyclerView取代,包括我本身的項目中也是使用的RecyclerView。那麼爲何要分析一個「過期」的東西呢?由於RecyclerView的前輩,許多遺留項目是基於ListView的,可能由於種種緣由不能更換或者更換代價太大,那麼咱們如何在ListView的基礎上優化App就成了咱們不得不面對的問題。同時對於ListView的學習也有助於RecyclerView的掌握。android
注:關於ListView的各部份內容,網上存在着大量的博客以及教程,講解的有淺有深。本篇博客呢立足於日常開發時所遇到的一些問題,也是自己對知識的掌握程度的檢視。git
關於ListView的簡單使用我這裏就不詳細分析了,只貼上一個實例源碼以及作一個小結,對應的源碼目錄已用紅框標出github
佈局文件activity_list_view.xml面試
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="#00000000" android:divider="#f4f4f4" android:dividerHeight="1dp" > </ListView> </LinearLayout>
對應的源碼ListViewActivity算法
public class ListViewActivity extends AppCompatActivity { @BindView(R.id.list_view) ListView mListView; private List<String> mArrayList= new ArrayList(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); ButterKnife.bind(this); //初始化數據 init(); //建立Adapater ListViewAdapter adapter = new ListViewAdapter(this,mArrayList); //設置Adapter mListView.setAdapter(adapter); //設置item點擊監聽事件 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(ListViewActivity.this,mArrayList.get(position),Toast.LENGTH_SHORT).show(); } }); } private void init() { for (int i=0;i<20;i++){ mArrayList.add("這是第"+i+"個View"); } } }
ListView對應的的Adapter數據庫
public class ListViewAdapter extends BaseAdapter { private static final String TAG = ListViewAdapter.class.getSimpleName(); private List<String> mList; private LayoutInflater inflater; private ViewHolder viewHolder; public ListViewAdapter(Context context, List<String> list) { mList = list; this.inflater = LayoutInflater.from(context); } @Override public int getCount() { return mList == null ? 0 : mList.size(); } @Override public Object getItem(int position) { return mList == null ? null : mList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null){ viewHolder = new ViewHolder(); convertView = inflater.inflate(R.layout.list_view_item_simple, null); viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view); convertView.setTag(viewHolder); Log.d(TAG,"convertView == null"); }else { Log.d(TAG,"convertView != null"); viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.mTextView.setText(mList.get(position)); return convertView; } static class ViewHolder { private TextView mTextView; } }
關於ListView的使用及Adapter優化,這裏給出了咱們經常使用的優化方法,使用ViewHolder進行性能優化,這些內容也都是老生常談的內容。不過想要深刻理解使用ViewHolder是如何作到優化的,咱們還得繼續向下看,這裏呢只是給出一個小例子讓讀者可以應對初中級開發工程師的面試提問。segmentfault
在面試初中級Android開發工程師的時候,關於列表項展現這塊基本上是必問的,你若是使用的ListView,那麼ListView的性能優化,以及後面要講到的下拉刷新上拉加載,基本也是必問的,由於這是你日常項目開發中也是確定要考慮到的點。數組
對於初中級Android開發工程師來講,面試ListView的性能優化時你要回答的上來如下兩點:①在ListView的Adapter中複用getView方法中的convertView ②使用靜態內部類ViewHolder,用於對控件的實例存儲進行緩存,減小findViewById的調用次數。緩存
在這一小節中,介紹一些ListView 中的一些重要屬性,有一些常常在項目開發中用到,而有一些不太經常使用,不過能夠做爲知識面的擴充
分割線
android:divider="#00000000" //或者在javaCode中以下定義:listView.setDividerHeight(0); android:divider="@drawable/list_driver" //設置分割線的圖片資源 android:divider="@drawable/@null" //不想顯示分割線
滾動條
android:scrollbars="none"//隱藏listView的滾動條 在javaCode中以下定義setVerticalScrollBarEnabled(true); android:fadeScrollbars="true" //設置爲true就能夠實現滾動條的自動隱藏和顯示
去掉上邊和下邊黑色的陰影
android:fadingEdge="none" 去掉上邊和下邊黑色的陰影
快速滾動
android:fastScrollEnabled="true" 或者在javaCode中以下定義:mListView.setFastScrollEnabled(true); 來控制啓用,參數false爲隱藏。
須要注意的是當你的滾動內容較小,不到當前ListView的3個屏幕高度時則不會出現這個快速滾動滑塊,同時該方法仍然是AbsListView的基礎方法,能夠在ListView或GridView等子類中使用快速滾動輔助。
stackFromBottom屬性,這隻該屬性以後你作好的列表就會顯示你列表的最下面,值爲true和false
android:stackFromBottom="true" //該屬性默認爲false,設置爲true後,你的列表會從最後一項向前顯示一屏
若是你只是換背景的顏色的話,能夠直接指定android:cacheColorHint爲你所要的顏色,若是你是用圖片作背景的話,那也只要將android:cacheColorHint指定爲透明(#00000000)就能夠了
關於上面的屬性,讀者能夠逐一測試,我這裏就不貼測試結果了。
上面的內容止步於Android初中級開發工程師,那麼對於中高級來講,面試官就不知足於你上面的回答了,可能會問你一些更深刻的問題。例如ListView展現成千上萬條數據爲何沒有發生OOM呢?ListView在滑動的時候異步請求所致使的圖片錯位問題產生的原理及如何解決??等等
要比較完美的回答出這樣的問題,那麼咱們就得向ListView的源碼進發。
咱們先來上一張圖
做爲咱們第一個詳細講解的系統控件ListView,其歸根究竟是個View,關於View以及ViewGroup咱們這裏就不分析了,咱們從AdapterView開始
AdapterView顧名思義是個有Adapter的View,其內定義了setAdapter、getAdapter等抽象方法供子類實現。View說究竟是展現數據的控件,就像咱們的TextView同樣,Android提供的這些View系統控件也都是爲了展現各類各樣的數據,那麼AdapterView也不例外。Android設計AdapterView呢就是爲了那些數據源沒法肯定的場景,你若是想展現大量數據,那麼你須要自定義數據源(數據源多是數組,也多是List,也多是數據庫)。而後你須要自定義適配器即Adapter,讓AdapterView經過適配器與數據源聯繫在一塊兒。
也就是說AdapterView提供了一種不須要關心數據源的通用的展現大量數據的方法。
Adapter是適配器的意思,它在ListView和數據源之間起到了一個橋樑的做用,ListView並不會直接和數據源打交道,而是會藉助Adapter這個橋樑來去訪問真正的數據源,與以前不一樣的是,Adapter的接口都是統一的,所以ListView不用再去擔憂任何適配方面的問題。而Adapter又是一個接口(interface),它能夠去實現各類各樣的子類,每一個子類都能經過本身的邏輯來去完成特定的功能,以及與特定數據源的適配操做,好比說ArrayAdapter能夠用於數組和List類型的數據源適配,SimpleCursorAdapter能夠用於遊標類型的數據源適配,這樣就很是巧妙地把數據源適配困難的問題解決掉了,而且還擁有至關不錯的擴展性。
也就是說Adapter是統一的接口,定義通用的適配接口,咱們實現該接口方法進行自定義適配操做便可。(Android已經預先定義了一些場景所須要的接口和基類如BaseAdapter,ArrayAdapter等)
做爲ListView和GridView的父類,AbsListView承擔了不少職責,下面咱們要分析的關於ListView的View複用機制便是經過該類的內部類RecycleBin完成的。
也就是說ListView和GridView使用的是同一種View複用機制,該機制主要是由二者的父類AbsListView中的內部類RecycleBin完成。別萬一被問到了GridView的View複用機制,GridView爲何展現成千上萬條數據不發生OOM等問題時傻了眼。。。。
注:如下源碼來自android-6.0.0_r5
/** *RecycleBin有助於在佈局中重用視圖。RecycleBin有兩個級別的存儲:ActiveViews和ScrapViews。 *ActiveViews是在佈局開始時出如今屏幕上的視圖。經過構造,它們顯示當前信息。 *在佈局的最後,ActiveViews中的全部視圖都被降級爲ScrapViews。 *ScrapViews是能夠被適配器使用的舊視圖,以免沒必要要地分配視圖。 * */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * 在mActiveViews中存儲的第一個View的位置. */ private int mFirstActivePosition; /** *在佈局開始時在屏幕上的視圖。這個數組在佈局開始時填充, *在佈局的末尾,mActiveViews中的全部視圖都被移動到mScrapViews *mActiveViews表示一個連續的視圖範圍,第一個視圖的位置存儲在mFirstActivePosition。 */ private View[] mActiveViews = new View[0]; /** *可將適配器用做轉換視圖的未排序視圖。 */ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; /** * RecycleBin當中使用mActiveViews這個數組來存儲View, * 調用這個方法後就會根據傳入的參數來將ListView中的指定元素存儲到mActiveViews數組當中。 * * @param childCount * 第一個參數表示要存儲的view的數量 * @param firstActivePosition * ListView中第一個可見元素的position值 */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { activeViews[i] = child; lp.scrappedFromPosition = firstActivePosition + i; } } } /** *該方法與上面的fillActiveViews對應,功能是獲取對應於指定位置的視圖。視圖若是被發現,就會從mActiveViews刪除 * * @param position * 表示元素在ListView當中的位置,方法內部會自動將position值轉換成mActiveViews數組對應的下標值。 * @return * 返回找到的View 下次獲取一樣位置的View將會返回null。 */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; } /** * 根據mViewTypeCount把一個View放進 ScapViews list. 這些View是未通過排序的. * * @param scrap * 須要被加入的View */ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } lp.scrappedFromPosition = position; final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<>(); } mTransientStateViews.put(position, scrap); } else { getSkippedScrap().add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } } /** * @return 根據mViewTypeCount從mScapViews中獲取 */ 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; } /** * @return 用於從ScapViews list中取出一個View,這些廢棄緩存中的View是沒有順序可言的, * 所以retrieveFromScrap方法中的算法也很是簡單,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回. */ private View retrieveFromScrap(ArrayList<View> scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. for (int i = 0; i < size; i++) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { final View scrap = scrapViews.remove(i); clearAccessibilityFromScrap(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearAccessibilityFromScrap(scrap); return scrap; } else { return null; } } /** *Adapter當中能夠重寫一個getViewTypeCount()來表示ListView中有幾種類型的數據項, *setViewTypeCount()方法的做用就是爲每種類型的數據項都單獨啓用一個RecycleBin緩存機制。 */ 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; } }
ListView雖然很複雜,可是其繼承自View,終究逃不過View的那5大過程,關於這部份內容讀者若是不清楚,可參看以前的博文,[Android開發之漫漫長途 Ⅴ——Activity的顯示之ViewRootImpl的預測量、窗口布局、最終測量、佈局、繪製]()
從以前的文章咱們就知道,View通過預測量、窗口布局(根據條件進入)、最終測量、佈局、繪製階段,那麼對於ListView也不例外,
在第一次「心跳」performTraversals()函數中,咱們會對ListView進行預測量、最終測量 2次測量,onMeasure()方法被調用兩次,1次佈局 onLayout()方法調用1次,
到最後調用draw方法咱們來看下面這段代碼
if (!cancelDraw && !newSurface) { if (!skipDraw || mReportNextDraw) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } //調用 performDraw(); performDraw(); } } else { if (viewVisibility == View.VISIBLE) { //調用 scheduleTraversals(); scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } }
從上面的代碼咱們能夠看出,,若是cancelDraw爲true或者newSurface爲true時,,會調用 scheduleTraversals();而這個函數會致使「心跳」performTraversals()函數的調用,再從新走一遍上面的過程
這也致使了
第1次「心跳」onMeasure() onMeasure() onLayout()
第2次「心跳」onMeasure() onLayout() onDraw()
有些讀者可能會問了,爲何第1次心跳是2次onMeasure() onMeasure(),第二次心跳是1次onMeasure呢,這跟View的measure機制有關
[View.java]
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ...... // 當FLAG_FORCE_LAYOUT位爲1時,就是當前視圖請求一次佈局操做 //或者當前當前widthSpec和heightSpec不等於上次調用時傳入的參數的時候 //才進行重新測量。 if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { ...... onMeasure(widthMeasureSpec, heightMeasureSpec); ...... } ...... }
也就是說對於任意一個View而言,想顯示到界面,至少通過兩次onMeasur及onLayout的調用
對於測量和繪製不是咱們這個ListView所關心的,咱們只關心它的佈局
上面說了半天,其實就是讓讀者對ListView的測量、佈局、繪製流程有個更深刻的瞭解,對於其餘View,咱們並不關心它進行了幾回Measure,幾回layout,可是對於ListView而言這個卻比較重要,由於ListView是在佈局過程當中向其中添加數據的,若是屢次佈局,那麼不就添加劇複數據了嗎?這個咱們能夠看到ListView巧妙的設計來避免了重複添加數據的問題。
談到layout,相信讀者也都瞭然一笑,確定看onLayout方法,結果發現ListView中沒有此方法,。不着急,咱們去它爸爸,果真在它爸爸那裏找到了
[AbsListView.java]
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mInLayout = true; final int childCount = getChildCount(); if (changed) { for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mInLayout = false; mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; // TODO: Move somewhere sane. This doesn't belong in onLayout(). if (mFastScroll != null) { mFastScroll.onItemCountChanged(getChildCount(), mItemCount); } }
上面的代碼很短,也不復雜,複雜的操做被封裝在layoutChildren();方法中了,跟進
/** * Subclasses must override this method to layout their children. */ protected void layoutChildren() { }
空方法。
看其說明應該是子類重寫了該方法了,,咱們又回到ListView
[ListView.java]
@Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (blockLayoutRequests) { return; } mBlockLayoutRequests = true; try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } final int childrenTop = mListPadding.top; final int childrenBottom = mBottom - mTop - mListPadding.bottom; /** *第1次Layout時,ListView內尚未數據,數據還在Adapter那,這裏childCount=0 */ final int childCount = getChildCount(); int index = 0; int delta = 0; ...... /** *dataChanged只有在數據源發生改變的狀況下才會變成true,其它狀況都是false, */ boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } ...... 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()方法緩存View *但是目前ListView中尚未任何的子View,所以這一行暫時還起不了任何做用。 */ recycleBin.fillActiveViews(childCount, firstPosition); } /** *目前ListView中尚未任何的子View,所以這一行暫時還起不了任何做用。 */ detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); /** *mLayoutMode默認爲LAYOUT_NORMAL */ switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { 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) { /** *mStackFromBottom咱們在上面的屬性中講過,該屬性默認爲false */ final int position = lookForSelectablePosition(0, true); setSelectedPositionInt(position); //調用fillFromTop方法 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; } /** *RecycleBin的scrapActiveViews從mActivieViews緩存到mScrapActiveViews *但是目前RecycleBin的mActivieViews也沒什麼數據,所以這一行暫時還起不了任何做用。 */ recycleBin.scrapActiveViews(); ...... } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } }
第1次layout時就是作一些初始化ListView的操做,調用fillFromTop方法去填充ListView,跟進fillFromTop
[ListView.java]
/** *參數nextTop表示下一個子View應該放置的位置, *這裏傳入的nextTop=mListPadding.top;明顯第一個子View是放在ListView的最上方, *注意padding不屬於子View,屬於父View的一部分 */ private View fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } //調用fillDown return fillDown(mFirstPosition, nextTop); }
跟進fillDown
[ListView.java]
/** * 從pos開始從上向下填充ListViwe * * @param pos * list中的位置 * * @param nextTop * 下一個Item應該放置的位置 * * @return * 返回選擇位置的View,這個位置在咱們的放置範圍以內 */ private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (mBottom - mTop); if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom; } /** *這裏循環判斷,終止的條件是下一個item放置的位置>listview的底部或者當前的位置>總數 */ while (nextTop < end && pos < mItemCount) { boolean selected = pos == mSelectedPosition; /** *這裏調用makeAndAddView來得到一個View */ View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); //更新nextTop的值 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } //自增pos pos++; } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }
繼續跟進makeAndAddView
[ListView.java]
/** * 獲取一個View並添加進ListView。這個View呢多是新建立的,也有多是來自mActiveViews,或者是來自mScrapViews * @param position * 列表中的邏輯位置 * @param y * 被添加View的上 邊位置或者下 邊位置 * @param flow * 若是flow是true,那麼y是View的上 邊位置,不然那麼y是View的下 邊位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return * 返回被添加的View */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // 嘗試從mActiveViews中獲取,第1次確定是沒有獲取到 child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // 爲這個position建立View child = obtainView(position, mIsScrap); // 調用setupChild setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
咱們第1次layout時,嘗試從mActiveViews中獲取View,這裏child爲null,那麼咱們跟進obtainView
咱們沒有在ListView中找到該方法,那麼應該在其父類中,,跟進
[AbsListView.java]
/** * 獲取一個視圖,並讓它顯示與指定的數據相關聯的數據的位置。 *當咱們已經發現視圖沒法在RecycleBin重複使用。剩下的惟一選擇就是轉換舊視圖或建立新視圖。 * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the scrap heap, false if otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } 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 { isScrap[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }
使用obtainView必定會獲得一個View,這個View要麼是新建立的,要麼是從mScrapViews中複用的。在第1次layout中,確定沒法獲得複用的View,那麼新建立的,
final View child = mAdapter.getView(position, scrapView, this);
這裏的mAdapter毫無疑問是跟ListView關聯的Adapter,那麼getView方法,讀者應該想到了,就是咱們須要重寫的那個方法
public class ListViewAdapter extends BaseAdapter { /** *convertView = scrapView在第1次layout中,scrapView爲null *parent = this 即ListView自己 */ @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null){ viewHolder = new ViewHolder(); convertView = inflater.inflate(R.layout.list_view_item_simple, null); viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view); convertView.setTag(viewHolder); Log.d(TAG,"convertView == null"); }else { Log.d(TAG,"convertView != null"); viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.mTextView.setText(mList.get(position)); return convertView; } static class ViewHolder { private TextView mTextView; } }
如今咱們對這個方法有了更深刻的認識,convertView就是咱們獲得的在mScrapViews中的View,因此咱們在getView中判斷convertView是否爲null,若是爲null,經過LayoutInfalter加載。
那麼到這裏咱們就須要回到
[ListView.java]
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; ...... // view咱們這裏已經獲得了 child = obtainView(position, mIsScrap); // 調用setupChild setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
接着跟進setupChild
[ListView.java]
/** * 添加一個子View而後肯定該子View的測量(若是必要的話)和合理的位置 * * @param child * 被添加的子View * @param position * 子View的位置 * @param y * 子View被放置的座標 * @param flowDown * 若是是true的話,那麼上面參數中的y表示子View的上 邊的位置,不然爲下 邊的位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled 布爾值,意爲child是不是從RecycleBin中獲得的,若是是的話,不須要從新Measure * 第1次layout時,該值爲false */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { if (child instanceof Checkable) { ((Checkable) child).setChecked(mCheckStates.get(position)); } else if (getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { child.setActivated(mCheckStates.get(position)); } } if (needToMeasure) { final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); final int lpHeight = p.height; final int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childTop = flowDown ? y : y - h; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) != position) { child.jumpDrawablesToCurrentState(); } Trace.traceEnd(Trace.TRACE_TAG_VIEW); }
這裏調用了addViewInLayout()方法將它添加到了ListView當中。那麼根據fillDown()方法中的while循環,會讓子元素View將整個ListView控件填滿而後就跳出,也就是說即便咱們的Adapter中有一千條數據,ListView也只會加載第一屏的數據,剩下的數據反正目前在屏幕上也看不到,因此不會去作多餘的加載工做,這樣就能夠保證ListView中的內容可以迅速展現到屏幕上。
那麼到此爲止,第一次Layout過程結束。
也就是說,ListView的第1次layout中,只是填充ListView的子View,即便咱們的Adapter中有一千條數據,ListView也只會加載第一屏的數據,並不涉及RecycleBin的運做
[ListView.java]
@Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (blockLayoutRequests) { return; } mBlockLayoutRequests = true; try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } final int childrenTop = mListPadding.top; final int childrenBottom = mBottom - mTop - mListPadding.bottom; /** *第2次Layout時,ListView中已經有了數據,這裏的childCount爲ListView一屏View的數目 */ final int childCount = getChildCount(); int index = 0; int delta = 0; ...... /** *dataChanged只有在數據源發生改變的狀況下才會變成true,其它狀況都是false, */ boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } ...... 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()方法緩存View *第2次Layout時,ListView中已經有了數據, *調用fillActiveViews(),那麼此時RecycleBin中mActiveViews中已有了數據 */ recycleBin.fillActiveViews(childCount, firstPosition); } /** *第2次Layout時,ListView中已經有了一屏的子View, *調用detachAllViewsFromParent();把ListView中的全部子View detach了 *這也是屢次調用layout不會重複添加數據的緣由 */ detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); /** *mLayoutMode默認爲LAYOUT_NORMAL */ switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { 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 { /** *第2次Layout時childCount不爲0 */ 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; } /** *RecycleBin的scrapActiveViews方法把mActivieViews中的View再緩存到mScrapActiveViews */ recycleBin.scrapActiveViews(); ...... } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } }
其實第二次Layout和第一次Layout的基本流程是差很少的,
第2次Layout時childCount不爲0,咱們進入了
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); }
第一個邏輯判斷不成立,由於默認狀況下咱們沒有選中任何子元素,mSelectedPosition應該等於-1。第二個邏輯判斷一般是成立的,由於mFirstPosition的值一開始是等於0的,只要adapter中的數據大於0條件就成立。那麼進入到fillSpecific()方法當中,代碼以下所示:
/** *它和fillUp()、fillDown()方法功能也是差很少的, *主要的區別在於,fillSpecific()方法會優先將指定位置的子View先加載到屏幕上, *而後再加載該子View往上以及往下的其它子View。 *那麼因爲這裏咱們傳入的position就是第一個子View的位置, *因而fillSpecific()方法的做用就基本上和fillDown()方法是差很少的了 * * @param position The reference view to use as the starting point * @param top Pixel offset from the top of this view to the top of the * reference view. * * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int top) { boolean tempIsSelected = position == mSelectedPosition; View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = position; View above; View below; final int dividerHeight = mDividerHeight; if (!mStackFromBottom) { above = fillUp(position - 1, temp.getTop() - dividerHeight); // This will correct for the top of the first view not touching the top of the list adjustViewsUpOrDown(); below = fillDown(position + 1, temp.getBottom() + dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooHigh(childCount); } } else { below = fillDown(position + 1, temp.getBottom() + dividerHeight); // This will correct for the bottom of the last view not touching the bottom of the list adjustViewsUpOrDown(); above = fillUp(position - 1, temp.getTop() - dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooLow(childCount); } } if (tempIsSelected) { return temp; } else if (above != null) { return above; } else { return below; } }
那麼咱們第3次回到makeAndAddView
[ListView.java]
/** * 獲取一個View並添加進ListView。這個View呢多是新建立的,也有多是來自mActiveViews,或者是來自mScrapViews * @param position * 列表中的邏輯位置 * @param y * 被添加View的上 邊位置或者下 邊位置 * @param flow * 若是flow是true,那麼y是View的上 邊位置,不然那麼y是View的下 邊位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return * 返回被添加的View */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // 嘗試從mActiveViews中獲取,第2次layout中child能夠從mActiveViews中獲取到 child = mRecycler.getActiveView(position); if (child != null) { //這裏child不爲空,調用setupChild,注意最後一個參數爲true setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // 爲這個position建立View child = obtainView(position, mIsScrap); // 調用setupChild setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
咱們這裏又調用了setupChild,跟上面不一樣的是最後一個參數
[ListView.java]
/** * 添加一個子View而後肯定該子View的測量(若是必要的話)和合理的位置 * * @param child * 被添加的子View * @param position * 子View的位置 * @param y * 子View被放置的座標 * @param flowDown * 若是是true的話,那麼上面參數中的y表示子View的上 邊的位置,不然爲下 邊的位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled 布爾值,意爲child是不是從RecycleBin中獲得的,若是是的話,不須要從新Measure * 第2次layout時,該值爲true */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { if (child instanceof Checkable) { ((Checkable) child).setChecked(mCheckStates.get(position)); } else if (getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { child.setActivated(mCheckStates.get(position)); } } if (needToMeasure) { final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); final int lpHeight = p.height; final int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childTop = flowDown ? y : y - h; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) != position) { child.jumpDrawablesToCurrentState(); } Trace.traceEnd(Trace.TRACE_TAG_VIEW); }
因爲recycled如今是true,因此會執行attachViewToParent()方法,而第一次Layout過程則是執行的else語句中的addViewInLayout()方法。這兩個方法最大的區別在於,若是咱們須要向ViewGroup中添加一個新的子View,應該調用addViewInLayout()方法,而若是是想要將一個以前detach的View從新attach到ViewGroup上,就應該調用attachViewToParent()方法。那麼因爲前面在layoutChildren()方法當中調用了detachAllViewsFromParent()方法,這樣ListView中全部的子View都是處於detach狀態的,因此這裏attachViewToParent()方法是正確的選擇。
經歷了這樣一個detach又attach的過程,ListView中全部的子View又均可以正常顯示出來了,那麼第二次Layout過程結束。
也就是說,ListView的第2次layout中,把ListView中的全部子View緩存到RecycleBin中的mActiveViews,而後再detach掉ListView中全部子View,接着attach回來(這時使用的是mActiveViews中的緩存,沒有從新inflate),而後再把mActiveViews緩存到mScrapViews中(還記得RecycleBin中的getActiveView方法嗎,咱們是怎麼描述這個方法的,功能是獲取對應於指定位置的視圖。視圖若是被發現,就會從mActiveViews刪除,也就是說不能從同一個位置的View不能從mActiveViews中得到第二次)
經歷了兩次Layout過程,雖然說咱們已經能夠在ListView中看到內容了,然而關於ListView最神奇的部分咱們卻尚未接觸到,由於目前ListView中只是加載並顯示了第一屏的數據而已。關於觸摸事件的分發機制,讀者不太清楚的可參看前面的博文Android開發之漫漫長途 Ⅵ——圖解Android事件分發機制(深刻底層源碼)
咱們這裏直接來看onTouchEvent
[AbsListView.java]
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (actionMasked) { ...... case MotionEvent.ACTION_MOVE: { onTouchMove(ev, vtev); break; } ...... } ...... return true; }
其餘的咱們一律不關心,徑直找到 MotionEvent.ACTION_MOVE當手指在屏幕上滑動時,TouchMode是等於TOUCH_MODE_SCROLL這個值的,
[AbsListView.java]
private void onTouchMove(MotionEvent ev, MotionEvent vtev) { ...... if (mDataChanged) { layoutChildren(); } final int y = (int) ev.getY(pointerIndex); switch (mTouchMode) { ...... case TOUCH_MODE_SCROLL: case TOUCH_MODE_OVERSCROLL: scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); break; } }
跟進scrollIfNeeded
[AbsListView.java]
private void scrollIfNeeded(int x, int y, MotionEvent vtev) { int rawDeltaY = y - mMotionY;//上次觸摸事件到這次觸摸事件移動的距離 ...... if (mLastY == Integer.MIN_VALUE) { rawDeltaY -= mMotionCorrection; } ...... //若是滑動須要滑動的距離 final int deltaY = rawDeltaY; int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY; int lastYCorrection = 0; if (mTouchMode == TOUCH_MODE_SCROLL) { ...... if (y != mLastY) {//這次觸摸事件和上次觸摸事件的y值發生了改變(須要滑動的距離>0) // We may be here after stopping a fling and continuing to scroll. // If so, we haven't disallowed intercepting touch events yet. // Make sure that we do so in case we're in a parent that can intercept. // 當中止一個拋動且繼續滑動以後,咱們可能會執行此處的代碼 //確保ListView的父視圖不會攔截觸摸事件 if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && Math.abs(rawDeltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } final int motionIndex;//down手勢事件,按住的子視圖在ListView之中的位置 if (mMotionPosition >= 0) { motionIndex = mMotionPosition - mFirstPosition; } else { // If we don't have a motion position that we can reliably track, // pick something in the middle to make a best guess at things below. motionIndex = getChildCount() / 2; } int motionViewPrevTop = 0;//down手勢事件,按住的子視圖的頂端位置 View motionView = this.getChildAt(motionIndex); if (motionView != null) { motionViewPrevTop = motionView.getTop(); } // No need to do all this work if we're not going to move anyway //不須要作全部的工做,若是咱們並無進行移動 boolean atEdge = false;//是否到達了ListView的邊緣 if (incrementalDeltaY != 0) { atEdge = trackMotionScroll(deltaY, incrementalDeltaY);//追蹤手勢滑動 } // Check to see if we have bumped into the scroll limit //查看咱們是否撞到了滑動限制(邊緣) motionView = this.getChildAt(motionIndex); if (motionView != null) { // Check if the top of the motion view is where it is // supposed to be final int motionViewRealTop = motionView.getTop(); if (atEdge) {//到達了ListView的邊緣 // Apply overscroll //響應的回彈效果實現 ...... } mMotionY = y + lastYCorrection + scrollOffsetCorrection;//更新 } mLastY = y + lastYCorrection + scrollOffsetCorrection;//更新 } } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { ...... } }
整體而言,這一步也算是一個外殼,真正跟蹤滑動運行的是trackMotionScroll方法。trackMotionScroll方法的邏輯較爲複雜;
[AbsListView.java]
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { ...... //是否從下往上滑動 final boolean down = incrementalDeltaY < 0; ...... if (down) {//從下往上移動 int top = -incrementalDeltaY;//移動的距離 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); //是否有子視圖徹底被滑動離開了ListView的可見範圍 if (child.getBottom() >= top) { break; } else {//當前子視圖 count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position);//回收子視圖 } } } } else { ...... } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) {//若是存在徹底離開了ListView可視範圍的子視圖 detachViewsFromParent(start, count);//將這些徹底離開了但是範圍的子視圖所有刪掉 mRecycler.removeSkippedScrap();//從視圖重用池中刪除須要丟棄的視圖 } //將未刪除的全部的子視圖朝上(下)移動incrementalDeltaY這麼多距離 offsetChildrenTopAndBottom(incrementalDeltaY); //更新第一個子視圖對應的item在適配器中的位置 if (down) { mFirstPosition += count; } final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); //若是還有可移動的範圍 if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { //由於有可能有一些子視圖徹底離開了ListView範圍,全部須要從新加載新的item來填充ListView的空白 fillGap(down); } ...... return false; }
這部分過程以下圖
fillGap在AbsListView.java是個抽象方法,那麼顯然在子類中找其重寫的具體實現方法
abstract void fillGap(boolean down);
[ListView.java]
//這裏參數down應該是true,咱們是從上向下滑動 @Override void fillGap(boolean down) { final int count = getChildCount(); if (down) { int paddingTop = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { paddingTop = getListPaddingTop(); } final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : paddingTop; //看到仍是調用fillDown方法 fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { int paddingBottom = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { paddingBottom = getListPaddingBottom(); } final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - paddingBottom; fillUp(mFirstPosition - 1, startOffset); correctTooLow(getChildCount()); } }
咱們再次來看fillDown方法
[ListView.java]
/** * 從pos開始從上向下填充ListViwe * * @param pos * list中的位置 * * @param nextTop * 下一個Item應該放置的位置 * * @return * 返回選擇位置的View,這個位置在咱們的放置範圍以內 */ private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (mBottom - mTop); if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom; } /** *這裏循環判斷,終止的條件是下一個item放置的位置>listview的底部或者當前的位置>總數 */ while (nextTop < end && pos < mItemCount) { boolean selected = pos == mSelectedPosition; /** *這裏調用makeAndAddView來得到一個View */ View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); //更新nextTop的值 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } //自增pos pos++; } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }
fillDown邏輯並無什麼變化,再次makeAndAddView
[ListView.java]
/** * 獲取一個View並添加進ListView。這個View呢多是新建立的,也有多是來自mActiveViews,或者是來自mScrapViews * @param position * 列表中的邏輯位置 * @param y * 被添加View的上 邊位置或者下 邊位置 * @param flow * 若是flow是true,那麼y是View的上 邊位置,不然那麼y是View的下 邊位置 * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return * 返回被添加的View */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // 嘗試從mActiveViews中獲取,由於咱們第2次layout中已經從mActiveViews中獲取到View了,,因此此次獲取的爲null child = mRecycler.getActiveView(position); if (child != null) { //這裏child不爲空,調用setupChild,注意最後一個參數爲true setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // 爲這個position建立View child = obtainView(position, mIsScrap); // 調用setupChild setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
接着咱們又來到了obtainView,不誇張的說,整個ListView中最重要的內容可能就在這個方法裏了
[ListView.java]
/** * 獲取一個視圖,並讓它顯示與指定的數據相關聯的數據的位置。 *當咱們已經發現視圖沒法在RecycleBin重複使用。剩下的惟一選擇就是轉換舊視圖或建立新視圖。 * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the scrap heap, false if otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } 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 { isScrap[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }
咱們能夠看到這句
final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this);
RecyleBin的getScrapView()方法來嘗試從廢棄緩存中獲取一個View,那麼廢棄緩存有沒有View呢?固然有,由於剛纔在trackMotionScroll()方法中咱們就已經看到了,一旦有任何子View被移出了屏幕,就會將它加入到廢棄緩存中,而從obtainView()方法中的邏輯來看,一旦有新的數據須要顯示到屏幕上,就會嘗試從廢棄緩存中獲取View。因此它們之間就造成了一個生產者和消費者的模式,那麼ListView神奇的地方也就在這裏體現出來了,無論你有任意多條數據須要顯示,ListView中的子View其實來來回回就那麼幾個,移出屏幕的子View會很快被移入屏幕的數據從新利用起來,於是無論咱們加載多少數據都不會出現OOM的狀況,甚至內存都不會有所增長。
那麼另外還有一點是須要你們留意的,這裏獲取到了一個scrapView,而後咱們在第上述代碼中第2行將它做爲第二個參數傳入到了Adapter的getView()方法當中。咱們再次來看咱們重寫的getView方法
public class ListViewAdapter extends BaseAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null){ viewHolder = new ViewHolder(); convertView = inflater.inflate(R.layout.list_view_item_simple, null); viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view); convertView.setTag(viewHolder); Log.d(TAG,"convertView == null"); }else { Log.d(TAG,"convertView != null"); viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.mTextView.setText(mList.get(position)); return convertView; } static class ViewHolder { private TextView mTextView; } }
此次convertView不爲null了,最後返回了convertView
這下你完全明白了嗎??
最後再上張圖
本篇呢,分析詳細介紹了ListView及其View複用機制,文中如有不正確或者不恰當的地方,歡迎各位讀者前來拍磚。
下篇呢咱們把ListView換成RecyclerView
http://blog.csdn.net/guolin_blog/article/details/44996879
此致,敬禮