一、ListView的继承结构

ListView是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此ListView和GridView在工作原理和实现上都是有很多共同点的。

AbsListView又继承自AdapterView,AdapterView继承自ViewGroup

二、Adapter的作用

适配器

ListView只承担交互和展示工作,至于这些数据来自哪里,ListView是不关心的,因此就有了Adapter。在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源。

Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题

三、RecycleBin机制

RecycleBin机制是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。

是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制

class RecycleBin {
    private RecyclerListener mRecyclerListener;

    /**
     * 存贮在mActiveViews中的第一个位置的view
     */
    private int mFirstActivePosition;

    /**
     * 存贮活动的view,即屏幕上可见的view
     */
    private View[] mActiveViews = new View[0];

    /**
	 * 所有废弃的view,即滑出屏幕的view,可以作为convertview被adapter使用
     */
    private ArrayList<View>[] mScrapViews;

    private int mViewTypeCount;

    private ArrayList<View> mCurrentScrap;

    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;
    }
	
	...
    /**
     * 存贮所有可见的view
	 *
     * @param childCount The minimum number of views mActiveViews should hold
     * @param firstActivePosition The position of the first view that will be stored in
     *        mActiveViews
     */
    void fillActiveViews(int childCount, int firstActivePosition) {
        if (mActiveViews.length < childCount) {
            mActiveViews = new View[childCount];
        }
        mFirstActivePosition = firstActivePosition;

        //noinspection MismatchedReadAndWriteOfArray
        final View[] activeViews = mActiveViews;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
            // Don't put header or footer views into the scrap heap
            if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
                //        However, we will NOT place them into scrap views.
                activeViews[i] = child;
                // Remember the position so that setupChild() doesn't reset state.
                lp.scrappedFromPosition = firstActivePosition + i;
            }
        }
    }

    /**
     * Get the view corresponding to the specified position. The view will be removed from
     * mActiveViews if it is found.
     *
     * @param position The position to look up in mActiveViews
     * @return The view if it is found, null otherwise
     */
    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >=0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }

    

    /**
     * @return A view from the ScrapViews collection. These are unordered.
     */
    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;
    }

    /**
     * Puts a view into the list of scrap views.
     * <p>
     * If the list data hasn't changed or the adapter has stable IDs, views
     * with transient state will be preserved for later retrieval.
     *
     * @param scrap The view to add
     * @param position The view's position within its parent
     */
    void addScrapView(View scrap, int position) {
        final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
        if (lp == null) {
            // Can't recycle, but we don't know anything about the view.
            // Ignore it completely.
            return;
        }

        lp.scrappedFromPosition = position;

        // Remove but don't scrap header or footer views, or views that
        // should otherwise not be recycled.
        final int viewType = lp.viewType;
        if (!shouldRecycleViewType(viewType)) {
            // Can't recycle. If it's not a header or footer, which have
            // special handling and should be ignored, then skip the scrap
            // heap and we'll fully detach the view later.
            if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                getSkippedScrap().add(scrap);
            }
            return;
        }

        scrap.dispatchStartTemporaryDetach();

        // The the accessibility state of the view may change while temporary
        // detached and we do not allow detached views to fire accessibility
        // events. So we are announcing that the subtree changed giving a chance
        // to clients holding on to a view in this subtree to refresh it.
        notifyViewAccessibilityStateChangedIfNeeded(
                AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

        // Don't scrap views that have transient state.
        final boolean scrapHasTransientState = scrap.hasTransientState();
        if (scrapHasTransientState) {
            if (mAdapter != null && mAdapterHasStableIds) {
                // If the adapter has stable IDs, we can reuse the view for
                // the same data.
                if (mTransientStateViewsById == null) {
                    mTransientStateViewsById = new LongSparseArray<>();
                }
                mTransientStateViewsById.put(lp.itemId, scrap);
            } else if (!mDataChanged) {
                // If the data hasn't changed, we can reuse the views at
                // their old positions.
                if (mTransientStateViews == null) {
                    mTransientStateViews = new SparseArray<>();
                }
                mTransientStateViews.put(position, scrap);
            } else {
                // Otherwise, we'll have to remove the view and start over.
                clearScrapForRebind(scrap);
                getSkippedScrap().add(scrap);
            }
        } else {
            clearScrapForRebind(scrap);
            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                mScrapViews[viewType].add(scrap);
            }

            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
        }
    }
	...
}

最主要的几个方法的解释:

fillActiveViews(int childCount, int firstActivePosition)

这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。

getActiveView(int position)

这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。

addScrapView(View scrap, int position)

用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。

getScrapView(int position)

用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。

setViewTypeCount(int viewTypeCount)

Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以只要知道RecycleBin当中有这样一个功能就行了。

从代码中可以知道:

只有屏幕中显示的view是被保存在内存中的,被滑出屏幕后就会被保存在RecycleBin中,新滑入的view会复用滑出屏幕的view。

四、缓存源码分析

1、第一次Layout

ListView本身还是一个View,所以还是按照View的绘制流程 onMeasure -> onLayout -> onDraw

onMeasure和onDraw在缓存机制中没有什么意义,主要看onLayout。ListView本身没有onLayout方法,可以在其父类AbsListView中找到

onLayout代码:

@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,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过之后调用了layoutChildren()这个方法,这个方法是用来进行子元素布局的,
不过进入到这个方法当中你会发现这是个空方法,没有一行代码。因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。

接下来进到ListView的layoutChildren代码

layoutChildren代码:

@Override
protected void layoutChildren() {
     ...
         
	int childCount = getChildCount();
    ...
		 
    if (dataChanged) {
	     ...
     } else {
         recycleBin.fillActiveViews(childCount, firstPosition);
     }
     ...
		 
     switch (mLayoutMode) {
          case LAYOUT_SET_SELECTION:
          ...
			 
          break;
          case LAYOUT_SYNC:
          ...
			 
          break;
          case LAYOUT_FORCE_BOTTOM:
	      ...
			 
          break;
          case LAYOUT_FORCE_TOP:
		  ...

          break;
          case LAYOUT_SPECIFIC:
	      ...
			 
          break;
          case LAYOUT_MOVE_SELECTION:
		  ...
			 
          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;
          }
	     ...
			 
}

由于是第一次layout,所以childCount是0,dataChanged只有在数据发生变化时才会为true,其余都是false。就会去调用recycleBin.fillActiveViews方法去存贮活动的view,但此时ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。接下来会去判断mLayoutMode,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入default中,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入fillFromTop()方法

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);
}

fillFromTop()方法仅仅是判断mFirstPosition的合法性,接着就会去调用fillDown方法

fillDown():

private View fillDown(int pos, int nextTop) {
      View selectedView = null;
      int end = (getBottom() - getTop()) - mListPadding.bottom;
      while (nextTop < end && pos < mItemCount) {
             // is this the selected item?
             boolean selected = pos == mSelectedPosition;
             View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
             nextTop = child.getBottom() + mDividerHeight;
             if (selected) {
                   selectedView = child;
             }
             pos++;
        }
        return selectedView;
}

fillDown()方法中有一个循环操作。一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。whilel循环中可以看到调用makeAndAddView方法

makeAndAddView():

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {
                 View child;
                 if (!mDataChanged) {
                     // Try to use an exsiting view for this position
                     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;
                     }
                 }
                 // Make a new view for this position, or convert an unused view if possible
                 child = obtainView(position, mIsScrap);
                 // This needs to be positioned and measured
                 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
                 return child;
             }

makeAndAddView()方法可以看到mRecycler.getActiveView(position)正在尝试从RcycleBin中取货一个view,但此时View还没有加载到ListView,所以为null。接下来可以看到调用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方法中mRecycler.getScrapView(position)由于此时没有view所以为null。接下来就可以看到mAdapter.getView(position, null, this)。这就是在自定义Adapter中实现的getView方法,它会返回一个实现的view。makeAndAddView拿到这个view就会调用setupChild方法

setupChild()方法

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled) {
                 ...
				 
                 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);
                 }
				 ...
				 
             }

setupChild()方法当中的核心代码非常简单了,刚才调用obtainView()方法获取到的子元素View,这里调用了addViewInLayout()方法将它添加到了ListView当中。根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。

至此第一次layout就结束了。

2、第二次layout

在View展示到界面上之前会经历至少两次onMeasure()和两次onLayout()的过程。平时影响并不大,因为不管是onMeasure()或者onLayout()几次,反正都是执行的相同的逻辑,不需要进行过多关心。但是在ListView中情况就不一样了,因为这就意味着layoutChildren()过程会执行两次,而这个过程当中涉及到向ListView中添加子元素,如果相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程当中做了第二次Layout的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次Layout的过程。

第二次Layout和第一次Layout的基本流程是差不多的,还是从layoutChildren()方法开始。

layoutChildren代码:

@Override
         protected void layoutChildren() {
         ...
         
		 int childCount = getChildCount();
         ...
		 
         if (dataChanged) {
		 ...
         } else {
             recycleBin.fillActiveViews(childCount, firstPosition);
         }
         ...
		 
         // Clear out old views
         detachAllViewsFromParent()
         switch (mLayoutMode) {
             case LAYOUT_SET_SELECTION:
             ...
			 
             break;
             case LAYOUT_SYNC:
             ...
			 
             break;
             case LAYOUT_FORCE_BOTTOM:
			 ...
			 
             break;
             case LAYOUT_FORCE_TOP:
			 ...

             break;
             case LAYOUT_SPECIFIC:
			 ...
			 
             break;
             case LAYOUT_MOVE_SELECTION:
			 ...
			 
             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;
             }
			 ...
			 
         }

由于是第二次进来,这时childCount就大于0,再次调用recycleBin.fillActiveViews就会将第一次添加到listview中的view被缓存到RecycleBin的mActiveViews数组。接下来就会调用detachAllViewsFromParent方法将将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。由于childCount不等于0,那么就会进入else中。在else中有三个判断,默认是没有选择view,所以就会进入第二个判断中调用fillSpecific方法

fillSpecific()方法:

private View fillSpecific(int position, int top) {
             boolean tempIsSelected = position == mSelectedPosition;
             View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
             ...
			 
         }
		 
		 fillSpecific()方法中又会调用makeAndAddView方法
		 
	<3> makeAndAddView()方法
	
	    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {
            View child;
            if (!mDataChanged) {
                // Try to use an exsiting view for this position
                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;
                }
            }
            // Make a new view for this position, or convert an unused view if possible
            child = obtainView(position, mIsScrap);
            // This needs to be positioned and measured
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
            return child;
        }

再次进入此方法后mRecycler.getActiveView(position)获得的child就不为null,将获得的view直接传给setupChild方法,注意setupChild最后一个参数为true

setupChild():

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled) {
            ...
				 
            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);
            }
		    ...
				 
        }

由于setupChild()方法中的recycled参数为true,此时调用了attachViewToParent()方法将它添加到了ListView当中。

至此第二次Layout过程结束,ListView中所有的子View又正常显示出来。

3、滑动

经历了两次Layout过程,可以在ListView中看到内容了,但目前ListView中只是加载并显示了第一屏的数据而已。要想看到更多数据,那么就要看一下ListView滑动部分的源码了。由于滑动是通用的,所以onTouchEvent是在父类AbsListView中

onTouchEvent()方法:

@Override
        public boolean onTouchEvent(MotionEvent ev) {
		...
		
            switch (action & MotionEvent.ACTION_MASK) {
             ...
		
                case MotionEvent.ACTION_MOVE: {
                 ...
		
         	        switch (mTouchMode) {
	                 ...
			
	                    case TOUCH_MODE_SCROLL:
		                ...
				
			                if (incrementalDeltaY != 0) {
				                atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
			                }
			        ...
	                }
	                break;
				}
			}
        }

这个方法中有很多代码,但由于处理滑动,所以只需要看ACTION_MOVE中的代码,ACTION_MOVE中又有一个判断mTouchMode,手指滑动TouchMode是TOUCH_MODE_SCROLL,在这个判断中可以看到会调用trackMotionScroll方法。相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。

trackMotionScroll()方法

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
            ...
            final boolean down = incrementalDeltaY < 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);
			            }
		            }
	            }
            }
            if (count > 0) {
	            detachViewsFromParent(start, count);
            }
			offsetChildrenTopAndBottom(incrementalDeltaY);
            ...
			
			if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
	            fillGap(down);
            }
			...
			
        }

这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。

当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。

如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。

根据当前计数器的值来进行一个detach操作,作用就是把所有移出屏幕的子View全部detach掉,在ListView中,所有看不到的View就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。接着调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。然后会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此fillGap()方法是用来加载屏幕外数据的。

AbsListView中的fillGap()是一个抽象方法,它的具体实现是在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进行填充,会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了。

makeAndAddView()方法

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {
            View child;
            if (!mDataChanged) {
                // Try to use an exsiting view for this position
                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;
                }
            }
            // Make a new view for this position, or convert an unused view if possible
            child = obtainView(position, mIsScrap);
            // This needs to be positioned and measured
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
            return child;
        }

仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。那么会走到obtainView()方法

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;
        }

会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,因为刚才在trackMotionScroll()方法中,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

这里获取到了一个scrapView,将它作为第二个参数传入到了Adapter的getView()方法当中,这时getView()中的convertView就不为null。这就是在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView。之后的代码又都是熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中。这样整个ListView的缓存机制就完成了。

down参数用于表

五、优化

1、contentView view复用

2、viewholder 减少findViewById

3、多图片显示 三级缓存

4、监听滑动事件 滑动停止时再去加载图片