何时调用getView?——从源码的角度给出解答
先来看ListView类中的makeAndAddView方法:
没有数据变化:从mRecycler中取得可视的view
数据有变化:obtainView
1 /** 2 * 获取视图填充到列表的item中去,视图可以是从未使用过的视图转换过来,也可以是从回收站复用的视图。 3 * 在该方法中,先查找是否有可重用视图,如果有,使用可重用视图。 4 * 然后通过obtainView方法获取一个view(有可能是从未使用视图转换过来 5 * (obtainView方法是在AbsListView方法中定义)),再重新测量和定位View。 6 */ 7 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 8 boolean selected) { 9 View child; 10 // 没有数据变化:从mRecycler中取得可视的view 11 if (!mDataChanged) { 12 // Try to use an existing view for this position 13 child = mRecycler.getActiveView(position); 14 ... 15 } 16 // 生成view,回收旧view和调用mApapter.getView的地方(AbsListView) 17 child = obtainView(position, mIsScrap); 18 ... 19 return child; 20 }
第11行调用了obtainView方法,该方法的实现是在package android.widget;的AbsListView类中
1 View obtainView(int position, boolean[] isScrap) { 2 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); 3 4 isScrap[0] = false; 5 View scrapView; 6 7 scrapView = mRecycler.getTransientStateView(position); 8 if (scrapView == null) { 9 // 从回收站回收一个View 10 scrapView = mRecycler.getScrapView(position); 11 } 12 13 View child; 14 if (scrapView != null) { 15 // 这里调用了getView!注意,第二个参数也就是convertView,传入的是刚才从回收站中回收的View(如果有的话) 16 child = mAdapter.getView(position, scrapView, this); 17 ... 18 return child; 19 }
第16行调用了getView!根据Java多态的特性,实际执行的getView将会是我们自定义BaseAdapter中的那个getView方法。
好,现在虽然找到getView的直接调用者了,问题来了,何时去触发makeAndAddView并调用getView呢?
我们首先来看ListView中的fillDown:自顶至底去填充ListView
1 /** 2 填充从pos到list底部所有的item。里面调用到了makeAndAddView方法: 3 */ 4 private View fillDown(int pos, int nextTop) { 5 ...
6 // 这里调用到了makeAndAddView方法 7 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); 8 ... 9 return selectedView; 10 }
有fillDown自然就有fillUp:
1 private View fillUp(int pos, int nextBottom) { 2 View selectedView = null; 3 ... 4 // 调用makeAndAddView 5 View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); 6 ... 7 return selectedView; 8 }
还有fillFromTop、fillFromMiddle、fillAboveAndBelow、fillFromSelection等,这些方法都是用来进行子元素布局的,区别是布局模式不同而已。
好了,现在布局子元素的方法有了,那么谁来触发这些方法呢?
通过查找ListView源码,发现刚才的那些方法在layoutChildren()中基本上都出现了。
1 @Override 2 protected void layoutChildren() { 3 ... 4 // 根据mLayoutMode的值来决定布局模式 5 switch (mLayoutMode) { 6 case LAYOUT_SET_SELECTION: 7 if (newSel != null) { 8 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 9 } else { 10 sel = fillFromMiddle(childrenTop, childrenBottom); 11 } 12 break; 13 case LAYOUT_SYNC: 14 sel = fillSpecific(mSyncPosition, mSpecificTop); 15 break; 16 case LAYOUT_FORCE_BOTTOM: 17 sel = fillUp(mItemCount - 1, childrenBottom); 18 adjustViewsUpOrDown(); 19 break; 20 case LAYOUT_FORCE_TOP: 21 mFirstPosition = 0; 22 sel = fillFromTop(childrenTop); 23 adjustViewsUpOrDown(); 24 break; 25 case LAYOUT_SPECIFIC: 26 sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); 27 break; 28 case LAYOUT_MOVE_SELECTION: 29 sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); 30 break; 31 default:// 默认的布局顺序是从上往下 32 if (childCount == 0) { 33 if (!mStackFromBottom) { 34 final int position = lookForSelectablePosition(0, true); 35 setSelectedPositionInt(position); 36 sel = fillFromTop(childrenTop); 37 } else { 38 final int position = lookForSelectablePosition(mItemCount - 1, false); 39 setSelectedPositionInt(position); 40 sel = fillUp(mItemCount - 1, childrenBottom); 41 } 42 } else { 43 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 44 sel = fillSpecific(mSelectedPosition, 45 oldSel == null ? childrenTop : oldSel.getTop()); 46 } else if (mFirstPosition < mItemCount) { 47 sel = fillSpecific(mFirstPosition, 48 oldFirst == null ? childrenTop : oldFirst.getTop()); 49 } else { 50 sel = fillSpecific(0, childrenTop); 51 } 52 } 53 break; 54 } 55 56 ... 57 }
继续查找,我们发现layoutChildren的调用者是onFocusChanged、setSelectionInt、父类AbsListView中的onTouchMove、onLayout(这个比较特殊,后文会说明)等,说明当ListView的焦点发生变化时、选中某一项、或者滑动ListView时都会触发ListView的layoutChildren()去布局子元素。
到此为止我们已经很清楚getView的调用时机了,根据掌握的知识点,我们很自然能想到,当初始化一个ListView时,getView的调用也是避免不了的。这是因为ListView在初始化时肯定会绑定一个adapter,即调用语句listview.setAdapter(adapter),我们看一下setAdapter的源码:
1 @Override 2 public void setAdapter(ListAdapter adapter) { 3 if (mAdapter != null && mDataSetObserver != null) { 4 mAdapter.unregisterDataSetObserver(mDataSetObserver); 5 } 6 // 去除原有adapter、观察者、选中项等信息 7 resetList(); 8 mRecycler.clear(); 9 // 包装adapter,加header或footer,并绑定到当前ListView 10 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { 11 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); 12 } else { 13 mAdapter = adapter; 14 } 15 // 重置选中项信息 16 mOldSelectedPosition = INVALID_POSITION; 17 mOldSelectedRowId = INVALID_ROW_ID; 18 19 // AbsListView#setAdapter will update choice mode states. 20 super.setAdapter(adapter); 21 22 if (mAdapter != null) { 23 mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); 24 mOldItemCount = mItemCount; 25 mItemCount = mAdapter.getCount(); 26 checkFocus(); 27 // 重新注册观察者 28 mDataSetObserver = new AdapterDataSetObserver(); 29 mAdapter.registerDataSetObserver(mDataSetObserver); 30 // 设置回收器中类型不同的View数目,这里与getView的回收机制紧密相关,值得深究 31 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 32 // 设置初始选中项 33 int position; 34 if (mStackFromBottom) { 35 position = lookForSelectablePosition(mItemCount - 1, false); 36 } else { 37 position = lookForSelectablePosition(0, true); 38 } 39 setSelectedPositionInt(position); 40 setNextSelectedPositionInt(position); 41 42 if (mItemCount == 0) { 43 // Nothing selected 44 checkSelectionChanged(); 45 } 46 } else { 47 mAreAllItemsSelectable = true; 48 checkFocus(); 49 // Nothing selected 50 checkSelectionChanged(); 51 } 52 // 请求布局重绘 53 requestLayout(); 54 }
通读setAdapter源码,我们发现其中并未出现生成新子视图,即调用mAdapter.getView的语句或相关方法,说明此时ListView并未包含子视图。那么疑问来了,ListView是如何在初始化的时候生成子视图的,也就是说第一屏的视图是如何加载到屏幕上的?往后看,我们发现在第53行调用了requestLayout请求布局重绘,我们知道requestLayout最终会去调用onMeasure、onLayout、onDraw方法,因此我们猜测会不会是在onMeasure、onLayout、onDraw某个方法中生成了子视图?
答案是肯定的,AbsListVIew.onLayout过程与普通视图的layout过程完全不同,如下:
1 protected void onLayout(boolean changed, int l, int t, int r, int b) { 2 super.onLayout(changed, l, t, r, b); 3 ... 4 layoutChildren(); 5 ... 6 }
该方法调用了layoutChildren();,即重新布局ListView列表视图。
由此说明调用requestLayout可以实现ListView列表视图的重新布局,这里联想到adapter.notifyDataSetChanged也会调用requestLayout,从而都能实现ListView的刷新。
以上过程只是个人探索,并非绝对正确,如有差错敬请批评指正,谢谢。
参考文献: