转载:http://blog.csdn.net/guolin_blog/article/details/44996879java
在Android全部经常使用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素不少,手机屏幕没法展现出全部内容的状况。ListView可使用列表的形式来展现内容,超出屏幕部分的内容只须要经过手指滑动就能够移动到屏幕内了。android
另外ListView还有一个很是神奇的功能,我相信你们应该都体验过,即便在ListView中加载很是很是多的数据,好比达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,并且随着咱们手指滑动来浏览更多数据时,程序所占用的内存居然都不会跟着增加。那么ListView是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把ListView的源码通读了一遍,基本了解了它的工做原理,在感叹Google大神可以写出如此精妙代码的同时我也有所敬畏,由于ListView的代码量比较大,复杂度也很高,很难用文字表达清楚,因而我就放弃了把它写成一篇博客的想法。那么如今回想起来这件事我已经肠子都悔青了,由于没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。因而如今我又从新定下心来再次把ListView的源码重读了一遍,那么此次我必定要把它写成一篇博客,分享给你们的同时也当成我本身的笔记吧。算法
首先咱们先来看一下ListView的继承结构,以下图所示:数据库

能够看到,ListView的继承结构仍是至关复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另外一个就是GridView,所以咱们从这一点就能够猜出来,ListView和GridView在工做原理和实现上都是有不少共同点的。而后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,后面就是咱们所熟知的了。先把ListView的继承结构了解一下,待会儿有助于咱们更加清晰地分析代码。数组
Adapter的做用
Adapter相信你们都不会陌生,咱们平时使用ListView的时候必定都会用到它。那么话说回来你们有没有仔细想过,为何须要Adapter这个东西呢?总感受正由于有了Adapter,ListView的使用变得要比其它控件复杂得多。那么这里咱们就先来学习一下Adapter到底起到了什么样的一个做用。缓存
其实说到底,控件就是为了交互和展现数据用的,只不过ListView更加特殊,它是为了展现不少不少数据用的,可是ListView只承担交互和展现工做而已,至于这些数据来自哪里,ListView是不关心的。所以,咱们能设想到的最基本的ListView工做模式就是要有一个ListView控件和一个数据源。微信
不过若是真的让ListView和数据源直接打交道的话,那ListView所要作的适配工做就很是繁杂了。由于数据源这个概念太模糊了,咱们只知道它包含了不少数据而已,至于这个数据源究竟是什么样类型,并无严格的定义,有多是数组,也有多是集合,甚至有多是数据库表中查询出来的游标。因此说若是ListView真的去为每一种数据源都进行适配操做的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它自己应该负责的工做范围,再也不是仅仅承担交互和展现工做就能够了,这样ListView就会变得比较臃肿。app
那么显然Android开发团队是不会容许这种事情发生的,因而就有了Adapter这样一个机制的出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的做用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与以前不一样的是,Adapter的接口都是统一的,所以ListView不用再去担忧任何适配方面的问题。而Adapter又是一个接口(interface),它能够去实现各类各样的子类,每一个子类都能经过本身的逻辑来去完成特定的功能,以及与特定数据源的适配操做,好比说ArrayAdapter能够用于数组和List类型的数据源适配,SimpleCursorAdapter能够用于游标类型的数据源适配,这样就很是巧妙地把数据源适配困难的问题解决掉了,而且还拥有至关不错的扩展性。简单的原理示意图以下所示:异步

固然Adapter的做用不只仅只有数据源适配这一点,还有一个很是很是重要的方法也须要咱们在Adapter当中去重写,就是getView()方法,这个在下面的文章中还会详细讲到。ide
RecycleBin机制
那么在开始分析ListView的源码以前,还有一个东西是咱们提早须要了解的,就是RecycleBin机制,这个机制也是ListView可以实现成百上千条数据都不会OOM最重要的一个缘由。其实RecycleBin的代码并很少,只有300行左右,它是写在AbsListView中的一个内部类,因此全部继承自AbsListView的子类,也就是ListView和GridView,均可以使用这个机制。那咱们来看一下RecycleBin中的主要代码,以下所示:
这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。那么咱们先来对这几个方法进行简单解读,这对后面分析ListView的工做原理将会有很大的帮助。
- fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
- getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。须要注意的是,mActiveViews当中所存储的View,一旦被获取了以后就会从mActiveViews当中移除,下次获取一样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
- addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View肯定要废弃掉的时候(好比滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
- getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,所以getScrapView()方法中的算法也很是简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
- setViewTypeCount() 咱们都知道Adapter当中能够重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的做用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法一般状况下使用的并非不少,因此咱们只要知道RecycleBin当中有这样一个功能就好了。
了解了RecycleBin中的主要方法以及它们的用处以后,下面就能够开始来分析ListView的工做原理了,这里我将仍是按照之前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,否则的话要是把ListView全部的代码都贴出来,那么本篇文章将会很长很长了。
第一次Layout
无论怎么说,ListView即便再特殊最终仍是继承自View的,所以它的执行流程还将会按照View的规则来执行,对于这方面不太熟悉的朋友能够参考我以前写的 Android视图绘制流程彻底解析,带你一步步深刻了解View(二) 。
View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于肯定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并无什么特殊的地方,由于它终归是一个View,占用的空间最多而且一般也就是整个屏幕。onDraw()在ListView当中也没有什么意义,由于ListView自己并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,所以咱们本篇文章也是主要分析的这个方法里的内容。
若是你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是由于这个方法是在ListView的父类AbsListView中实现的,代码以下所示:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- mInLayout = true;
- if (changed) {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).forceLayout();
- }
- mRecycler.markChildrenDirty();
- }
- layoutChildren();
- mInLayout = false;
- }
能够看到,onLayout()方法中并无作什么复杂的逻辑操做,主要就是一个判断,若是ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求全部的子布局都强制进行重绘。除此以外倒没有什么难理解的地方了,不过咱们注意到,在第16行调用了layoutChildren()这个方法,从方法名上咱们就能够猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这固然是能够理解的了,由于子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码以下所示:
- @Override
- protected void layoutChildren() {
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
- try {
- super.layoutChildren();
- invalidate();
- if (mAdapter == null) {
- resetList();
- invokeOnItemScrollListener();
- return;
- }
- int childrenTop = mListPadding.top;
- int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
- View sel;
- View oldSel = null;
- View oldFirst = null;
- View newSel = null;
- View focusLayoutRestoreView = null;
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
-
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
-
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
-
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
-
-
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "ListView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
- setSelectedPositionInt(mNextSelectedPosition);
-
-
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
-
- View focusLayoutRestoreDirectChild = null;
-
-
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
-
-
-
-
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
-
-
-
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
-
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
-
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
-
- detachAllViewsFromParent();
- 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 {
- 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();
- if (sel != null) {
-
-
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
-
-
-
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- positionSelector(sel);
- } else {
- sel.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(sel);
- }
- mSelectedTop = sel.getTop();
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null) positionSelector(child);
- } else {
- mSelectedTop = 0;
- mSelectorRect.setEmpty();
- }
-
-
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
-
-
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
- setNextSelectedPositionInt(mSelectedPosition);
- updateScrollIndicators();
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- }
- }
- }
这段代码比较长,咱们挑重点的看。首先能够肯定的是,ListView当中目前尚未任何子View,数据都仍是由Adapter管理的,并无展现到界面上,所以第19行getChildCount()方法获得的值确定是0。接着在第81行会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的状况下才会变成true,其它状况都是false,所以这里会进入到第90行的执行逻辑,调用RecycleBin的fillActiveViews()方法。按理来讲,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,但是目前ListView中尚未任何的子View,所以这一行暂时还起不了任何做用。
接下来在第114行会根据mLayoutMode的值来决定布局模式,默认状况下都是普通模式LAYOUT_NORMAL,所以会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,而且默认的布局顺序是从上往下,所以会进入到第145行的fillFromTop()方法,咱们跟进去瞧一瞧:
- private View fillFromTop(int nextTop) {
- mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
- mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
- if (mFirstPosition < 0) {
- mFirstPosition = 0;
- }
- return fillDown(mFirstPosition, nextTop);
- }
从这个方法的注释中能够看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法自己并无什么逻辑,就是判断了一下mFirstPosition值的合法性,而后调用fillDown()方法,那么咱们就有理由能够猜想,填充ListView的操做是在fillDown()方法中完成的。进入fillDown()方法,代码以下所示:
- private View fillDown(int pos, int nextTop) {
- View selectedView = null;
- int end = (getBottom() - getTop()) - mListPadding.bottom;
- while (nextTop < end && pos < mItemCount) {
-
- boolean selected = pos == mSelectedPosition;
- View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
- nextTop = child.getBottom() + mDividerHeight;
- if (selected) {
- selectedView = child;
- }
- pos++;
- }
- return selectedView;
- }
能够看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。所以一开始的状况下nextTop一定是小于end值的,而且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,而且nextTop也会增长,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是全部Adapter中的元素都被遍历结束了,就会跳出while循环。
那么while循环当中又作了什么事情呢?值得让人留意的就是第18行调用的makeAndAddView()方法,进入到这个方法当中,代码以下所示:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中尚未缓存任何的View,因此这里获得的值确定是null。那么取得了null以后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,此次的obtainView()方法是能够保证必定返回一个View的,因而下面马上将获取到的View传入到了setupChild()方法当中。那么obtainView()内部究竟是怎么工做的呢?咱们先进入到这个方法里面看一下:
- View obtainView(int position, boolean[] isScrap) {
- isScrap[0] = false;
- View scrapView;
- scrapView = mRecycler.getScrapView(position);
- View child;
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
- if (child != scrapView) {
- mRecycler.addScrapView(scrapView);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- } else {
- isScrap[0] = true;
- dispatchFinishTemporaryDetach(child);
- }
- } else {
- child = mAdapter.getView(position, null, this);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- }
- return child;
- }
obtainView()方法中的代码并很少,但却包含了很是很是重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么咱们仍是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,一样的道理,这里确定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?固然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是咱们平时使用ListView时最最常常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。
那么咱们平时写ListView的Adapter时,getView()方法一般会怎么写呢?这里我举个简单的例子:
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- Fruit fruit = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(resourceId, null);
- } else {
- view = convertView;
- }
- ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
- TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
- fruitImage.setImageResource(fruit.getImageId());
- fruitName.setText(fruit.getName());
- return view;
- }
getView()方法接受的三个参数,第一个参数position表明当前子元素的的位置,咱们能够经过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView能够利用,所以咱们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。
那么这个View也会做为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程中,全部的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,可是不用担忧,后面就不会再有这种状况了,那么咱们继续往下看:
- private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
- boolean selected, boolean recycled) {
- 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();
-
-
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT, 0);
- }
- 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 (needToMeasure) {
- int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
- }
- }
setupChild()方法当中的代码虽然比较多,可是咱们只看核心代码的话就很是简单了,刚才调用obtainView()方法获取到的子元素View,这里在第40行调用了addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满而后就跳出,也就是说即便咱们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,因此不会去作多余的加载工做,这样就能够保证ListView中的内容可以迅速展现到屏幕上。
那么到此为止,第一次Layout过程结束。
第二次Layout
虽然我在源码中并无找出具体的缘由,但若是你本身作一下实验的话就会发现,即便是一个再简单的View,在展现到界面上以前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对咱们影响并不大,由于无论是onMeasure()或者onLayout()几回,反正都是执行的相同的逻辑,咱们并不须要进行过多关心。可是在ListView中状况就不同了,由于这就意味着layoutChildren()过程会执行两次,而这个过程中涉及到向ListView中添加子元素,若是相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。所以ListView在layoutChildren()过程中作了第二次Layout的逻辑处理,很是巧妙地解决了这个问题,下面咱们就来分析一下第二次Layout的过程。
其实第二次Layout和第一次Layout的基本流程是差很少的,那么咱们仍是从layoutChildren()方法开始看起:
- @Override
- protected void layoutChildren() {
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
- try {
- super.layoutChildren();
- invalidate();
- if (mAdapter == null) {
- resetList();
- invokeOnItemScrollListener();
- return;
- }
- int childrenTop = mListPadding.top;
- int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
- View sel;
- View oldSel = null;
- View oldFirst = null;
- View newSel = null;
- View focusLayoutRestoreView = null;
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
-
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
-
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
-
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
-
-
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "ListView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
- setSelectedPositionInt(mNextSelectedPosition);
-
-
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
-
- View focusLayoutRestoreDirectChild = null;
-
-
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
-
-
-
-
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
-
-
-
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
-
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
-
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
-
- detachAllViewsFromParent();
- 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 {
- 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();
- if (sel != null) {
-
-
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
-
-
-
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- positionSelector(sel);
- } else {
- sel.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(sel);
- }
- mSelectedTop = sel.getTop();
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null) positionSelector(child);
- } else {
- mSelectedTop = 0;
- mSelectorRect.setEmpty();
- }
-
-
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
-
-
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
- setNextSelectedPositionInt(mSelectedPosition);
- updateScrollIndicators();
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- }
- }
- }
一样仍是在第19行,调用getChildCount()方法来获取子View的数量,只不过如今获得的值不会再是0了,而是ListView中一屏能够显示的子View数量,由于咱们刚刚在第一次Layout过程中向ListView添加了这么多的子View。下面在第90行调用了RecycleBin的fillActiveViews()方法,此次效果可就不同了,由于目前ListView中已经有子View了,这样全部的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。
接下来将会是很是很是重要的一个操做,在第113行调用了detachAllViewsFromParent()方法。这个方法会将全部ListView当中的子View所有清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再从新加载一遍,这不是严重影响效率吗?不用担忧,还记得咱们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会从新执行一遍inflate过程,所以效率方面并不会有什么明显的影响。
那么咱们接着看,在第141行的判断逻辑当中,因为再也不等于0了,所以会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,由于默认状况下咱们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断一般是成立的,由于mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码以下所示:
- private View fillSpecific(int position, int top) {
- boolean tempIsSelected = position == mSelectedPosition;
- View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
-
- mFirstPosition = position;
- View above;
- View below;
- final int dividerHeight = mDividerHeight;
- if (!mStackFromBottom) {
- above = fillUp(position - 1, temp.getTop() - dividerHeight);
-
- adjustViewsUpOrDown();
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
- int childCount = getChildCount();
- if (childCount > 0) {
- correctTooHigh(childCount);
- }
- } else {
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
-
- 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;
- }
- }
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差很少的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,而后再加载该子View往上以及往下的其它子View。那么因为这里咱们传入的position就是第一个子View的位置,因而fillSpecific()方法的做用就基本上和fillDown()方法是差很少的了,这里咱们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码以下所示:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
仍然仍是在第19行尝试从RecycleBin当中获取Active View,然而此次就必定能够获取到了,由于前面咱们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了不少时间,由于若是在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大下降了。
注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数代表当前的View是以前被回收过的,那么咱们再次回到setupChild()方法当中:
- private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
- boolean selected, boolean recycled) {
- 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();
-
-
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT, 0);
- }
- 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 (needToMeasure) {
- int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
- }
- }
能够看到,setupChild()方法的最后一个参数是recycled,而后在第32行会对这个变量进行判断,因为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过程结束。
滑动加载更多数据
经历了两次Layout过程,虽然说咱们已经能够在ListView中看到内容了,然而关于ListView最神奇的部分咱们却尚未接触到,由于目前ListView中只是加载并显示了第一屏的数据而已。好比说咱们的Adapter当中有1000条数据,可是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工做并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,由于咱们是经过手指滑动来显示更多数据的。
因为滑动部分的机制是属于通用型的,即ListView和GridView都会使用一样的机制,所以这部分代码就确定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,咱们就来看一下AbsListView中的这个方法:
这个方法中的代码就很是多了,由于它所处理的逻辑也很是多,要监听各类各样的触屏事件。可是咱们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动做,那么咱们就只看这部分代码就能够了。
能够看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我能够直接告诉你们,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为何那又要牵扯到另外的好几个方法,这里限于篇幅缘由就再也不展开讲解了,喜欢寻根究底的朋友们能够本身去源码里找一找缘由。
这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并无什么太多须要注意的东西,惟一一点很是重要的就是第92行调用的trackMotionScroll()方法,至关于咱们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而若是是正常在屏幕上滑动的话,那么这个方法就会被调用不少次。那么咱们进入到这个方法中瞧一瞧,代码以下所示:
- boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return true;
- }
- final int firstTop = getChildAt(0).getTop();
- final int lastBottom = getChildAt(childCount - 1).getBottom();
- final Rect listPadding = mListPadding;
- final int spaceAbove = listPadding.top - firstTop;
- final int end = getHeight() - listPadding.bottom;
- final int spaceBelow = lastBottom - end;
- final int height = getHeight() - getPaddingBottom() - getPaddingTop();
- if (deltaY < 0) {
- deltaY = Math.max(-(height - 1), deltaY);
- } else {
- deltaY = Math.min(height - 1, deltaY);
- }
- if (incrementalDeltaY < 0) {
- incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
- } else {
- incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
- }
- final int firstPosition = mFirstPosition;
- if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
-
-
- return true;
- }
- if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
-
-
- return true;
- }
- final boolean down = incrementalDeltaY < 0;
- final boolean inTouchMode = isInTouchMode();
- if (inTouchMode) {
- hideSelector();
- }
- final int headerViewsCount = getHeaderViewsCount();
- final int footerViewsStart = mItemCount - getFooterViewsCount();
- int start = 0;
- int count = 0;
- if (down) {
- final int top = listPadding.top - incrementalDeltaY;
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- if (child.getBottom() >= top) {
- break;
- } else {
- count++;
- int position = firstPosition + i;
- if (position >= headerViewsCount && position < footerViewsStart) {
- mRecycler.addScrapView(child);
- }
- }
- }
- } else {
- final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
- for (int i = childCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- if (child.getTop() <= bottom) {
- break;
- } else {
- start = i;
- count++;
- int position = firstPosition + i;
- if (position >= headerViewsCount && position < footerViewsStart) {
- mRecycler.addScrapView(child);
- }
- }
- }
- }
- mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
- mBlockLayoutRequests = true;
- if (count > 0) {
- detachViewsFromParent(start, count);
- }
- offsetChildrenTopAndBottom(incrementalDeltaY);
- if (down) {
- mFirstPosition += count;
- }
- invalidate();
- final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
- if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
- fillGap(down);
- }
- if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
- final int childIndex = mSelectedPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(getChildAt(childIndex));
- }
- }
- mBlockLayoutRequests = false;
- invokeOnItemScrollListener();
- awakenScrollBars();
- return false;
- }
这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实咱们就能够经过incrementalDeltaY的正负值状况来判断用户是向上仍是向下滑动的了。如第34行代码所示,若是incrementalDeltaY小于0,说明是向下滑动,不然就是向上滑动。
下面将会进行一个边界值检测的过程,能够看到,从第43行开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,第47行当中,若是该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,因此会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么若是是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,而后判断该子View的top值是否是大于bottom值了,若是大于的话说明子View已经移出了屏幕,一样把它加入到废弃缓存中,并将计数器加1。
接下来在第76行,会根据当前计数器的值来进行一个detach操做,它的做用就是把全部移出屏幕的子View所有detach掉,在ListView的概念当中,全部看不到的View就没有必要为它进行保存,由于屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。紧接着在第78行调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY做为参数传入,这个方法的做用是让ListView中全部的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。
而后在第84行会进行判断,若是ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么所以咱们就能够猜出fillGap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,以下所示:
- abstract void fillGap(boolean down);
OK,AbsListView中的fillGap()是一个抽象方法,那么咱们马上就可以想到,它的具体实现确定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码以下所示:
- void fillGap(boolean down) {
- final int count = getChildCount();
- if (down) {
- final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
- getListPaddingTop();
- fillDown(mFirstPosition + count, startOffset);
- correctTooHigh(getChildCount());
- } else {
- final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
- getHeight() - getListPaddingBottom();
- fillUp(mFirstPosition - 1, startOffset);
- correctTooLow(getChildCount());
- }
- }
down参数用于表示ListView是向下滑动仍是向上滑动的,能够看到,若是是向下滑动的话就会调用fillDown()方法,而若是是向上滑动的话就会调用fillUp()方法。那么这两个方法咱们都已经很是熟悉了,内部都是经过一个循环来去对ListView进行填充,因此这两个方法咱们就不看了,可是填充ListView会经过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但此次的逻辑再次不一样了,因此咱们仍是回到这个方法瞧一瞧:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
无论怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过确定是获取不到的了,由于在第二次Layout过程当中咱们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不可以重复利用的,所以这里返回的值确定是null。
既然getActiveView()方法返回的值是null,那么就仍是会走到第28行的obtainView()方法当中,代码以下所示:
- View obtainView(int position, boolean[] isScrap) {
- isScrap[0] = false;
- View scrapView;
- scrapView = mRecycler.getScrapView(position);
- View child;
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
- if (child != scrapView) {
- mRecycler.addScrapView(scrapView);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- } else {
- isScrap[0] = true;
- dispatchFinishTemporaryDetach(child);
- }
- } else {
- child = mAdapter.getView(position, null, this);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- }
- return child;
- }
这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?固然有,由于刚才在trackMotionScroll()方法中咱们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据须要显示到屏幕上,就会尝试从废弃缓存中获取View。因此它们之间就造成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,无论你有任意多条数据须要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据从新利用起来,于是无论咱们加载多少数据都不会出现OOM的状况,甚至内存都不会有所增长。
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/44996879
在Android全部经常使用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素不少,手机屏幕没法展现出全部内容的状况。ListView可使用列表的形式来展现内容,超出屏幕部分的内容只须要经过手指滑动就能够移动到屏幕内了。
另外ListView还有一个很是神奇的功能,我相信你们应该都体验过,即便在ListView中加载很是很是多的数据,好比达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,并且随着咱们手指滑动来浏览更多数据时,程序所占用的内存居然都不会跟着增加。那么ListView是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把ListView的源码通读了一遍,基本了解了它的工做原理,在感叹Google大神可以写出如此精妙代码的同时我也有所敬畏,由于ListView的代码量比较大,复杂度也很高,很难用文字表达清楚,因而我就放弃了把它写成一篇博客的想法。那么如今回想起来这件事我已经肠子都悔青了,由于没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。因而如今我又从新定下心来再次把ListView的源码重读了一遍,那么此次我必定要把它写成一篇博客,分享给你们的同时也当成我本身的笔记吧。
首先咱们先来看一下ListView的继承结构,以下图所示:

能够看到,ListView的继承结构仍是至关复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另外一个就是GridView,所以咱们从这一点就能够猜出来,ListView和GridView在工做原理和实现上都是有不少共同点的。而后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,后面就是咱们所熟知的了。先把ListView的继承结构了解一下,待会儿有助于咱们更加清晰地分析代码。
Adapter的做用
Adapter相信你们都不会陌生,咱们平时使用ListView的时候必定都会用到它。那么话说回来你们有没有仔细想过,为何须要Adapter这个东西呢?总感受正由于有了Adapter,ListView的使用变得要比其它控件复杂得多。那么这里咱们就先来学习一下Adapter到底起到了什么样的一个做用。
其实说到底,控件就是为了交互和展现数据用的,只不过ListView更加特殊,它是为了展现不少不少数据用的,可是ListView只承担交互和展现工做而已,至于这些数据来自哪里,ListView是不关心的。所以,咱们能设想到的最基本的ListView工做模式就是要有一个ListView控件和一个数据源。
不过若是真的让ListView和数据源直接打交道的话,那ListView所要作的适配工做就很是繁杂了。由于数据源这个概念太模糊了,咱们只知道它包含了不少数据而已,至于这个数据源究竟是什么样类型,并无严格的定义,有多是数组,也有多是集合,甚至有多是数据库表中查询出来的游标。因此说若是ListView真的去为每一种数据源都进行适配操做的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它自己应该负责的工做范围,再也不是仅仅承担交互和展现工做就能够了,这样ListView就会变得比较臃肿。
那么显然Android开发团队是不会容许这种事情发生的,因而就有了Adapter这样一个机制的出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的做用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与以前不一样的是,Adapter的接口都是统一的,所以ListView不用再去担忧任何适配方面的问题。而Adapter又是一个接口(interface),它能够去实现各类各样的子类,每一个子类都能经过本身的逻辑来去完成特定的功能,以及与特定数据源的适配操做,好比说ArrayAdapter能够用于数组和List类型的数据源适配,SimpleCursorAdapter能够用于游标类型的数据源适配,这样就很是巧妙地把数据源适配困难的问题解决掉了,而且还拥有至关不错的扩展性。简单的原理示意图以下所示:

固然Adapter的做用不只仅只有数据源适配这一点,还有一个很是很是重要的方法也须要咱们在Adapter当中去重写,就是getView()方法,这个在下面的文章中还会详细讲到。
RecycleBin机制
那么在开始分析ListView的源码以前,还有一个东西是咱们提早须要了解的,就是RecycleBin机制,这个机制也是ListView可以实现成百上千条数据都不会OOM最重要的一个缘由。其实RecycleBin的代码并很少,只有300行左右,它是写在AbsListView中的一个内部类,因此全部继承自AbsListView的子类,也就是ListView和GridView,均可以使用这个机制。那咱们来看一下RecycleBin中的主要代码,以下所示:
这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。那么咱们先来对这几个方法进行简单解读,这对后面分析ListView的工做原理将会有很大的帮助。
- fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
- getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。须要注意的是,mActiveViews当中所存储的View,一旦被获取了以后就会从mActiveViews当中移除,下次获取一样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
- addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View肯定要废弃掉的时候(好比滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
- getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,所以getScrapView()方法中的算法也很是简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
- setViewTypeCount() 咱们都知道Adapter当中能够重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的做用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法一般状况下使用的并非不少,因此咱们只要知道RecycleBin当中有这样一个功能就好了。
了解了RecycleBin中的主要方法以及它们的用处以后,下面就能够开始来分析ListView的工做原理了,这里我将仍是按照之前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,否则的话要是把ListView全部的代码都贴出来,那么本篇文章将会很长很长了。
第一次Layout
无论怎么说,ListView即便再特殊最终仍是继承自View的,所以它的执行流程还将会按照View的规则来执行,对于这方面不太熟悉的朋友能够参考我以前写的 Android视图绘制流程彻底解析,带你一步步深刻了解View(二) 。
View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于肯定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并无什么特殊的地方,由于它终归是一个View,占用的空间最多而且一般也就是整个屏幕。onDraw()在ListView当中也没有什么意义,由于ListView自己并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,所以咱们本篇文章也是主要分析的这个方法里的内容。
若是你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是由于这个方法是在ListView的父类AbsListView中实现的,代码以下所示:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- mInLayout = true;
- if (changed) {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).forceLayout();
- }
- mRecycler.markChildrenDirty();
- }
- layoutChildren();
- mInLayout = false;
- }
能够看到,onLayout()方法中并无作什么复杂的逻辑操做,主要就是一个判断,若是ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求全部的子布局都强制进行重绘。除此以外倒没有什么难理解的地方了,不过咱们注意到,在第16行调用了layoutChildren()这个方法,从方法名上咱们就能够猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这固然是能够理解的了,由于子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码以下所示:
- @Override
- protected void layoutChildren() {
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
- try {
- super.layoutChildren();
- invalidate();
- if (mAdapter == null) {
- resetList();
- invokeOnItemScrollListener();
- return;
- }
- int childrenTop = mListPadding.top;
- int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
- View sel;
- View oldSel = null;
- View oldFirst = null;
- View newSel = null;
- View focusLayoutRestoreView = null;
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
-
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
-
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
-
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
-
-
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "ListView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
- setSelectedPositionInt(mNextSelectedPosition);
-
-
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
-
- View focusLayoutRestoreDirectChild = null;
-
-
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
-
-
-
-
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
-
-
-
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
-
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
-
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
-
- detachAllViewsFromParent();
- 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 {
- 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();
- if (sel != null) {
-
-
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
-
-
-
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- positionSelector(sel);
- } else {
- sel.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(sel);
- }
- mSelectedTop = sel.getTop();
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null) positionSelector(child);
- } else {
- mSelectedTop = 0;
- mSelectorRect.setEmpty();
- }
-
-
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
-
-
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
- setNextSelectedPositionInt(mSelectedPosition);
- updateScrollIndicators();
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- }
- }
- }
这段代码比较长,咱们挑重点的看。首先能够肯定的是,ListView当中目前尚未任何子View,数据都仍是由Adapter管理的,并无展现到界面上,所以第19行getChildCount()方法获得的值确定是0。接着在第81行会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的状况下才会变成true,其它状况都是false,所以这里会进入到第90行的执行逻辑,调用RecycleBin的fillActiveViews()方法。按理来讲,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,但是目前ListView中尚未任何的子View,所以这一行暂时还起不了任何做用。
接下来在第114行会根据mLayoutMode的值来决定布局模式,默认状况下都是普通模式LAYOUT_NORMAL,所以会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,而且默认的布局顺序是从上往下,所以会进入到第145行的fillFromTop()方法,咱们跟进去瞧一瞧:
- private View fillFromTop(int nextTop) {
- mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
- mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
- if (mFirstPosition < 0) {
- mFirstPosition = 0;
- }
- return fillDown(mFirstPosition, nextTop);
- }
从这个方法的注释中能够看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法自己并无什么逻辑,就是判断了一下mFirstPosition值的合法性,而后调用fillDown()方法,那么咱们就有理由能够猜想,填充ListView的操做是在fillDown()方法中完成的。进入fillDown()方法,代码以下所示:
- private View fillDown(int pos, int nextTop) {
- View selectedView = null;
- int end = (getBottom() - getTop()) - mListPadding.bottom;
- while (nextTop < end && pos < mItemCount) {
-
- boolean selected = pos == mSelectedPosition;
- View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
- nextTop = child.getBottom() + mDividerHeight;
- if (selected) {
- selectedView = child;
- }
- pos++;
- }
- return selectedView;
- }
能够看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。所以一开始的状况下nextTop一定是小于end值的,而且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,而且nextTop也会增长,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是全部Adapter中的元素都被遍历结束了,就会跳出while循环。
那么while循环当中又作了什么事情呢?值得让人留意的就是第18行调用的makeAndAddView()方法,进入到这个方法当中,代码以下所示:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中尚未缓存任何的View,因此这里获得的值确定是null。那么取得了null以后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,此次的obtainView()方法是能够保证必定返回一个View的,因而下面马上将获取到的View传入到了setupChild()方法当中。那么obtainView()内部究竟是怎么工做的呢?咱们先进入到这个方法里面看一下:
- View obtainView(int position, boolean[] isScrap) {
- isScrap[0] = false;
- View scrapView;
- scrapView = mRecycler.getScrapView(position);
- View child;
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
- if (child != scrapView) {
- mRecycler.addScrapView(scrapView);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- } else {
- isScrap[0] = true;
- dispatchFinishTemporaryDetach(child);
- }
- } else {
- child = mAdapter.getView(position, null, this);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- }
- return child;
- }
obtainView()方法中的代码并很少,但却包含了很是很是重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么咱们仍是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,一样的道理,这里确定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?固然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是咱们平时使用ListView时最最常常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。
那么咱们平时写ListView的Adapter时,getView()方法一般会怎么写呢?这里我举个简单的例子:
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- Fruit fruit = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(resourceId, null);
- } else {
- view = convertView;
- }
- ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
- TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
- fruitImage.setImageResource(fruit.getImageId());
- fruitName.setText(fruit.getName());
- return view;
- }
getView()方法接受的三个参数,第一个参数position表明当前子元素的的位置,咱们能够经过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView能够利用,所以咱们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。
那么这个View也会做为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程中,全部的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,可是不用担忧,后面就不会再有这种状况了,那么咱们继续往下看:
- private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
- boolean selected, boolean recycled) {
- 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();
-
-
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT, 0);
- }
- 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 (needToMeasure) {
- int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
- }
- }
setupChild()方法当中的代码虽然比较多,可是咱们只看核心代码的话就很是简单了,刚才调用obtainView()方法获取到的子元素View,这里在第40行调用了addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满而后就跳出,也就是说即便咱们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,因此不会去作多余的加载工做,这样就能够保证ListView中的内容可以迅速展现到屏幕上。
那么到此为止,第一次Layout过程结束。
第二次Layout
虽然我在源码中并无找出具体的缘由,但若是你本身作一下实验的话就会发现,即便是一个再简单的View,在展现到界面上以前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对咱们影响并不大,由于无论是onMeasure()或者onLayout()几回,反正都是执行的相同的逻辑,咱们并不须要进行过多关心。可是在ListView中状况就不同了,由于这就意味着layoutChildren()过程会执行两次,而这个过程中涉及到向ListView中添加子元素,若是相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。所以ListView在layoutChildren()过程中作了第二次Layout的逻辑处理,很是巧妙地解决了这个问题,下面咱们就来分析一下第二次Layout的过程。
其实第二次Layout和第一次Layout的基本流程是差很少的,那么咱们仍是从layoutChildren()方法开始看起:
- @Override
- protected void layoutChildren() {
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
- try {
- super.layoutChildren();
- invalidate();
- if (mAdapter == null) {
- resetList();
- invokeOnItemScrollListener();
- return;
- }
- int childrenTop = mListPadding.top;
- int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
- View sel;
- View oldSel = null;
- View oldFirst = null;
- View newSel = null;
- View focusLayoutRestoreView = null;
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
-
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
-
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
-
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
-
-
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "ListView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
- setSelectedPositionInt(mNextSelectedPosition);
-
-
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
-
- View focusLayoutRestoreDirectChild = null;
-
-
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
-
-
-
-
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
-
-
-
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
-
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
-
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
-
- detachAllViewsFromParent();
- 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 {
- 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();
- if (sel != null) {
-
-
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
-
-
-
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- positionSelector(sel);
- } else {
- sel.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(sel);
- }
- mSelectedTop = sel.getTop();
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null) positionSelector(child);
- } else {
- mSelectedTop = 0;
- mSelectorRect.setEmpty();
- }
-
-
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
-
-
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
- setNextSelectedPositionInt(mSelectedPosition);
- updateScrollIndicators();
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- }
- }
- }
一样仍是在第19行,调用getChildCount()方法来获取子View的数量,只不过如今获得的值不会再是0了,而是ListView中一屏能够显示的子View数量,由于咱们刚刚在第一次Layout过程中向ListView添加了这么多的子View。下面在第90行调用了RecycleBin的fillActiveViews()方法,此次效果可就不同了,由于目前ListView中已经有子View了,这样全部的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。
接下来将会是很是很是重要的一个操做,在第113行调用了detachAllViewsFromParent()方法。这个方法会将全部ListView当中的子View所有清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再从新加载一遍,这不是严重影响效率吗?不用担忧,还记得咱们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会从新执行一遍inflate过程,所以效率方面并不会有什么明显的影响。
那么咱们接着看,在第141行的判断逻辑当中,因为再也不等于0了,所以会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,由于默认状况下咱们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断一般是成立的,由于mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码以下所示:
- private View fillSpecific(int position, int top) {
- boolean tempIsSelected = position == mSelectedPosition;
- View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
-
- mFirstPosition = position;
- View above;
- View below;
- final int dividerHeight = mDividerHeight;
- if (!mStackFromBottom) {
- above = fillUp(position - 1, temp.getTop() - dividerHeight);
-
- adjustViewsUpOrDown();
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
- int childCount = getChildCount();
- if (childCount > 0) {
- correctTooHigh(childCount);
- }
- } else {
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
-
- 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;
- }
- }
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差很少的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,而后再加载该子View往上以及往下的其它子View。那么因为这里咱们传入的position就是第一个子View的位置,因而fillSpecific()方法的做用就基本上和fillDown()方法是差很少的了,这里咱们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码以下所示:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
仍然仍是在第19行尝试从RecycleBin当中获取Active View,然而此次就必定能够获取到了,由于前面咱们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了不少时间,由于若是在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大下降了。
注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数代表当前的View是以前被回收过的,那么咱们再次回到setupChild()方法当中:
- private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
- boolean selected, boolean recycled) {
- 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();
-
-
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT, 0);
- }
- 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 (needToMeasure) {
- int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
- }
- }
能够看到,setupChild()方法的最后一个参数是recycled,而后在第32行会对这个变量进行判断,因为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过程结束。
滑动加载更多数据
经历了两次Layout过程,虽然说咱们已经能够在ListView中看到内容了,然而关于ListView最神奇的部分咱们却尚未接触到,由于目前ListView中只是加载并显示了第一屏的数据而已。好比说咱们的Adapter当中有1000条数据,可是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工做并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,由于咱们是经过手指滑动来显示更多数据的。
因为滑动部分的机制是属于通用型的,即ListView和GridView都会使用一样的机制,所以这部分代码就确定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,咱们就来看一下AbsListView中的这个方法:
这个方法中的代码就很是多了,由于它所处理的逻辑也很是多,要监听各类各样的触屏事件。可是咱们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动做,那么咱们就只看这部分代码就能够了。
能够看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我能够直接告诉你们,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为何那又要牵扯到另外的好几个方法,这里限于篇幅缘由就再也不展开讲解了,喜欢寻根究底的朋友们能够本身去源码里找一找缘由。
这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并无什么太多须要注意的东西,惟一一点很是重要的就是第92行调用的trackMotionScroll()方法,至关于咱们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而若是是正常在屏幕上滑动的话,那么这个方法就会被调用不少次。那么咱们进入到这个方法中瞧一瞧,代码以下所示:
- boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return true;
- }
- final int firstTop = getChildAt(0).getTop();
- final int lastBottom = getChildAt(childCount - 1).getBottom();
- final Rect listPadding = mListPadding;
- final int spaceAbove = listPadding.top - firstTop;
- final int end = getHeight() - listPadding.bottom;
- final int spaceBelow = lastBottom - end;
- final int height = getHeight() - getPaddingBottom() - getPaddingTop();
- if (deltaY < 0) {
- deltaY = Math.max(-(height - 1), deltaY);
- } else {
- deltaY = Math.min(height - 1, deltaY);
- }
- if (incrementalDeltaY < 0) {
- incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
- } else {
- incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
- }
- final int firstPosition = mFirstPosition;
- if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
-
-
- return true;
- }
- if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
-
-
- return true;
- }
- final boolean down = incrementalDeltaY < 0;
- final boolean inTouchMode = isInTouchMode();
- if (inTouchMode) {
- hideSelector();
- }
- final int headerViewsCount = getHeaderViewsCount();
- final int footerViewsStart = mItemCount - getFooterViewsCount();
- int start = 0;
- int count = 0;
- if (down) {
- final int top = listPadding.top - incrementalDeltaY;
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- if (child.getBottom() >= top) {
- break;
- } else {
- count++;
- int position = firstPosition + i;
- if (position >= headerViewsCount && position < footerViewsStart) {
- mRecycler.addScrapView(child);
- }
- }
- }
- } else {
- final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
- for (int i = childCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- if (child.getTop() <= bottom) {
- break;
- } else {
- start = i;
- count++;
- int position = firstPosition + i;
- if (position >= headerViewsCount && position < footerViewsStart) {
- mRecycler.addScrapView(child);
- }
- }
- }
- }
- mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
- mBlockLayoutRequests = true;
- if (count > 0) {
- detachViewsFromParent(start, count);
- }
- offsetChildrenTopAndBottom(incrementalDeltaY);
- if (down) {
- mFirstPosition += count;
- }
- invalidate();
- final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
- if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
- fillGap(down);
- }
- if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
- final int childIndex = mSelectedPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(getChildAt(childIndex));
- }
- }
- mBlockLayoutRequests = false;
- invokeOnItemScrollListener();
- awakenScrollBars();
- return false;
- }
这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实咱们就能够经过incrementalDeltaY的正负值状况来判断用户是向上仍是向下滑动的了。如第34行代码所示,若是incrementalDeltaY小于0,说明是向下滑动,不然就是向上滑动。
下面将会进行一个边界值检测的过程,能够看到,从第43行开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,第47行当中,若是该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,因此会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么若是是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,而后判断该子View的top值是否是大于bottom值了,若是大于的话说明子View已经移出了屏幕,一样把它加入到废弃缓存中,并将计数器加1。
接下来在第76行,会根据当前计数器的值来进行一个detach操做,它的做用就是把全部移出屏幕的子View所有detach掉,在ListView的概念当中,全部看不到的View就没有必要为它进行保存,由于屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。紧接着在第78行调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY做为参数传入,这个方法的做用是让ListView中全部的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。
而后在第84行会进行判断,若是ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么所以咱们就能够猜出fillGap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,以下所示:
- abstract void fillGap(boolean down);
OK,AbsListView中的fillGap()是一个抽象方法,那么咱们马上就可以想到,它的具体实现确定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码以下所示:
- void fillGap(boolean down) {
- final int count = getChildCount();
- if (down) {
- final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
- getListPaddingTop();
- fillDown(mFirstPosition + count, startOffset);
- correctTooHigh(getChildCount());
- } else {
- final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
- getHeight() - getListPaddingBottom();
- fillUp(mFirstPosition - 1, startOffset);
- correctTooLow(getChildCount());
- }
- }
down参数用于表示ListView是向下滑动仍是向上滑动的,能够看到,若是是向下滑动的话就会调用fillDown()方法,而若是是向上滑动的话就会调用fillUp()方法。那么这两个方法咱们都已经很是熟悉了,内部都是经过一个循环来去对ListView进行填充,因此这两个方法咱们就不看了,可是填充ListView会经过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但此次的逻辑再次不一样了,因此咱们仍是回到这个方法瞧一瞧:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
-
- child = mRecycler.getActiveView(position);
- if (child != null) {
-
-
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
-
- child = obtainView(position, mIsScrap);
-
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
无论怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过确定是获取不到的了,由于在第二次Layout过程当中咱们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不可以重复利用的,所以这里返回的值确定是null。
既然getActiveView()方法返回的值是null,那么就仍是会走到第28行的obtainView()方法当中,代码以下所示:
- View obtainView(int position, boolean[] isScrap) {
- isScrap[0] = false;
- View scrapView;
- scrapView = mRecycler.getScrapView(position);
- View child;
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
- if (child != scrapView) {
- mRecycler.addScrapView(scrapView);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- } else {
- isScrap[0] = true;
- dispatchFinishTemporaryDetach(child);
- }
- } else {
- child = mAdapter.getView(position, null, this);
- if (mCacheColorHint != 0) {
- child.setDrawingCacheBackgroundColor(mCacheColorHint);
- }
- }
- return child;
- }
这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?固然有,由于刚才在trackMotionScroll()方法中咱们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据须要显示到屏幕上,就会尝试从废弃缓存中获取View。因此它们之间就造成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,无论你有任意多条数据须要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据从新利用起来,于是无论咱们加载多少数据都不会出现OOM的状况,甚至内存都不会有所增长。
那么另外还有一点是须要你们留意的,这里获取到了一个scrapView,而后咱们在第22行将它做为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?咱们再次看一下一个简单的getView()方法示例:
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- Fruit fruit = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(resourceId, null);
- } else {
- view = convertView;
- }
- ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
- TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
- fruitImage.setImageResource(fruit.getImageId());
- fruitName.setText(fruit.getName());
- return view;
- }
第二个参数就是咱们最熟悉的convertView呀,难怪平时咱们在写getView()方法是要判断一下convertView是否是等于null,若是等于null才调用inflate()方法来加载布局,不等于null就能够直接利用convertView,由于convertView就是咱们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,如今又从新拿出来使用而已。而后咱们只须要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局同样,这背后的道理你是否是已经彻底搞明白了?
以后的代码又都是咱们熟悉的流程了,从缓存中拿到子View以后再调用setupChild()方法将它从新attach到ListView当中,由于缓存中的View也是以前从ListView中detach掉的,这部分代码就再也不重复进行分析了。
为了方便你们理解,这里我再附上一张图解说明:

那么到目前为止,咱们就把ListView的整个工做流程代码基本分析结束了,文章比较长,但愿你们能够理解清楚,下篇文章中会讲解咱们平时使用ListView时遇到的问题,感兴趣的朋友请继续阅读 Android ListView异步加载图片乱序问题,缘由分析及解决方案 。
第一时间得到博客更新提醒,以及更多技术信息分享,欢迎关注个人微信公众号,扫一扫下方二维码或搜索微信号guolin_blog,便可关注。
那么另外还有一点是须要你们留意的,这里获取到了一个scrapView,而后咱们在第22行将它做为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?咱们再次看一下一个简单的getView()方法示例:
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- Fruit fruit = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(resourceId, null);
- } else {
- view = convertView;
- }
- ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
- TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
- fruitImage.setImageResource(fruit.getImageId());
- fruitName.setText(fruit.getName());
- return view;
- }
第二个参数就是咱们最熟悉的convertView呀,难怪平时咱们在写getView()方法是要判断一下convertView是否是等于null,若是等于null才调用inflate()方法来加载布局,不等于null就能够直接利用convertView,由于convertView就是咱们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,如今又从新拿出来使用而已。而后咱们只须要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局同样,这背后的道理你是否是已经彻底搞明白了?
以后的代码又都是咱们熟悉的流程了,从缓存中拿到子View以后再调用setupChild()方法将它从新attach到ListView当中,由于缓存中的View也是以前从ListView中detach掉的,这部分代码就再也不重复进行分析了。
为了方便你们理解,这里我再附上一张图解说明:
