何时调用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的刷新。

 以上过程只是个人探索,并非绝对正确,如有差错敬请批评指正,谢谢。

 参考文献:

Android ListView初始化简单分析

Android ListView工作原理完全解析,带你从源码的角度彻底理解

android源码解析--ListView(上)

ListView源代码分析

原文地址:https://www.cnblogs.com/nailperry/p/4671951.html