一、前述
通常,我们在使用ListView / GridView时,都需要使用Adapter,Adapter有多种,最常用的就是BaseAdapter 和 CursorAdapter了,前者是属于通用的,而后者通常会与数据库一起来使用。
1. Adapter在源码中是interface,而不是AbstractClass(http://developer.android.com/reference/android/widget/Adapter.html);
2. BaseAdapter是抽象类,而不是interface;
3. CursorAdapter也是抽象类,它是继承于BaseAdapter的;
4. 还有其它Adapter,如ListAdapter等;
大家看了以上几点后,肯定会有疑问,BaseAdapter与Adapter是怎么联系起来的?
看过文档或源码的朋友肯定知道:
/** * Common base class of common implementation for an [email protected] Adapter} that can be * used in both [email protected] ListView} (by implementing the specialized * [email protected] ListAdapter} interface} and [email protected] Spinner} (by implementing the * specialized [email protected] SpinnerAdapter} interface. */ public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter
ListAdapter和SpinnerAdapter是两个interface,它们直接继承于Adapter,BaseAdapter是抽象类,可以选择性的实现接口里定义的方法;
其实,BaseAdapter也是大多数XXXAdapter(不是接口,而是类或抽象类)的parent,只有少数和BaseAdapter一样,如:HeaderViewListAdapter。
二、Adapter
2.1 Adapter
public interface Adapter { /** * Register an observer that is called when changes happen to the data used by this adapter. * * @param observer the object that gets notified when the data set changes. */ void registerDataSetObserver(DataSetObserver observer); /** * Unregister an observer that has previously been registered with this * adapter via [email protected] #registerDataSetObserver}. * * @param observer the object to unregister. */ void unregisterDataSetObserver(DataSetObserver observer); /** * How many items are in the data set represented by this Adapter. * * @return Count of items. */ int getCount(); /** * Get the data item associated with the specified position in the data set. * * @param position Position of the item whose data we want within the adapter's * data set. * @return The data at the specified position. */ Object getItem(int position); /** * Get the row id associated with the specified position in the list. * * @param position The position of the item within the adapter's data set whose row id we want. * @return The id of the item at the specified position. */ long getItemId(int position); /** * Indicates whether the item ids are stable across changes to the * underlying data. * * @return True if the same id always refers to the same object. */ boolean hasStableIds(); /** * Get a View that displays the data at the specified position in the data set. You can either * create a View manually or inflate it from an XML layout file. When the View is inflated, the * parent View (GridView, ListView...) will apply default layout parameters unless you use * [email protected] android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)} * to specify a root view and to prevent attachment to the root. * * @param position The position of the item within the adapter's data set of the item whose view * we want. * @param convertView The old view to reuse, if possible. Note: You should check that this view * is non-null and of an appropriate type before using. If it is not possible to convert * this view to display the correct data, this method can create a new view. * Heterogeneous lists can specify their number of view types, so that this View is * always of the right type (see [email protected] #getViewTypeCount()} and * [email protected] #getItemViewType(int)}). * @param parent The parent that this view will eventually be attached to * @return A View corresponding to the data at the specified position. */ View getView(int position, View convertView, ViewGroup parent); /** * An item view type that causes the [email protected] AdapterView} to ignore the item * view. For example, this can be used if the client does not want a * particular view to be given for conversion in * [email protected] #getView(int, View, ViewGroup)}. * * @see #getItemViewType(int) * @see #getViewTypeCount() */ static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; /** * Get the type of View that will be created by [email protected] #getView} for the specified item. * * @param position The position of the item within the adapter's data set whose view type we * want. * @return An integer representing the type of View. Two views should share the same type if one * can be converted to the other in [email protected] #getView}. Note: Integers must be in the * range 0 to [email protected] #getViewTypeCount} - 1. [email protected] #IGNORE_ITEM_VIEW_TYPE} can * also be returned. * @see #IGNORE_ITEM_VIEW_TYPE */ int getItemViewType(int position); /** * <p> * Returns the number of types of Views that will be created by * [email protected] #getView}. Each type represents a set of views that can be * converted in [email protected] #getView}. If the adapter always returns the same * type of View for all items, this method should return 1. * </p> * <p> * This method will only be called when when the adapter is set on the * the [email protected] AdapterView}. * </p> * * @return The number of types of Views that will be created by this adapter */ int getViewTypeCount(); static final int NO_SELECTION = Integer.MIN_VALUE; /** * @return true if this adapter doesn't contain any data. This is used to determine * whether the empty view should be displayed. A typical implementation will return * getCount() == 0 but since getCount() includes the headers and footers, specialized * adapters might want a different behavior. */ boolean isEmpty();}
这里面只定义了方法,其中:
void registerDataSetObserver(DataSetObserver observer); void unregisterDataSetObserver(DataSetObserver observer);
这两个方法我们通常不需要太关心,因为ListView, GridView会自动去注册/取消注册的,DataSetObserver用的是一个观察者模式,目的就是当Adapter中的数据发生变化是,能够通知ListView 或 GridView。
int getCount(); Object getItem(int position); long getItemId(int position); View getView(int position, View convertView, ViewGroup parent);
当我们继承BaseAdapter时,通常需要实现以上四个方法,这四个方法大家应该用的很熟了,这里我就不说了;
int getItemViewType(int position); int getViewTypeCount();
特别要注意的是这两个方法,通常在BaseAdapter里面,默认是0和1,那我们在什么时候会需要override这两个方法呢?
我们知道Adapter.getView中有个convertView,是用来复用View的,如果数据要进行分组,比如按照手机中的联系人按ABCD....来分类,那么,这个Listview就有两种view,一种是分组标签,另一种就是联系人,因此,我们需要设置ViewTypeCount为2,同时将分组view和联系人view设置不同的view type,这样,复用时,就能区分开来,有效的节省内存。
2.2 ListAdapter
public interface ListAdapter extends Adapter { /** * Indicates whether all the items in this adapter are enabled. If the * value returned by this method changes over time, there is no guarantee * it will take effect. If true, it means all items are selectable and * clickable (there is no separator.) * * @return True if all items are enabled, false otherwise. * * @see #isEnabled(int) */ public boolean areAllItemsEnabled(); /** * Returns true if the item at the specified position is not a separator. * (A separator is a non-selectable, non-clickable item). * * The result is unspecified if position is invalid. An [email protected] ArrayIndexOutOfBoundsException} * should be thrown in that case for fast failure. * * @param position Index of the item * * @return True if the item is not a separator * * @see #areAllItemsEnabled() */ boolean isEnabled(int position);}
这里面的方法就两个,一个是判断在Adapter中所有的item可选择或可点击,另一个是指点位置的item可选择或可点击,没啥可讲的;
2.3 SpinnerAdapter
这个是给下拉列表控件用的,就一个方法getDropDownView,其它没啥好讲的;
2.4 BaseAdapter
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { private final DataSetObservable mDataSetObservable = new DataSetObservable(); public boolean hasStableIds() { return false; } public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); } public void unregisterDataSetObserver(DataSetObserver observer) { mDataSetObservable.unregisterObserver(observer); } /** * Notifies the attached observers that the underlying data has been changed * and any View reflecting the data set should refresh itself. */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } /** * Notifies the attached observers that the underlying data is no longer valid * or available. Once invoked this adapter is no longer valid and should * not report further data set changes. */ public void notifyDataSetInvalidated() { mDataSetObservable.notifyInvalidated(); } public boolean areAllItemsEnabled() { return true; } public boolean isEnabled(int position) { return true; } public View getDropDownView(int position, View convertView, ViewGroup parent) { return getView(position, convertView, parent); } public int getItemViewType(int position) { return 0; } public int getViewTypeCount() { return 1; } public boolean isEmpty() { return getCount() == 0; }}
BaseAdapter也没啥讲的,我们继承时,只用实现它未实现的方法就行了,有需要的话,override一下也行。
三、RecycleBin
在AbsListView(ListView / GridView)中,View复用全是依赖RecycleBin来缓存用过的View。
在《Android ListView的理解(一)》中,讲解obtainView方法中,有提到过该类,不过,只说了如何用,而未说明这个类里面的方法和实现,这次就来分析下。
3.1 类成员
/** * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the * start of a layout. By construction, they are displaying current information. At the end of * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that * could potentially be used by the adapter to avoid allocating views unnecessarily. * * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * @see android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is populated at the start of * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. * Views in mActiveViews represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; }
区区几个成员变量:
1. 当发生View回收时,mRecyclerListener若有注册,则会通知给注册者;
2. mFirstActivePosition对应的是在ListView中,可视区域中的第一个item position(即getFirstVisiblePosition);
3. mActiveViews存储着当前ListView中,可见区域中的View;
4. mScrapViews是一个ArrayList,为啥?这个与mViewTypeCount相关,那么大家也就猜到了,mScrapViews缓存着ViewTypeCount种类型的View,默认是1,手机联系人是2等;
5. mCurrentScrap就是指向当前mScrapViews中的一组,默认ViewTypeCount = 1的情况下,mCurrentScrap = mScrapViews[0];
3.2 类方法分析
3.2.1 setViewTypeCount
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; }
设置ViewTypeCount,然后初始化类成员变量;
3.2.2 markChildrenDirty
public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).forceLayout(); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).forceLayout(); } } } }
将mScrapView中回收回来的View设置一样标志,在下次被复用到ListView中时,告诉viewroot重新layout该view
forceLayout方法
/** * Forces this view to be laid out during the next layout pass. * This method does not call requestLayout() or forceLayout() * on the parent. */ public void forceLayout() { mPrivateFlags |= FORCE_LAYOUT; mPrivateFlags |= INVALIDATED; }
该方法只是设置标志,并不会通知其parent来重新layout。
3.2.3 shouldRecycleViewType
public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; }
判断给定的view的viewType指明是否可以回收回。
那什么情况下,viewType < 0?即不能回收?
我们在ListView, GridView 或 AbsListView中,找不到小于0的TYPE定义,但是,在AbsListView的parent类AdapterView中找到了:
public abstract class AdapterView<T extends Adapter> extends ViewGroup { /** * The item view type returned by [email protected] Adapter#getItemViewType(int)} when * the adapter does not want the item's view recycled. */ public static final int ITEM_VIEW_TYPE_IGNORE = -1; /** * The item view type returned by [email protected] Adapter#getItemViewType(int)} when * the item is a header or footer. */ public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; .........}
上述表明,指定忽略的,或者是 HeaderView / FootView是不被回收的。如有特殊需要可以将自己定义的viewType设置为-1,否则,将会浪费内存,导致OOM,切记!
3.2.4 clear
/** * Clears the scrap heap. */ void clear() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { removeDetachedView(scrap.remove(scrapCount - 1 - i), false); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { removeDetachedView(scrap.remove(scrapCount - 1 - j), false); } } } }
这个方法一目了然,清理ScrapView中的View,并将这些View从窗口中Detach。
3.2.5 fillActiveViews
/** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount The minimum number of views mActiveViews should hold * @param firstActivePosition The position of the first view that will be stored in * mActiveViews */ 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(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; } } }
该方法就是填充mActiveView数组。当Adapter中的数据个数未发生变化时,此时用户可能只是滚动,或点击等操作,ListView中item的个数会发生变化,因此,需要将可视的item加入到mActiveView中来管理。
3.2.6 getActiveView
/** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ 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; }
position是adapter中的绝对下标值,mFirstActivePosition前面说过了,是当前可视区域的下标值,对应在adapter中的绝对值,如果找到,则返回找到的View,并将mActiveView对应的位置设置为null。
对于3.2.5和3.2.6这两个方法,在ListView.layoutChildren或GridView.layoutChildren中调用的,用户可能做了一些操作(未导致个数发生变化),以ListView.layoutChildren为例,代码片断:
@Override protected void layoutChildren() { ......... // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; // reset the focus restoration View focusLayoutRestoreDirectChild = null; // Don't put header or footer views into the Recycler. Those are // already cached in mHeaderViews; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(getChildAt(i), ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i); } } } else { recycleBin.fillActiveViews(childCount, firstPosition); } ......... fillXXX ......... // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); }
上面代码分成了三段:
step1:如果数据发生变化,就将所有view加入到mScrapView中,否则,将所有view放到mActiveView中;
step2:添加view到listview中;
step3:回收mActiveView中的未使用的view到mScrapView中;
注:在step1中,如果是addScrapView,则所有的view将会detach,如果是fillActiveViews,则不会detach,只有在step3中,未用到的view才会detach。
3.2.7 retrieveFromScrap(这个不属于RecycleBin类,是属于AbslistView中的方法,不过为了讲之后的方法,这个方法就单独拿出来讲解)
static View retrieveFromScrap(ArrayList<View> scrapViews, int position) { int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i=0; i<size; i++) { View view = scrapViews.get(i); if (((AbsListView.LayoutParams)view.getLayoutParams()) .scrappedFromPosition == position) { scrapViews.remove(i); return view; } } return scrapViews.remove(size - 1); } else { return null; } }
根据position,从mScrapView中找:
1. 如果有view.scrappedFromPosition = position的,直接返回该view;
2. 否则返回mScrapView中最后一个;
3. 如果缓存中没有view,则返回null;
下面,我们来分析下这三种情况在什么条件下满足?
a. 第三种情况,这个最简单:
一开始,listview稳定后,显示N个,此时mScrapView中是没有缓存view的,当我们向上滚动一小段距离(第一个此时仍显示部分),新的view将会显示,此时listview会调用Adapter.getView,但是缓存中没有,因此convertView是null,所以,我们得分配一块内存来创建新的convertView;
b. 第二种情况:
在a中,我们继续向上滚动,直接第一个view完全移出屏幕(假设没有新的item),此时,第一个view就会被detach,并被加入到mScrapView中;然后,我们还继续向上滚动,直接后面又将要显示新的item view时,此时,系统会从mScrapView中找position对应的View,显然,是找不到的,则将从mScrapView中,取最后一个缓存的view传递给convertView;
c. 第一种情况:
紧接着在b中(标示为橙色的文字后面),第一个被完全移出,加入到mScrapView中,且没有新增的item到listview中,此时,缓存中就只有第一个view;然后,我此时向下滑动,则之前的第一个item,将被显示出来,此时,从缓存中查找position对应的view有没有,当然,肯定是找到了,就直接返回了。
以上三种情况分析完毕!大家也理解了吧,没完全理解的,多想想就能想通啦。
3.2.8 getScrapView
/** * @return A view from the ScrapViews collection. These are unordered. */ View getScrapView(int position) { if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else { int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } } return null; }
getScrapView实际上就是根据viewType,来查找不同的mScrapView,分析了retrieveFromScrap后,getScrapView就很清楚了。
3.2.9 addScrapView
/** * Put a view into the ScapViews list. These views are unordered. * * @param scrap The view to add */ void addScrapView(View scrap, int position) { AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } // Don't put header or footer views or views that should be ignored // into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } lp.scrappedFromPosition = position; if (mViewTypeCount == 1) { scrap.dispatchStartTemporaryDetach(); mCurrentScrap.add(scrap); } else { scrap.dispatchStartTemporaryDetach(); mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } }
此方法顾名思义,就是将移出可视区域的view,设置它的scrappedFromPosition,然后从窗口中detach该view,并根据viewType加入到mScrapView中。
3.2.10 scrapActiveViews
/** * Move all views remaining in mActiveViews to mScrapViews. */ void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams(); int whichScrap = lp.viewType; activeViews[i] = null; if (!shouldRecycleViewType(whichScrap)) { // Do not move views that should be ignored if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(victim, false); } continue; } if (multipleScraps) { scrapViews = mScrapViews[whichScrap]; } victim.dispatchStartTemporaryDetach(); lp.scrappedFromPosition = mFirstActivePosition + i; scrapViews.add(victim); if (hasListener) { mRecyclerListener.onMovedToScrapHeap(victim); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(victim, ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP, mFirstActivePosition + i, -1); } } } pruneScrapViews(); }
在分析3.2.5和3.2.6方法时,step3已经提到这个方法了,实际上就是将mActiveView中未使用的view回收(因为,此时已经移出可视区域了)。
3.2.11 pruneScrapViews
/** * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews. * (This can happen if an adapter does not recycle its views). */ private void pruneScrapViews() { final int maxViews = mActiveViews.length; final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> scrapPile = scrapViews[i]; int size = scrapPile.size(); final int extras = size - maxViews; size--; for (int j = 0; j < extras; j++) { removeDetachedView(scrapPile.remove(size--), false); } } }
该方法解释为:mScrapView中每个ScrapView数组大小不应该超过mActiveView的大小,如果超过,系统认为程序并没有复用convertView,而是每次都是创建一个新的view,为了避免产生大量的闲置内存且增加OOM的风险,系统会在每次回收后,去检查一下,将超过的部分释放掉,节约内存降低OOM风险。
3.2.12 reclaimScrapViews
/** * Puts all views in the scrap heap into the supplied list. */ void reclaimScrapViews(List<View> views) { if (mViewTypeCount == 1) { views.addAll(mCurrentScrap); } else { final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> scrapPile = scrapViews[i]; views.addAll(scrapPile); } } }
将mScrapView中所有的缓存view全部添加到指定的view list中,只看到有AbsListView.reclaimViews有调用到,但没有其它方法使用这个函数,可能在特殊情况下会使用到,但目前从framework中,看不出来。
3.2.13 setCacheColorHint
/** * Updates the cache color hint of all known views. * * @param color The new cache color hint. */ void setCacheColorHint(int color) { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).setDrawingCacheBackgroundColor(color); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).setDrawingCacheBackgroundColor(color); } } } // Just in case this is called during a layout pass final View[] activeViews = mActiveViews; final int count = activeViews.length; for (int i = 0; i < count; ++i) { final View victim = activeViews[i]; if (victim != null) { victim.setDrawingCacheBackgroundColor(color); } } }
该方法就是为所有的view绘置它们的背景色。
四、总结
通过分析Adapter, BaseAdapter之间的关系,以及分析RecycleBin里面的方法,和对应使用的情况,我想,大家应该也很清楚了。RecycleBin是一个很重要的类,学习了这个类,无论是我们通过ListView 或 GridView来间接使用它,还是将来自己写控件时,需要考虑到复用view时,都很有帮助。
ListView, GridView 以及 AbsListView 包括它们的parent都是很复杂的,我们不能一头埋进去,否则你会发现无从下手,我们只能通过一些关键的类,接口或方法,来找到突破口,理解了这些代码,再去看整个类时,你会发现,其实没那么神秘,甚至有些地方我们都可以联想到情景。
好了,暂时就这么多,当然,对ListView的分析仍未结束,因为,我们只是了解了它的VIEW的添加,删除机制,但它如何与用户交互(即Touch,Scroll,Fling,Click, LongClick等)都未分析,大家可以尝试着去看看,我会在之后的时间里,慢慢的带领大家一起学习,分析。
有什么不对的地方,或是值得大家分析,讨论的,也欢迎大家积极留言,谢谢!