该文章是一个系列文章,是本人在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
此致,敬礼