可以看到,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中的主要代码,如下所示:
- /**
- * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
- * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
- * those views which were onscreen at the start of a layout. By
- * construction, they are displaying current information. At the end of
- * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
- * are old views that could potentially be used by the adapter to avoid
- * allocating views unnecessarily.
- *
- * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
- * @see android.widget.AbsListView.RecyclerListener
- */
- class RecycleBin {
- private RecyclerListener mRecyclerListener;
- /**
- * The position of the first view stored in mActiveViews.
- */
- private int mFirstActivePosition;
- /**
- * Views that were on screen at the start of layout. This array is
- * populated at the start of layout, and at the end of layout all view
- * in mActiveViews are moved to mScrapViews. Views in mActiveViews
- * represent a contiguous range of Views, with position of the first
- * view store in mFirstActivePosition.
- */
- private View[] mActiveViews = new View[0];
- /**
- * Unsorted views that can be used by the adapter as a convert view.
- */
- private ArrayList<View>[] mScrapViews;
- private int mViewTypeCount;
- private ArrayList<View> mCurrentScrap;
- /**
- * Fill ActiveViews with all of the children of the AbsListView.
- *
- * @param childCount
- * The minimum number of views mActiveViews should hold
- * @param firstActivePosition
- * The position of the first view that will be stored in
- * mActiveViews
- */
- void fillActiveViews(int childCount, int firstActivePosition) {
- if (mActiveViews.length < childCount) {
- mActiveViews = new View[childCount];
- }
- mFirstActivePosition = firstActivePosition;
- final View[] activeViews = mActiveViews;
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
- // Don't put header or footer views into the scrap heap
- if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
- // active views.
- // However, we will NOT place them into scrap views.
- activeViews[i] = child;
- }
- }
- }
- /**
- * 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;
- }
- /**
- * Put a view into the ScapViews list. These views are unordered.
- *
- * @param scrap
- * The view to add
- */
- void addScrapView(View scrap) {
- AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
- if (lp == null) {
- return;
- }
- // Don't put header or footer views or views that should be ignored
- // into the scrap heap
- int viewType = lp.viewType;
- if (!shouldRecycleViewType(viewType)) {
- if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- removeDetachedView(scrap, false);
- }
- return;
- }
- if (mViewTypeCount == 1) {
- dispatchFinishTemporaryDetach(scrap);
- mCurrentScrap.add(scrap);
- } else {
- dispatchFinishTemporaryDetach(scrap);
- mScrapViews[viewType].add(scrap);
- }
- if (mRecyclerListener != null) {
- mRecyclerListener.onMovedToScrapHeap(scrap);
- }
- }
- /**
- * @return A view from the ScrapViews collection. These are unordered.
- */
- View getScrapView(int position) {
- ArrayList<View> scrapViews;
- if (mViewTypeCount == 1) {
- scrapViews = mCurrentScrap;
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);
- } else {
- return null;
- }
- } else {
- int whichScrap = mAdapter.getItemViewType(position);
- if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
- scrapViews = mScrapViews[whichScrap];
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);
- }
- }
- }
- return null;
- }
- 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;
- }
- }
这里的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中实现的,代码如下所示:
- /**
- * Subclasses should NOT override this method but {@link #layoutChildren()}
- * instead.
- */
- @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;
- }
- @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;
- // Remember stuff we will need down below
- 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:
- // Remember the previously selected view
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
- // Remember the previous first child
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
- // Caution: newSel might be null
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
- // Handle the empty set by removing all views that are visible
- // and calling it a day
- 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);
- // Pull all children into the RecycleBin.
- // These views will be reused if possible
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
- // reset the focus restoration
- View focusLayoutRestoreDirectChild = null;
- // Don't put header or footer views into the Recycler. Those are
- // already cached in mHeaderViews;
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
- // take focus back to us temporarily to avoid the eventual
- // call to clear focus when removing the focused child below
- // from messing things up when ViewRoot assigns focus back
- // to someone else
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- // TODO: in some cases focusedChild.getParent() == null
- // we can remember the focused view to restore after relayout if the
- // data hasn't changed, or if the focused position is a header or footer
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
- // remember the specific view that had focus
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
- // tell it we are going to mess with it
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
- // Clear out old views
- 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;
- }
- // Flush any cached views that did not get reused above
- recycleBin.scrapActiveViews();
- if (sel != null) {
- // the current selected item should get focus if items
- // are focusable
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
- // selected item didn't take focus, fine, but still want
- // to make sure something else outside of the selected view
- // has focus
- 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();
- }
- // even if there is not selected position, we may need to restore
- // focus (i.e. something focusable in touch mode)
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
- // tell focus view we are done mucking with it, if it is still in
- // our view hierarchy.
- 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()方法,我们跟进去瞧一瞧:
- /**
- * Fills the list from top to bottom, starting with mFirstPosition
- *
- * @param nextTop The location where the top of the first item should be
- * drawn
- *
- * @return The view that is currently selected
- */
- 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);
- }
- /**
- * Fills the list from pos down to the end of the list view.
- *
- * @param pos The first position to put in the list
- *
- * @param nextTop The location where the top of the item associated with pos
- * should be drawn
- *
- * @return The view that is currently selected, if it happens to be in the
- * range that we draw.
- */
- 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;
- }
可以看到,这里使用了一个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()方法,进入到这个方法当中,代码如下所示:
- /**
- * Obtain the view and add it to our list of children. The view can be made
- * fresh, converted from an unused view, or used as is if it was in the
- * recycle bin.
- *
- * @param position Logical position in the list
- * @param y Top or bottom edge of the view to add
- * @param flow If flow is true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @return View that was added
- */
- 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;
- }
- /**
- * Get a view and have it show the data associated with the specified
- * position. This is called when we have already discovered that the view is
- * not available for reuse in the recycle bin. The only choices left are
- * converting an old view or making a new one.
- *
- * @param position
- * The position to display
- * @param isScrap
- * Array of at least 1 boolean, the first entry will become true
- * if the returned view was taken from the scrap heap, false if
- * otherwise.
- *
- * @return A view displaying the data associated with the specified position
- */
- View obtainView(int position, boolean[] isScrap) {
- 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;
- }
那么我们平时写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;
- }
那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:
- /**
- * Add a view as a child and make sure it is measured (if necessary) and
- * positioned properly.
- *
- * @param child The view to add
- * @param position The position of this child
- * @param y The y position relative to which this view will be positioned
- * @param flowDown If true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @param recycled Has this view been pulled from the recycle bin? If so it
- * does not need to be remeasured.
- */
- 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();
- // Respect layout params that are already in the view. Otherwise make some up...
- // noinspection unchecked
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = 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);
- }
- }
那么到此为止,第一次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;
- // Remember stuff we will need down below
- 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:
- // Remember the previously selected view
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
- // Remember the previous first child
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
- // Caution: newSel might be null
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
- // Handle the empty set by removing all views that are visible
- // and calling it a day
- 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);
- // Pull all children into the RecycleBin.
- // These views will be reused if possible
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
- // reset the focus restoration
- View focusLayoutRestoreDirectChild = null;
- // Don't put header or footer views into the Recycler. Those are
- // already cached in mHeaderViews;
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
- // take focus back to us temporarily to avoid the eventual
- // call to clear focus when removing the focused child below
- // from messing things up when ViewRoot assigns focus back
- // to someone else
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- // TODO: in some cases focusedChild.getParent() == null
- // we can remember the focused view to restore after relayout if the
- // data hasn't changed, or if the focused position is a header or footer
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
- // remember the specific view that had focus
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
- // tell it we are going to mess with it
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
- // Clear out old views
- 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;
- }
- // Flush any cached views that did not get reused above
- recycleBin.scrapActiveViews();
- if (sel != null) {
- // the current selected item should get focus if items
- // are focusable
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
- // selected item didn't take focus, fine, but still want
- // to make sure something else outside of the selected view
- // has focus
- 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();
- }
- // even if there is not selected position, we may need to restore
- // focus (i.e. something focusable in touch mode)
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
- // tell focus view we are done mucking with it, if it is still in
- // our view hierarchy.
- 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()方法当中,代码如下所示:
- /**
- * Put a specific item at a specific location on the screen and then build
- * up and down from there.
- *
- * @param position The reference view to use as the starting point
- * @param top Pixel offset from the top of this view to the top of the
- * reference view.
- *
- * @return The selected view, or null if the selected view is outside the
- * visible area.
- */
- private View fillSpecific(int position, int top) {
- boolean tempIsSelected = position == mSelectedPosition;
- View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
- // Possibly changed again in fillUp if we add rows above this one.
- mFirstPosition = position;
- View above;
- View below;
- final int dividerHeight = mDividerHeight;
- if (!mStackFromBottom) {
- above = fillUp(position - 1, temp.getTop() - dividerHeight);
- // This will correct for the top of the first view not touching the top of the list
- adjustViewsUpOrDown();
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
- int childCount = getChildCount();
- if (childCount > 0) {
- correctTooHigh(childCount);
- }
- } else {
- below = fillDown(position + 1, temp.getBottom() + dividerHeight);
- // This will correct for the bottom of the last view not touching the bottom of the list
- adjustViewsUpOrDown();
- above = fillUp(position - 1, temp.getTop() - dividerHeight);
- int childCount = getChildCount();
- if (childCount > 0) {
- correctTooLow(childCount);
- }
- }
- if (tempIsSelected) {
- return temp;
- } else if (above != null) {
- return above;
- } else {
- return below;
- }
- }
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:
- /**
- * Obtain the view and add it to our list of children. The view can be made
- * fresh, converted from an unused view, or used as is if it was in the
- * recycle bin.
- *
- * @param position Logical position in the list
- * @param y Top or bottom edge of the view to add
- * @param flow If flow is true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @return View that was added
- */
- 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;
- }
仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。
注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:
- /**
- * Add a view as a child and make sure it is measured (if necessary) and
- * positioned properly.
- *
- * @param child The view to add
- * @param position The position of this child
- * @param y The y position relative to which this view will be positioned
- * @param flowDown If true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @param recycled Has this view been pulled from the recycle bin? If so it
- * does not need to be remeasured.
- */
- 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();
- // Respect layout params that are already in the view. Otherwise make some up...
- // noinspection unchecked
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = 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);
- }
- }
经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。
滑动加载更多数据
经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。
由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (!isEnabled()) {
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return isClickable() || isLongClickable();
- }
- final int action = ev.getAction();
- View v;
- int deltaY;
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(ev);
- switch (action & MotionEvent.ACTION_MASK) {
- case MotionEvent.ACTION_DOWN: {
- mActivePointerId = ev.getPointerId(0);
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
- int motionPosition = pointToPosition(x, y);
- if (!mDataChanged) {
- if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
- && (getAdapter().isEnabled(motionPosition))) {
- // User clicked on an actual view (and was not stopping a
- // fling). It might be a
- // click or a scroll. Assume it is a click until proven
- // otherwise
- mTouchMode = TOUCH_MODE_DOWN;
- // FIXME Debounce
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- } else {
- if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
- // If we couldn't find a view to click on, but the down
- // event was touching
- // the edge, we will bail out and try again. This allows
- // the edge correcting
- // code in ViewRoot to try to find a nearby view to
- // select
- return false;
- }
- if (mTouchMode == TOUCH_MODE_FLING) {
- // Stopped a fling. It is a scroll.
- createScrollingCache();
- mTouchMode = TOUCH_MODE_SCROLL;
- mMotionCorrection = 0;
- motionPosition = findMotionRow(y);
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
- }
- }
- }
- if (motionPosition >= 0) {
- // Remember where the motion event started
- v = getChildAt(motionPosition - mFirstPosition);
- mMotionViewOriginalTop = v.getTop();
- }
- mMotionX = x;
- mMotionY = y;
- mMotionPosition = motionPosition;
- mLastY = Integer.MIN_VALUE;
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- final int pointerIndex = ev.findPointerIndex(mActivePointerId);
- final int y = (int) ev.getY(pointerIndex);
- deltaY = y - mMotionY;
- switch (mTouchMode) {
- case TOUCH_MODE_DOWN:
- case TOUCH_MODE_TAP:
- case TOUCH_MODE_DONE_WAITING:
- // Check if we have moved far enough that it looks more like a
- // scroll than a tap
- startScrollIfNeeded(deltaY);
- break;
- case TOUCH_MODE_SCROLL:
- if (PROFILE_SCROLLING) {
- if (!mScrollProfilingStarted) {
- Debug.startMethodTracing("AbsListViewScroll");
- mScrollProfilingStarted = true;
- }
- }
- if (y != mLastY) {
- deltaY -= mMotionCorrection;
- int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
- // No need to do all this work if we're not going to move
- // anyway
- boolean atEdge = false;
- if (incrementalDeltaY != 0) {
- atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
- }
- // Check to see if we have bumped into the scroll limit
- if (atEdge && getChildCount() > 0) {
- // Treat this like we're starting a new scroll from the
- // current
- // position. This will let the user start scrolling back
- // into
- // content immediately rather than needing to scroll
- // back to the
- // point where they hit the limit first.
- int motionPosition = findMotionRow(y);
- if (motionPosition >= 0) {
- final View motionView = getChildAt(motionPosition - mFirstPosition);
- mMotionViewOriginalTop = motionView.getTop();
- }
- mMotionY = y;
- mMotionPosition = motionPosition;
- invalidate();
- }
- mLastY = y;
- }
- break;
- }
- break;
- }
- case MotionEvent.ACTION_UP: {
- switch (mTouchMode) {
- case TOUCH_MODE_DOWN:
- case TOUCH_MODE_TAP:
- case TOUCH_MODE_DONE_WAITING:
- final int motionPosition = mMotionPosition;
- final View child = getChildAt(motionPosition - mFirstPosition);
- if (child != null && !child.hasFocusable()) {
- if (mTouchMode != TOUCH_MODE_DOWN) {
- child.setPressed(false);
- }
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- final AbsListView.PerformClick performClick = mPerformClick;
- performClick.mChild = child;
- performClick.mClickMotionPosition = motionPosition;
- performClick.rememberWindowAttachCount();
- mResurrectToPosition = motionPosition;
- if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
- final Handler handler = getHandler();
- if (handler != null) {
- handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
- : mPendingCheckForLongPress);
- }
- mLayoutMode = LAYOUT_NORMAL;
- if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
- mTouchMode = TOUCH_MODE_TAP;
- setSelectedPositionInt(mMotionPosition);
- layoutChildren();
- child.setPressed(true);
- positionSelector(child);
- setPressed(true);
- if (mSelector != null) {
- Drawable d = mSelector.getCurrent();
- if (d != null && d instanceof TransitionDrawable) {
- ((TransitionDrawable) d).resetTransition();
- }
- }
- postDelayed(new Runnable() {
- public void run() {
- child.setPressed(false);
- setPressed(false);
- if (!mDataChanged) {
- post(performClick);
- }
- mTouchMode = TOUCH_MODE_REST;
- }
- }, ViewConfiguration.getPressedStateDuration());
- } else {
- mTouchMode = TOUCH_MODE_REST;
- }
- return true;
- } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
- post(performClick);
- }
- }
- mTouchMode = TOUCH_MODE_REST;
- break;
- case TOUCH_MODE_SCROLL:
- final int childCount = getChildCount();
- if (childCount > 0) {
- if (mFirstPosition == 0
- && getChildAt(0).getTop() >= mListPadding.top
- && mFirstPosition + childCount < mItemCount
- && getChildAt(childCount - 1).getBottom() <= getHeight()
- - mListPadding.bottom) {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- } else {
- final VelocityTracker velocityTracker = mVelocityTracker;
- velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
- final int initialVelocity = (int) velocityTracker
- .getYVelocity(mActivePointerId);
- if (Math.abs(initialVelocity) > mMinimumVelocity) {
- if (mFlingRunnable == null) {
- mFlingRunnable = new FlingRunnable();
- }
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
- mFlingRunnable.start(-initialVelocity);
- } else {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- }
- }
- } else {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- }
- break;
- }
- setPressed(false);
- // Need to redraw since we probably aren't drawing the selector
- // anymore
- invalidate();
- final Handler handler = getHandler();
- if (handler != null) {
- handler.removeCallbacks(mPendingCheckForLongPress);
- }
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- mActivePointerId = INVALID_POINTER;
- if (PROFILE_SCROLLING) {
- if (mScrollProfilingStarted) {
- Debug.stopMethodTracing();
- mScrollProfilingStarted = false;
- }
- }
- break;
- }
- case MotionEvent.ACTION_CANCEL: {
- mTouchMode = TOUCH_MODE_REST;
- setPressed(false);
- View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
- if (motionView != null) {
- motionView.setPressed(false);
- }
- clearScrollingCache();
- final Handler handler = getHandler();
- if (handler != null) {
- handler.removeCallbacks(mPendingCheckForLongPress);
- }
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- mActivePointerId = INVALID_POINTER;
- break;
- }
- case MotionEvent.ACTION_POINTER_UP: {
- onSecondaryPointerUp(ev);
- final int x = mMotionX;
- final int y = mMotionY;
- final int motionPosition = pointToPosition(x, y);
- if (motionPosition >= 0) {
- // Remember where the motion event started
- v = getChildAt(motionPosition - mFirstPosition);
- mMotionViewOriginalTop = v.getTop();
- mMotionPosition = motionPosition;
- }
- mLastY = y;
- break;
- }
- }
- return true;
- }
这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是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) {
- // Don't need to move views down if the top of the first position
- // is already visible
- return true;
- }
- if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
- // Don't need to move views up if the bottom of the last position
- // is already visible
- 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()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:
- /**
- * Fills the gap left open by a touch-scroll. During a touch scroll,
- * children that remain on screen are shifted and the other ones are
- * discarded. The role of this method is to fill the gap thus created by
- * performing a partial layout in the empty space.
- *
- * @param down
- * true if the scroll is going down, false if it is going up
- */
- abstract void fillGap(boolean down);
- 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());
- }
- }
- /**
- * Obtain the view and add it to our list of children. The view can be made
- * fresh, converted from an unused view, or used as is if it was in the
- * recycle bin.
- *
- * @param position Logical position in the list
- * @param y Top or bottom edge of the view to add
- * @param flow If flow is true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @return View that was added
- */
- 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;
- }
既然getActiveView()方法返回的值是null,那么就还是会走到第28行的obtainView()方法当中,代码如下所示:
- /**
- * Get a view and have it show the data associated with the specified
- * position. This is called when we have already discovered that the view is
- * not available for reuse in the recycle bin. The only choices left are
- * converting an old view or making a new one.
- *
- * @param position
- * The position to display
- * @param isScrap
- * Array of at least 1 boolean, the first entry will become true
- * if the returned view was taken from the scrap heap, false if
- * otherwise.
- *
- * @return A view displaying the data associated with the specified position
- */
- View obtainView(int position, boolean[] isScrap) {
- 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;
- }
那么另外还有一点是需要大家留意的,这里获取到了一个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;
- }
之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。
为了方便大家理解,这里我再附上一张图解说明:
那么到目前为止,我们就把ListView的整个工作流程代码基本分析结束了,文章比较长,希望大家可以理解清楚,下篇文章中会讲解我们平时使用ListView时遇到的问题,感兴趣的朋友请继续阅读 Android ListView异步加载图片乱序问题,原因分析及解决方案 。
版权声明:本文出自郭霖的博客,转载必须注明出处。
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/45586553
在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼。比如说在ListView中加载图片,如果是同步加载图片倒还好,但是一旦使用异步加载图片那么问题就来了,这个问题我相信很多Android开发者都曾经遇到过,就是异步加载图片会出现错位乱序的情况。遇到这个问题时,不少人在网上搜索找到了相应的解决方案,但是真正深入理解这个问题出现的原因并对症解决的人恐怕还并不是很多。那么今天我们就来具体深入分析一下ListView异步加载图片出现乱序问题的原因,以及怎么样对症下药去解决它。
本篇文章的原理基础建立在上一篇文章之上,如果你对ListView的工作原理还不够了解的话,建议先去阅读Android ListView工作原理完全解析,带你从源码的角度彻底理解 。
问题重现
要想解决问题首先我们要把问题重现出来,这里只需要搭建一个最基本的ListView项目,然后在ListView中去异步请求图片并显示,问题就能够得以重现了,那么我们就新建一个ListViewTest项目。
项目建好之后第一个要解决的是数据源的问题,由于ListView中需要从网络上请求图片,那么我就提前准备好了许多张图片,将它们上传到了我的CSDN相册当中,然后新建一个Images类,将所有相册中图片的URL地址都配置进去就可以了,代码如下所示:
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class Images {
- public final static String[] imageUrls = new String[] {
- "https://img-my.csdn.net/uploads/201508/05/1438760758_3497.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760758_6667.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760757_3588.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760756_3304.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760755_6715.jpeg",
- "https://img-my.csdn.net/uploads/201508/05/1438760726_5120.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760726_8364.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760725_4031.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760724_9463.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760724_2371.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760707_4653.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760706_6864.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760706_9279.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760704_2341.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760704_5707.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760685_5091.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760685_4444.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760684_8827.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760683_3691.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760683_7315.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760663_7318.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760662_3454.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760662_5113.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760661_3305.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760661_7416.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760589_2946.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760589_1100.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760588_8297.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760587_2575.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760587_8906.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760550_2875.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760550_9517.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760549_7093.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760549_1352.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760548_2780.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760531_1776.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760531_1380.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760530_4944.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760530_5750.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760529_3289.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760500_7871.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760500_6063.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760499_6304.jpeg",
- "https://img-my.csdn.net/uploads/201508/05/1438760499_5081.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760498_7007.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760478_3128.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760478_6766.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760477_1358.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760477_3540.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760476_1240.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760446_7993.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760446_3641.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760445_3283.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760444_8623.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760444_6822.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760422_2224.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760421_2824.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760420_2660.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760420_7188.jpg",
- "https://img-my.csdn.net/uploads/201508/05/1438760419_4123.jpg",
- };
- }
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
- <ListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- >
- </ListView>
- </LinearLayout>
很简单,只是在LinearLayout中写了一个ListView而已。接着我们要定义ListView中每一个子View的布局,新建一个image_item.xml布局,加入如下代码:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <ImageView
- android:id="@+id/image"
- android:layout_width="match_parent"
- android:layout_height="120dp"
- android:src="@drawable/empty_photo"
- android:scaleType="fitXY"/>
- </LinearLayout>
仍然很简单,image_item.xml布局中只有一个ImageView控件,就是用它来显示图片的,控件在默认情况下会显示一张empty_photo。这样我们就把所有的布局文件都写好了。
接下来新建ImageAdapter做为ListView的适配器,代码如下所示:
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class ImageAdapter extends ArrayAdapter<String> {
- /**
- * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
- */
- private LruCache<String, BitmapDrawable> mMemoryCache;
- public ImageAdapter(Context context, int resource, String[] objects) {
- super(context, resource, objects);
- // 获取应用程序最大可用内存
- int maxMemory = (int) Runtime.getRuntime().maxMemory();
- int cacheSize = maxMemory / 8;
- mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
- @Override
- protected int sizeOf(String key, BitmapDrawable drawable) {
- return drawable.getBitmap().getByteCount();
- }
- };
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- String url = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
- } else {
- view = convertView;
- }
- ImageView image = (ImageView) view.findViewById(R.id.image);
- BitmapDrawable drawable = getBitmapFromMemoryCache(url);
- if (drawable != null) {
- image.setImageDrawable(drawable);
- } else {
- BitmapWorkerTask task = new BitmapWorkerTask(image);
- task.execute(url);
- }
- return view;
- }
- /**
- * 将一张图片存储到LruCache中。
- *
- * @param key
- * LruCache的键,这里传入图片的URL地址。
- * @param drawable
- * LruCache的值,这里传入从网络上下载的BitmapDrawable对象。
- */
- public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
- if (getBitmapFromMemoryCache(key) == null) {
- mMemoryCache.put(key, drawable);
- }
- }
- /**
- * 从LruCache中获取一张图片,如果不存在就返回null。
- *
- * @param key
- * LruCache的键,这里传入图片的URL地址。
- * @return 对应传入键的BitmapDrawable对象,或者null。
- */
- public BitmapDrawable getBitmapFromMemoryCache(String key) {
- return mMemoryCache.get(key);
- }
- /**
- * 异步下载图片的任务。
- *
- * @author guolin
- */
- class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
- private ImageView mImageView;
- public BitmapWorkerTask(ImageView imageView) {
- mImageView = imageView;
- }
- @Override
- protected BitmapDrawable doInBackground(String... params) {
- String imageUrl = params[0];
- // 在后台开始下载图片
- Bitmap bitmap = downloadBitmap(imageUrl);
- BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
- addBitmapToMemoryCache(imageUrl, drawable);
- return drawable;
- }
- @Override
- protected void onPostExecute(BitmapDrawable drawable) {
- if (mImageView != null && drawable != null) {
- mImageView.setImageDrawable(drawable);
- }
- }
- /**
- * 建立HTTP请求,并获取Bitmap对象。
- *
- * @param imageUrl
- * 图片的URL地址
- * @return 解析后的Bitmap对象
- */
- private Bitmap downloadBitmap(String imageUrl) {
- Bitmap bitmap = null;
- HttpURLConnection con = null;
- try {
- URL url = new URL(imageUrl);
- con = (HttpURLConnection) url.openConnection();
- con.setConnectTimeout(5 * 1000);
- con.setReadTimeout(10 * 1000);
- bitmap = BitmapFactory.decodeStream(con.getInputStream());
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (con != null) {
- con.disconnect();
- }
- }
- return bitmap;
- }
- }
- }
ImageAdapter中的代码还算是比较简单的,在getView()方法中首先根据当前的位置获取到图片的URL地址,然后使用inflate()方法加载image_item.xml这个布局,并获取到ImageView控件的实例,接下来开启了一个BitmapWorkerTask异步任务来从网络上加载图片,最终将加载好的图片设置到ImageView上面。注意这里为了防止图片占用过多的内存,我们还是使用了LruCache技术来进行内存控制,对这个技术不熟悉的朋友可以参考我之前的一篇文章 Android高效加载大图、多图解决方案,有效避免程序OOM 。
最后,程序主界面的代码就非常简单了,修改MainActivity中的代码,如下所示:
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class MainActivity extends Activity {
- private ListView listView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- listView = (ListView) findViewById(R.id.list_view);
- ImageAdapter adapter = new ImageAdapter(this, 0, Images.imageThumbUrls);
- listView.setAdapter(adapter);
- }
- }
那么目前程序的思路其实是很简单的,我们在ListView的getView()方法中开启异步请求,从网络上获取图片,当图片获取成功就后就将图片显示到ImageView上面。看起来没什么问题对吗?那么现在我们就来运行一下程序看一看效果吧。
恩?怎么会这个样子,当滑动ListView的时候,图片竟然会自动变来变去,而且图片显示的位置也不正确,简直快乱成一锅粥了!可是我们所有的逻辑都很简单呀,怎么会导致出现这种图片自动变来变去的情况?很遗憾,这是由于Listview内部的工作机制所导致的,如果你对Listview的工作机制不了解,那么就会很难理解这种现象,不过好在上篇文章中我已经讲解过ListView的工作原理了,因此下面就让我们一起分析一下这个问题出现的原因。
原因分析
上篇文章中已经提到了,ListView之所以能够实现加载成百上千条数据都不会OOM,最主要在于它内部优秀的实现机制。虽然作为普通的使用者,我们大可不必关心ListView内部到底是怎么实现的,但是当你了解了它的内部原理之后,很多之前难以解释的问题都变得有理有据了。
ListView在借助RecycleBin机制的帮助下,实现了一个生产者和消费者的模式,不管有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,原理示意图如下所示:
那么这里我们就可以思考一下了,目前数据源当中大概有60个图片的URL地址,而根据ListView的工作原理,显然不可能为每张图片都单独分配一个ImageView控件,ImageView控件的个数其实就比一屏能显示的图片数量稍微多一点而已,移出屏幕的ImageView控件会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取ImageView控件。
那么,每当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。
但是还没完,新进入屏幕的元素它也会发起一条网络请求来获取当前位置的图片,等到图片下载完的时候会设置到同样的ImageView上面,因此就会出现先显示一张图片,然后又变成了另外一张图片的情况,那么刚才我们看到的图片会自动变来变去的情况也就得到了解释。
问题原因已经分析出来了,但是这个问题该怎么解决呢?说实话,ListView异步加载图片的问题并没有什么标准的解决方案,很多人都有自己的一套解决思路,这里我准备给大家讲解三种比较经典的解决办法,大家通过任何一种都可以解决这个问题,但是我们每多学习一种思路,水平就能够更进一步的提高。
解决方案一 使用findViewWithTag
findViewWithTag算是一种比较简单易懂的解决方案,其实早在 Android照片墙应用实现,再多的图片也不怕崩溃 这篇文章当中,我就采用了findViewWithTag来避免图片出现乱序的情况。那么这里我们先来看看怎么通过修改代码把这个问题解决掉,然后再研究一下findViewWithTag的工作原理。
使用findViewWithTag并不需要修改太多的代码,只需要改动ImageAdapter这一个类就可以了,如下所示:
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class ImageAdapter extends ArrayAdapter<String> {
- private ListView mListView;
- ......
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (mListView == null) {
- mListView = (ListView) parent;
- }
- String url = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
- } else {
- view = convertView;
- }
- ImageView image = (ImageView) view.findViewById(R.id.image);
- image.setImageResource(R.drawable.empty_photo);
- image.setTag(url);
- BitmapDrawable drawable = getBitmapFromMemoryCache(url);
- if (drawable != null) {
- image.setImageDrawable(drawable);
- } else {
- BitmapWorkerTask task = new BitmapWorkerTask();
- task.execute(url);
- }
- return view;
- }
- ......
- /**
- * 异步下载图片的任务。
- *
- * @author guolin
- */
- class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
- String imageUrl;
- @Override
- protected BitmapDrawable doInBackground(String... params) {
- imageUrl = params[0];
- // 在后台开始下载图片
- Bitmap bitmap = downloadBitmap(imageUrl);
- BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
- addBitmapToMemoryCache(imageUrl, drawable);
- return drawable;
- }
- @Override
- protected void onPostExecute(BitmapDrawable drawable) {
- ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);
- if (imageView != null && drawable != null) {
- imageView.setImageDrawable(drawable);
- }
- }
- ......
- }
- }
改动的地方就只有这么多,那么我们来分析一下。由于使用findViewWithTag必须要有ListView的实例才行,那么我们在Adapter中怎样才能拿到ListView的实例呢?其实如果你仔细通读了上一篇文章就能知道,getView()方法中传入的第三个参数其实就是ListView的实例,那么这里我们定义一个全局变量mListView,然后在getView()方法中判断它是否为空,如果为空就把parent这个参数赋值给它。
另外在getView()方法中我们还做了一个操作,就是调用了ImageView的setTag()方法,并把当前位置图片的URL地址作为参数传了进去,这个是为后续的findViewWithTag()方法做准备。
最后,我们修改了BitmapWorkerTask的构造函数,这里不再通过构造函数把ImageView的实例传进去了,而是在onPostExecute()方法当中通过ListView的findVIewWithTag()方法来去获取ImageView控件的实例。获取到控件实例后判断下是否为空,如果不为空就让图片显示到控件上。
这里我们可以尝试分析一下findViewWithTag的工作原理,其实顾名思义,这个方法就是通过Tag的名字来获取具备该Tag名的控件,我们先要调用控件的setTag()方法来给控件设置一个Tag,然后再调用ListView的findViewWithTag()方法使用相同的Tag名来找回控件。
那么为什么用了findViewWithTag()方法之后,图片就不会再出现乱序情况了呢?其实原因很简单,由于ListView中的ImageView控件都是重用的,移出屏幕的控件很快会被进入屏幕的图片重新利用起来,那么getView()方法就会再次得到执行,而在getView()方法中会为这个ImageView控件设置新的Tag,这样老的Tag就会被覆盖掉,于是这时再调用findVIewWithTag()方法并传入老的Tag,就只能得到null了,而我们判断只有ImageView不等于null的时候才会设置图片,这样图片乱序的问题也就不存在了。
这是第一种解决方案。
解决方案二 使用弱引用关联
虽然这里我给这种解决方案起名叫弱引用关联,但实际上弱引用只是辅助手段而已,最主要的还是关联,这种解决方案的本质是要让ImageView和BitmapWorkerTask之间建立一个双向关联,互相持有对方的引用,再通过适当的逻辑判断来解决图片乱序问题,然后为了防止出现内存泄漏的情况,双向关联要使用弱引用的方式建立。相比于第一种解决方案,第二种解决方案要明显复杂不少,但在性能和效率方面都会有更好的表现。
我们仍然只需要改动ImageAdapter中的代码,但这次改动的地方比较多,所以我就把ImageAdapter中的全部代码都贴出来了,如下所示:
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class ImageAdapter extends ArrayAdapter<String> {
- private ListView mListView;
- private Bitmap mLoadingBitmap;
- /**
- * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
- */
- private LruCache<String, BitmapDrawable> mMemoryCache;
- public ImageAdapter(Context context, int resource, String[] objects) {
- super(context, resource, objects);
- mLoadingBitmap = BitmapFactory.decodeResource(context.getResources(),
- R.drawable.empty_photo);
- // 获取应用程序最大可用内存
- int maxMemory = (int) Runtime.getRuntime().maxMemory();
- int cacheSize = maxMemory / 8;
- mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
- @Override
- protected int sizeOf(String key, BitmapDrawable drawable) {
- return drawable.getBitmap().getByteCount();
- }
- };
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (mListView == null) {
- mListView = (ListView) parent;
- }
- String url = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
- } else {
- view = convertView;
- }
- ImageView image = (ImageView) view.findViewById(R.id.image);
- BitmapDrawable drawable = getBitmapFromMemoryCache(url);
- if (drawable != null) {
- image.setImageDrawable(drawable);
- } else if (cancelPotentialWork(url, image)) {
- BitmapWorkerTask task = new BitmapWorkerTask(image);
- AsyncDrawable asyncDrawable = new AsyncDrawable(getContext()
- .getResources(), mLoadingBitmap, task);
- image.setImageDrawable(asyncDrawable);
- task.execute(url);
- }
- return view;
- }
- /**
- * 自定义的一个Drawable,让这个Drawable持有BitmapWorkerTask的弱引用。
- */
- class AsyncDrawable extends BitmapDrawable {
- private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
- public AsyncDrawable(Resources res, Bitmap bitmap,
- BitmapWorkerTask bitmapWorkerTask) {
- super(res, bitmap);
- bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
- bitmapWorkerTask);
- }
- public BitmapWorkerTask getBitmapWorkerTask() {
- return bitmapWorkerTaskReference.get();
- }
- }
- /**
- * 获取传入的ImageView它所对应的BitmapWorkerTask。
- */
- private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
- if (imageView != null) {
- Drawable drawable = imageView.getDrawable();
- if (drawable instanceof AsyncDrawable) {
- AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
- return asyncDrawable.getBitmapWorkerTask();
- }
- }
- return null;
- }
- /**
- * 取消掉后台的潜在任务,当认为当前ImageView存在着一个另外图片请求任务时
- * ,则把它取消掉并返回true,否则返回false。
- */
- public boolean cancelPotentialWork(String url, ImageView imageView) {
- BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
- if (bitmapWorkerTask != null) {
- String imageUrl = bitmapWorkerTask.imageUrl;
- if (imageUrl == null || !imageUrl.equals(url)) {
- bitmapWorkerTask.cancel(true);
- } else {
- return false;
- }
- }
- return true;
- }
- /**
- * 将一张图片存储到LruCache中。
- *
- * @param key
- * LruCache的键,这里传入图片的URL地址。
- * @param drawable
- * LruCache的值,这里传入从网络上下载的BitmapDrawable对象。
- */
- public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
- if (getBitmapFromMemoryCache(key) == null) {
- mMemoryCache.put(key, drawable);
- }
- }
- /**
- * 从LruCache中获取一张图片,如果不存在就返回null。
- *
- * @param key
- * LruCache的键,这里传入图片的URL地址。
- * @return 对应传入键的BitmapDrawable对象,或者null。
- */
- public BitmapDrawable getBitmapFromMemoryCache(String key) {
- return mMemoryCache.get(key);
- }
- /**
- * 异步下载图片的任务。
- *
- * @author guolin
- */
- class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
- String imageUrl;
- private WeakReference<ImageView> imageViewReference;
- public BitmapWorkerTask(ImageView imageView) {
- imageViewReference = new WeakReference<ImageView>(imageView);
- }
- @Override
- protected BitmapDrawable doInBackground(String... params) {
- imageUrl = params[0];
- // 在后台开始下载图片
- Bitmap bitmap = downloadBitmap(imageUrl);
- BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
- addBitmapToMemoryCache(imageUrl, drawable);
- return drawable;
- }
- @Override
- protected void onPostExecute(BitmapDrawable drawable) {
- ImageView imageView = getAttachedImageView();
- if (imageView != null && drawable != null) {
- imageView.setImageDrawable(drawable);
- }
- }
- /**
- * 获取当前BitmapWorkerTask所关联的ImageView。
- */
- private ImageView getAttachedImageView() {
- ImageView imageView = imageViewReference.get();
- BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
- if (this == bitmapWorkerTask) {
- return imageView;
- }
- return null;
- }
- /**
- * 建立HTTP请求,并获取Bitmap对象。
- *
- * @param imageUrl
- * 图片的URL地址
- * @return 解析后的Bitmap对象
- */
- private Bitmap downloadBitmap(String imageUrl) {
- Bitmap bitmap = null;
- HttpURLConnection con = null;
- try {
- URL url = new URL(imageUrl);
- con = (HttpURLConnection) url.openConnection();
- con.setConnectTimeout(5 * 1000);
- con.setReadTimeout(10 * 1000);
- bitmap = BitmapFactory.decodeStream(con.getInputStream());
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (con != null) {
- con.disconnect();
- }
- }
- return bitmap;
- }
- }
- }
下面来看一下这个双向弱引用关联是怎么建立的。BitmapWorkerTask指向ImageView的弱引用关联比较简单,就是在BitmapWorkerTask中加入一个构造函数,并在构造函数中要求传入ImageView这个参数。不过我们不再直接持有ImageView的引用,而是使用WeakReference对ImageView进行了一层包装,这样就OK了。
但是ImageView指向BitmapWorkerTask的弱引用关联就没这么容易了,因为我们很难将BitmapWorkerTask的一个弱引用直接设置到ImageView当中。这该怎么办呢?这里使用了一个比较巧的方法,就是借助自定义Drawable的方式来实现。可以看到,我们自定义了一个AsyncDrawable类并让它继承自BitmapDrawable,然后重写了AsyncDrawable的构造函数,在构造函数中要求把BitmapWorkerTask传入,然后在这里给它包装了一层弱引用。那么现在AsyncDrawable指向BitmapWorkerTask的关联已经有了,但是ImageView指向BitmapWorkerTask的关联还不存在,怎么办呢?很简单,让ImageView和AsyncDrawable再关联一下就可以了。可以看到,在getView()方法当中,我们调用了ImageView的setImageDrawable()方法把AsyncDrawable设置了进去,那么ImageView就可以通过getDrawable()方法获取到和它关联的AsyncDrawable,然后再借助AsyncDrawable就可以获取到BitmapWorkerTask了。这样ImageView指向BitmapWorkerTask的弱引用关联也成功建立。
现在双向弱引用的关联已经建立好了,接下来就是逻辑判断的工作了。那么怎样通过逻辑判断来避免图片出现乱序的情况呢?这里我们引入了两个方法,一个是getBitmapWorkerTask()方法,这个方法可以根据传入的ImageView来获取到它对应的BitmapWorkerTask,内部的逻辑就是先获取ImageView对应的AsyncDrawable,再获取AsyncDrawable对应的BitmapWorkerTask。另一个是getAttachedImageView()方法,这个方法会获取当前BitmapWorkerTask所关联的ImageView,然后调用getBitmapWorkerTask()方法来获取该ImageView所对应的BitmapWorkerTask,最后判断,如果获取到的BitmapWorkerTask等于this,也就是当前的BitmapWorkerTask,那么就将ImageView返回,否则就返回null。最后,在onPostExecute()方法当中,只需要使用getAttachedImageView()方法获取到的ImageView来显示图片就可以了。
那么为什么做了这个逻辑判断之后,图片乱序的问题就可以得到解决呢?其实最主要的奥秘就是在getAttachedImageView()方法当中,它会使用当前BitmapWorkerTask所关联的ImageView来反向获取这个ImageView所关联的BitmapWorkerTask,然后用这两个BitmapWorkerTask做对比,如果发现是同一个BitmapWorkerTask才会返回ImageView,否则就返回null。那么什么情况下这两个BitmapWorkerTask才会不同呢?比如说某个图片被移出了屏幕,它的ImageView被另外一个新进入屏幕的图片重用了,那么就会给这个ImageView关联一个新的BitmapWorkerTask,这种情况下,上一个BitmapWorkerTask和新的BitmapWorkerTask肯定就不相等了,这时getAttachedImageView()方法会返回null,而我们又判断ImageView等于null的话是不会设置图片的,因此就不会出现图片乱序的情况了。
除此之外还有另外一个方法非常值得大家注意,就是cancelPotentialWork()方法,这个方法可以大大提高整个ListView图片加载的工作效率。这个方法接收两个参数,一个图片的url,一个ImageView。看一下它的内部逻辑,首先它也是调用了getBitmapWorkerTask()方法来获取传入的ImageView所对应的BitmapWorkerTask,接下来拿BitmapWorkerTask中的imageUrl和传入的url做比较,如果两个url不等的话就调用BitmapWorkerTask的cancel()方法,然后返回true,如果两个url相等的话就返回false。
那么这段逻辑是什么意思呢?其实并不复杂,两个url做比对时,如果发现是相同的,说明请求的是同一张图片,那么直接返回false,这样就不会再去启动BitmapWorkerTask来请求图片,而如果两个url不相同,说明这个ImageView被另外一张图片重新利用了,这个时候就调用了BitmapWorkerTask的cancel()方法把之前的请求取消掉,然后重新启动BitmapWorkerTask来去请求新图片。有了这个操作保护之后,就可以把一些已经移出屏幕的无效的图片请求过滤掉,从而整体提升ListView加载图片的工作效率。
这是第二种解决方案。
解决方案三 使用NetworkImageView
前面两种解决方案都需要我们自己去做额外的逻辑处理,因为ImageView本身是不能自动解决这个问题的,但是如果我们使用NetworkImageView这个控件的话就非常简单了,它自身就已经考虑到了这个问题,我们直接使用它就可以了,不用做任何额外的处理也不会出现图片乱序的情况。
NetworkImageView是Volley当中提供的控件,对于这个控件我之前专门写过一篇博客来讲解,还不熟悉这个控件的朋友可以先去阅读 Android Volley完全解析(二),使用Volley加载网络图片 。
下面我们看一下如何用NetworkImageView来解决这个问题,首先需要修改一下image_item.xml文件,因为我们已经不再使用ImageView控件了,代码如下所示:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <com.android.volley.toolbox.NetworkImageView
- android:id="@+id/image"
- android:layout_width="match_parent"
- android:layout_height="120dp"
- android:src="@drawable/empty_photo"
- android:scaleType="fitXY"/>
- </LinearLayout>
- /**
- * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553
- * @author guolin
- */
- public class ImageAdapter extends ArrayAdapter<String> {
- ImageLoader mImageLoader;
- public ImageAdapter(Context context, int resource, String[] objects) {
- super(context, resource, objects);
- RequestQueue queue = Volley.newRequestQueue(context);
- mImageLoader = new ImageLoader(queue, new BitmapCache());
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- String url = getItem(position);
- View view;
- if (convertView == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
- } else {
- view = convertView;
- }
- NetworkImageView image = (NetworkImageView) view.findViewById(R.id.image);
- image.setDefaultImageResId(R.drawable.empty_photo);
- image.setErrorImageResId(R.drawable.empty_photo);
- image.setImageUrl(url, mImageLoader);
- return view;
- }
- /**
- * 使用LruCache来缓存图片
- */
- public class BitmapCache implements ImageCache {
- private LruCache<String, Bitmap> mCache;
- public BitmapCache() {
- // 获取应用程序最大可用内存
- int maxMemory = (int) Runtime.getRuntime().maxMemory();
- int cacheSize = maxMemory / 8;
- mCache = new LruCache<String, Bitmap>(cacheSize) {
- @Override
- protected int sizeOf(String key, Bitmap bitmap) {
- return bitmap.getRowBytes() * bitmap.getHeight();
- }
- };
- }
- @Override
- public Bitmap getBitmap(String url) {
- return mCache.get(url);
- }
- @Override
- public void putBitmap(String url, Bitmap bitmap) {
- mCache.put(url, bitmap);
- }
- }
- }
那么当然了,虽然现在没有做任何额外的逻辑处理,但是也根本不会出现图片乱序的情况,因为NetworkImageView在内部都帮我们处理掉了。不过大家可能都很好奇,NetworkImageView到底是如何做到的呢?那么就让我们来分析一下它的源码吧。
NetworkImageView中开始加载图片的代码是setImageUrl()方法,源码分析就从这里开始吧,如下所示:
- /**
- * Sets URL of the image that should be loaded into this view. Note that calling this will
- * immediately either set the cached image (if available) or the default image specified by
- * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
- *
- * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
- * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
- * this function.
- *
- * @param url The URL that should be loaded into this ImageView.
- * @param imageLoader ImageLoader that will be used to make the request.
- */
- public void setImageUrl(String url, ImageLoader imageLoader) {
- mUrl = url;
- mImageLoader = imageLoader;
- // The URL has potentially changed. See if we need to load it.
- loadImageIfNecessary(false);
- }
- /**
- * Loads the image for the view if it isn't already loaded.
- * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
- */
- private void loadImageIfNecessary(final boolean isInLayoutPass) {
- int width = getWidth();
- int height = getHeight();
- boolean isFullyWrapContent = getLayoutParams() != null
- && getLayoutParams().height == LayoutParams.WRAP_CONTENT
- && getLayoutParams().width == LayoutParams.WRAP_CONTENT;
- // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
- // view, hold off on loading the image.
- if (width == 0 && height == 0 && !isFullyWrapContent) {
- return;
- }
- // if the URL to be loaded in this view is empty, cancel any old requests and clear the
- // currently loaded image.
- if (TextUtils.isEmpty(mUrl)) {
- if (mImageContainer != null) {
- mImageContainer.cancelRequest();
- mImageContainer = null;
- }
- setDefaultImageOrNull();
- return;
- }
- // if there was an old request in this view, check if it needs to be canceled.
- if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
- if (mImageContainer.getRequestUrl().equals(mUrl)) {
- // if the request is from the same URL, return.
- return;
- } else {
- // if there is a pre-existing request, cancel it if it's fetching a different URL.
- mImageContainer.cancelRequest();
- setDefaultImageOrNull();
- }
- }
- // The pre-existing content of this view didn't match the current URL. Load the new image
- // from the network.
- ImageContainer newContainer = mImageLoader.get(mUrl,
- new ImageListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- if (mErrorImageId != 0) {
- setImageResource(mErrorImageId);
- }
- }
- @Override
- public void onResponse(final ImageContainer response, boolean isImmediate) {
- // If this was an immediate response that was delivered inside of a layout
- // pass do not set the image immediately as it will trigger a requestLayout
- // inside of a layout. Instead, defer setting the image by posting back to
- // the main thread.
- if (isImmediate && isInLayoutPass) {
- post(new Runnable() {
- @Override
- public void run() {
- onResponse(response, false);
- }
- });
- return;
- }
- if (response.getBitmap() != null) {
- setImageBitmap(response.getBitmap());
- } else if (mDefaultImageId != 0) {
- setImageResource(mDefaultImageId);
- }
- }
- });
- // update the ImageContainer to be the new bitmap container.
- mImageContainer = newContainer;
- }
那么解决图片乱序最核心的逻辑就在这里了,其实NetworkImageView的解决思路还是比较简单的,就是如果这个控件已经被移出了屏幕且被重新利用了,那么就把之前的请求取消掉,仅此而已。
而我们都知道,在通常情况下,仅仅这么处理可能是解决不了问题的,因为Java的线程无法保证一定可以中断,即使像第二种解决方案里使用的BitmapWorkerTask的cancel()方法,也不能保证一定可以把请求取消掉,所以还需要使用弱引用关联的处理方式。但是在NetworkImageView当中就可以这么任性,仅仅调用cancelRequest()方法把请求取消掉就可以了,这主要是得益于Volley的出色设计。由于Volley在网络方面的封装非常优秀,它可以保证,只要是取消掉的请求,就绝对不会进行回调,既然不会回调,那么也就不会回到NetworkImageView当中,自然也就不会出现乱序的情况了。
需要注意的是,Volley只是保证取消掉的请求不会进行回调而已,但并没有说可以中断任何请求。由此可见即使是Volley也无法做到中断一个正在执行的线程,如果有一个线程正在执行,Volley只会保证在它执行完之后不会进行回调,但在调用者看来,就好像是这个请求就被取消掉了一样。
那么这里我们只分析与图片乱序相关部分的源码,如果你想了解关于Volley更多的源码,可以参考我之前的一篇文章 Android Volley完全解析(四),带你从源码的角度理解Volley 。
这是第三种解决方案。
好了,关于ListView异步加载图片乱序的问题今天我们就讨论到这里,如果你把三种解决方案都理解清楚的话,那么对于这个问题研究的就算比较透彻了。下一篇文章仍然是ListView主题,我们将学习一下如何对ListView控件进行一些功能扩展,感兴趣的朋友请继续阅读 Android ListView功能扩展,实现高性能的瀑布流布局 。