【android】ListView工作原理解析

来源:互联网 发布:java 静态导入 编辑:程序博客网 时间:2024/05/21 06:20

  • ListView 工作原理
    • Adapter 的作用
    • RecycleBin 机制
    • 第一次 Layout
    • 第二次 Layout
    • 滑动加载更多数据

ListView 工作原理

ListView 是用来显示垂直滚动列表中的条目的控件。ListView 有一个神奇的功能,就是在 ListView 中加载非常非常多的数据,它都不会发生 OOM,而且通过滑动来浏览更多数据时,程序所占的内存竟然都不会增长。那么 ListView 是怎么实现的呢?
这里写图片描述

Adapter 的作用

为什么要使用 Adapter?

因为如果让 ListView 和数据源打交道的话,ListView 所要做的适配工作就非常复杂了。因为并不确定数据源究竟是什么类型。所以如果 ListView 真的为每一种数据源都进行适配操作的话,扩展性会比较差,而且超出了它本身应该负责的工作范围,会变得比较臃肿。所以需要一个桥梁,Adapter 就为 ListView 和 数据源搭建了桥梁。Adapter 的接口都是统一的,因此 ListView 不用再去担心任何适配方面的问题。而Adapter 又是一个接口,它可以去实现各种各样的子类,每个子类都通过自己的逻辑去完成特定的功能,以及与数据源的适配操作。
这里写图片描述

Adapter 除了数据源适配这一点,还有一个非常非常重要的方法要去重写,就是 getView 。

RecycleBin 机制

RecycleBin 是 ListView 不会 OOM 的一个重要原因,它是写在 AbsListView 中的一个内部类,所有继承自 AbsListView 的子类,也就是 ListView 和 GirdView 都可以使用这个机制。

    /**     * 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;        private ArrayList<View> mSkippedScrap;        private SparseArray<View> mTransientStateViews;        private LongSparseArray<View> mTransientStateViewsById;        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;        }        /**         * 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;            //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.                    getSkippedScrap().add(scrap);                }            } else {                if (mViewTypeCount == 1) {                    mCurrentScrap.add(scrap);                } else {                    mScrapViews[viewType].add(scrap);                }                if (mRecyclerListener != null) {                    mRecyclerListener.onMovedToScrapHeap(scrap);                }            }        }    }

一些重要的方法:

  • addScrapView(View scrap, int position):用于将一个废弃的 View 进行缓存。接收一个View参数,当有某个 View 确定要废弃掉的时候(比如滚动除了屏幕)就应该调用这个方法来对 View 进行缓存,RecycleBin 当中使用 mSrapViews 和 mCurrentScrap 这两个 View 来存储废弃 View。
  • getScrapView(int position):用于从废弃的缓存中取出一个 View,这些废弃缓存中的 View 是没有顺序可言的,所以就直接从 mCurrentScrap 当中获取尾部的一个 scrapview 进行返回。
  • fillActiveViews(int childCount, int firstActivePosition):第一个参数表示要存储的 view 的数量,第二个参数表示 ListView 中第一个可见元素的 position 值。RecycleBin 当中使用 mActiveViews 这个数组来存储View,调用这个方法后会根据传入的参数来将 ListView 中指定元素存储到 mActiveViews 数组当中。
  • getActiveView(int position):这个方法和上一个对应,用于从 mActiveViews 数组中获取数组。接受一个 position 参数,表示元素在 ListView 当中的参数,方法内部会自动的将 position 值转换成 mActiveViews 数组对应的下标值。需要注意的是,这个 get 过程会伴随着删除,下次获取同一位置的 View 会返回 null ,也就是说 mActiveViews 不能被重复利用。
  • setViewTypeCount(int viewTypeCount):getViewTypeCount() 是表示 ListView 中有几种类型的数据项,而 setViewTypeCount() 是为每种类型的数据项单独启用一个 RecycleBin 缓存机制。

第一次 Layout

ListView 再特殊也还是继承自 View 的,因此它的执行流程还是会按照 View 的规则来执行。

View 的执行流程无非就分为三步:

  1. onMeasure() :用于测量 View 的大小。View 系统的绘制流程会从 ViewRoot 的 performTraversals() 方法开始,在其内部调用 View 的measure() (final的)方法,它接收两个参数:widthMeasureSpec() 和 heightMeasureSpec,用来确定 View 的宽度和高度的规格 specMode 和大小 specSize。specMode 有三种类型:EXACTLY(由 specSize 的值决定)、AT_MOST(最多只能是 specSize 的大小)、UNSPECIFIED(无限制,自己设定)。View 的大小控制是由父视图、布局文件、以及视图本身共同完成,父视图会提供给父视图参考的大小,而开发人员可以再 xml 文件中指定视图的大小,然后视图本身会对最终的大小进行排版。

  2. onLayout(): 用于确定 View 的布局,也就是确定视图的位置。 ViewRoot 的 performTracersals() 方法会在measure 结束之后继续执行,并调用 View 的 layout 方法来执行此过程。layout 方法接受四个参数,分别表示左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的。在 layout 会调用 setFrame() 方法来判断视图的大小是否发生过变化,确定要不要重绘。接下来同 measure 中一样,调用 onLayout方法。但是这个 onLayout 是一个空方法,是因为 onLayout 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。所以应该是 ViewGroup 中的 onLayout 方法,但是这个方法是一个抽象方法。所以就是子类LinearLayout、RelativeLayout等布局重写了这个方法,然后在内部按照各自的规则对姿势图进行布局。

    getWidth() 和 getMeasureWidth() 区别:getMeasureWidth() 在 measure() 过程结束后就可以获取到,而 getWidth() 要在 layout() 结束之后获取到。getMeasureWidth() 方法中的值是通过 setMeasuredDimension() 方法来设置的,而 getWidth() 的值是通过视图右边的坐标减去左边的坐标计算出来的。

  3. onDraw(): 用于将 View 绘制到界面上。 ViewRoot 中的代码会继续执行并创建出一个 Canvas 对象,然后调用 View 的 draw() 方法来执行具体的绘制工作总共可以分为六步,第二步和第五步很少用。step1:绘制视图背景,这里会得到一个 mBGDrawable 对象,然后根据 layout 过程确定的视图位置来设置背景的绘制区域,自己后再调用 Drawable 的 draw() 方法来完成背景的绘制工作。这个 mBGDrawable 对象其实就是再 XML 中通过 android:background 属性设置的图片或颜色。当然也可以再代码中 setBackgroundColor() 等方法进行赋值。step3:对视图内容进行绘制。这里调用了 onDraw() (空方法,子类实现)方法。子类中主要是借助Canvas这个类。step4:对当前视图的所有子视图进行绘制。diapatchDraw()(空方法,ViewGroup 中有)。step6:对视图的滚动条进行绘制。每一个 View 都有滚动条,只是有的不显示。

在 ListView 中 onMeasure 和 onDraw 没有什么特殊的,主要是 onLayout,ListView 的 onLayout 是在AbsListView 中实现的。

/** * Subclasses should NOT override this method but {@link #layoutChildren()} * instead. */@Overrideprotected 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;}
  • layoutChildren(): 用来进行子元素布局,这个子元素的布局应该是由具体的实现类来负责完成的,而不是在这里,所以是在 ListView 中。
 @Override    protected void layoutChildren() {        final boolean blockLayoutRequests = mBlockLayoutRequests;        if (blockLayoutRequests) {            return;        }        mBlockLayoutRequests = true;        try {            super.layoutChildren();            invalidate();            if (mAdapter == null) {                resetList();                invokeOnItemScrollListener();                return;            }            final int childrenTop = mListPadding.top;            final int childrenBottom = mBottom - mTop - mListPadding.bottom;            final int childCount = getChildCount();            int index = 0;            int delta = 0;            View sel;            View oldSel = null;            View oldFirst = null;            View newSel = 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. Make sure your adapter calls notifyDataSetChanged() "                        + "when its content changes. [in ListView(" + getId() + ", " + getClass()                        + ") with Adapter(" + mAdapter.getClass() + ")]");            }            setSelectedPositionInt(mNextSelectedPosition);            AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;            View accessibilityFocusLayoutRestoreView = null;            int accessibilityFocusPosition = INVALID_POSITION;            // Remember which child, if any, had accessibility focus. This must            // occur before recycling any views, since that will clear            // accessibility focus.            final ViewRootImpl viewRootImpl = getViewRootImpl();            if (viewRootImpl != null) {                final View focusHost = viewRootImpl.getAccessibilityFocusedHost();                if (focusHost != null) {                    final View focusChild = getAccessibilityFocusedChild(focusHost);                    if (focusChild != null) {                        if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)                                || focusChild.hasTransientState() || mAdapterHasStableIds) {                            // The views won't be changing, so try to maintain                            // focus on the current host and virtual view.                            accessibilityFocusLayoutRestoreView = focusHost;                            accessibilityFocusLayoutRestoreNode = viewRootImpl                                    .getAccessibilityFocusedVirtualView();                        }                        // If all else fails, maintain focus at the same                        // position.                        accessibilityFocusPosition = getPositionForView(focusChild);                    }                }            }            View focusLayoutRestoreDirectChild = null;            View focusLayoutRestoreView = null;            // 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 ViewAncestor 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 re-layout                // if the data hasn't changed, or if the focused position is a                // header or footer.                if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)                        || focusedChild.hasTransientState() || mAdapterHasStableIds) {                    focusLayoutRestoreDirectChild = focusedChild;                    // Remember the specific view that had focus.                    focusLayoutRestoreView = findFocus();                    if (focusLayoutRestoreView != null) {                        // Tell it we are going to mess with it.                        focusLayoutRestoreView.dispatchStartTemporaryDetach();                    }                }                requestFocus();            }            // Pull all children into the RecycleBin.            // These views will be reused if possible            final int firstPosition = mFirstPosition;            final RecycleBin recycleBin = mRecycler;            if (dataChanged) {                for (int i = 0; i < childCount; i++) {                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);                }            } else {                recycleBin.fillActiveViews(childCount, firstPosition);            }            // Clear out old views            detachAllViewsFromParent();            recycleBin.removeSkippedScrap();            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:                final int selectedPosition = reconcileSelectedPosition();                sel = fillSpecific(selectedPosition, mSpecificTop);                /**                 * When ListView is resized, FocusSelector requests an async selection for the                 * previously focused item to make sure it is still visible. If the item is not                 * selectable, it won't regain focus so instead we call FocusSelector                 * to directly request focus on the view after it is visible.                 */                if (sel == null && mFocusSelector != null) {                    final Runnable focusRunnable = mFocusSelector                            .setupFocusIfValid(selectedPosition);                    if (focusRunnable != null) {                        post(focusRunnable);                    }                }                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();            // remove any header/footer that has been temp detached and not re-attached            removeUnusedFixedViews(mHeaderViewInfos);            removeUnusedFixedViews(mFooterViewInfos);            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 != null &&                            focusLayoutRestoreView.requestFocus()) || sel.requestFocus();                    if (!focusWasTaken) {                        // Selected item didn't take focus, but we still want to                        // make sure something else outside of the selected view                        // has focus.                        final View focused = getFocusedChild();                        if (focused != null) {                            focused.clearFocus();                        }                        positionSelector(INVALID_POSITION, sel);                    } else {                        sel.setSelected(false);                        mSelectorRect.setEmpty();                    }                } else {                    positionSelector(INVALID_POSITION, sel);                }                mSelectedTop = sel.getTop();            } else {                final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP                        || mTouchMode == TOUCH_MODE_DONE_WAITING;                if (inTouchMode) {                    // If the user's finger is down, select the motion position.                    final View child = getChildAt(mMotionPosition - mFirstPosition);                    if (child != null) {                        positionSelector(mMotionPosition, child);                    }                } else if (mSelectorPosition != INVALID_POSITION) {                    // If we had previously positioned the selector somewhere,                    // put it back there. It might not match up with the data,                    // but it's transitioning out so it's not a big deal.                    final View child = getChildAt(mSelectorPosition - mFirstPosition);                    if (child != null) {                        positionSelector(mSelectorPosition, child);                    }                } else {                    // Otherwise, clear selection.                    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();                }            }            // Attempt to restore accessibility focus, if necessary.            if (viewRootImpl != null) {                final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();                if (newAccessibilityFocusedView == null) {                    if (accessibilityFocusLayoutRestoreView != null                            && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {                        final AccessibilityNodeProvider provider =                                accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();                        if (accessibilityFocusLayoutRestoreNode != null && provider != null) {                            final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(                                    accessibilityFocusLayoutRestoreNode.getSourceNodeId());                            provider.performAction(virtualViewId,                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);                        } else {                            accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();                        }                    } else if (accessibilityFocusPosition != INVALID_POSITION) {                        // Bound the position within the visible children.                        final int position = MathUtils.constrain(                                accessibilityFocusPosition - mFirstPosition, 0,                                getChildCount() - 1);                        final View restoreView = getChildAt(position);                        if (restoreView != null) {                            restoreView.requestAccessibilityFocus();                        }                    }                }            }            // Tell focus view we are done mucking with it, if it is still in            // our view hierarchy.            if (focusLayoutRestoreView != null                    && focusLayoutRestoreView.getWindowToken() != null) {                focusLayoutRestoreView.dispatchFinishTemporaryDetach();            }            mLayoutMode = LAYOUT_NORMAL;            mDataChanged = false;            if (mPositionScrollAfterLayout != null) {                post(mPositionScrollAfterLayout);                mPositionScrollAfterLayout = null;            }            mNeedSync = false;            setNextSelectedPositionInt(mSelectedPosition);            updateScrollIndicators();            if (mItemCount > 0) {                checkSelectionChanged();            }            invokeOnItemScrollListener();        } finally {            if (mFocusSelector != null) {                mFocusSelector.onLayoutComplete();            }            if (!blockLayoutRequests) {                mBlockLayoutRequests = false;            }        }    }

这段代码做了什么事情呢?调用 RecycleBin 的 fillActiveViews() 方法,按理来说,调用 fillActiveViews 是为了将ListView 的子 view 进行缓存的,但是目前 ListView 中还没有任何的子 view,所以这个现在暂时还起不了啥作用。接下来会根据 mLayoutMode 的值来觉id那个布局模式,接下来是 fillFromTop(), 这个方法负责的主要任务就是自顶向底去填充 ListView,这个方法里面又是各种调用,到 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;}

这个方法是整个 ListView 中最重要的内容。显示调用了 RecycleBin 的getSrcapView 来获取一个废弃缓存中的 view,同样道理,这里是获取不到的,返回 null,继续执行,会调用 mAdapter 的 getView 方法获取一个 view。这个getView 就是平时用的 getView(int position, View convertView,ViewGroup perent), 第一个表示当前子元素的位置,第二个如果是空的话会调用 LayoutInflater 的 inflate 方法来加载一个布局,接下来就是对这个 view 进行一些属性和值上的设置,然后将这个 view 返回。这个 view也作为 obtainView 的结果返回,并最终传到 setupChild 中。也就是说,第一次 Layout 过程中,所有的子 View 都是调用 LayoutInflater 的 inflate 方法加载出来的,相对比较耗时,但是后面就不会了。

在 setupChild 中,调用 addViewInLayout 将刚才传入的 view 添加到 ListView 当中,然后根据 fillDown() 方法中的循环,会让子元素的 View 将整个 ListView 控件填满然后跳出,也就是说,就算 Adapter 中有上千条数据, ListView 也只会加载第一屏的数据,剩下的反正看不到,就不加载了,这样就可以保证 ListView 的内容能够迅速的展示到屏幕上。

第一次 Layout 结束。

第二次 Layout

即便是再简单的 View,在展示到界面之前都会尽力至少两次 onMeasure 和两次 onLayout,但是在 ListView 中就意味着要执行两次 layoutChildren,这个过程涉及到向 ListView 中添加元素,如果相同的逻辑执行两遍的话 ListView 中就会存在一份重复的数据了。因此 ListView 在 layoutChildren 过程当中做了第二次Layout 的逻辑处理,非常巧妙的解决了这些问题。

这样在 layoutChildren 中调用getchildcount 的方法就不是0了,而是 ListView 一屏可以加载的 view 数。

调用 RecycleBin 的 fillActiveViews 就会将 ListView 中的所有子 view 缓存到 RecycleBin 的 mActiveViews 数组中。

调用 detachAllViewsFromParent ,这个方法会将所有 ListView 当中的子 view 全部清除掉,从而保证第二次 Layout 过程不会产生一份重复的数据。把已经加载好的 view 清除掉,待会又要加载一遍,不是严重影响效率吗?还记得之前缓存在 mActiveViews 中的 view 吗,待会就直接使用这些缓存好的 view 进行加载,而不会重新执行一遍 inflate 过程,因此效率并不会有什么明显的影响。

和第一次不一样的是 childcount != 0 了,所以会执行第一次 Layout 没有执行的 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;    }}

这个方法会优先将指定位置的子 view 先加载到屏幕上,然后再加载该子 view 往上以及往下的其他子 view。注意 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;}

这个方法尝试从 RecycleBin 当中获取 ActiveView,然后这次就一定可以获取到了,就不会进入obtainView 中了,而是直接进入 setupChild 方法,这样就省去了很多时间,不用去 inflate 布局。注意 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);    }}

最后一个参数是 recycled ,现在是 true,所以它会执行 attachViewToParent 而不是第一次 Layout 的 addViewInLayout,这里两个方法的区别是如果要向 ViewGroup 中添加一个新的子 view 就用 add..,如果想要将之前 detach 的 view 重新 attach 到 ViewGroup 就用 attach…

这样,经历一个 detach 又 attach 的过程,ListView 中所有的子 view 又都可以正常的显示出来了。

第二次 Layout 结束。

滑动加载更多数据

经历了两次 Layout ,已经可以在 ListView 中看到内容了,那剩下的 Adaper 中的 view 是怎么通过手指滑动加载进来的呢?

由于滑动部分的机制是属于通用型的,即在 ListView 和 GirdView 都会使用同样的机制,因此这部分代码在 AbsListView 中,监听触控事件是在 onTouchEvent 方法中进行的。

    @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();        }        if (mPositionScroller != null) {            mPositionScroller.stop();        }        if (mIsDetaching || !isAttachedToWindow()) {            // Something isn't right.            // Since we rely on being attached to get data set change notifications,            // don't risk doing anything where we might try to resync and find things            // in a bogus state.            return false;        }        startNestedScroll(SCROLL_AXIS_VERTICAL);        if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {            return true;        }        initVelocityTrackerIfNotExists();        final MotionEvent vtev = MotionEvent.obtain(ev);        final int actionMasked = ev.getActionMasked();        if (actionMasked == MotionEvent.ACTION_DOWN) {            mNestedYOffset = 0;        }        vtev.offsetLocation(0, mNestedYOffset);        switch (actionMasked) {            case MotionEvent.ACTION_DOWN: {                onTouchDown(ev);                break;            }            case MotionEvent.ACTION_MOVE: {                onTouchMove(ev, vtev);                break;            }            case MotionEvent.ACTION_UP: {                onTouchUp(ev);                break;            }            case MotionEvent.ACTION_CANCEL: {                onTouchCancel();                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                    final View child = getChildAt(motionPosition - mFirstPosition);                    mMotionViewOriginalTop = child.getTop();                    mMotionPosition = motionPosition;                }                mLastY = y;                break;            }            case MotionEvent.ACTION_POINTER_DOWN: {                // New pointers take over dragging duties                final int index = ev.getActionIndex();                final int id = ev.getPointerId(index);                final int x = (int) ev.getX(index);                final int y = (int) ev.getY(index);                mMotionCorrection = 0;                mActivePointerId = id;                mMotionX = x;                mMotionY = y;                final int motionPosition = pointToPosition(x, y);                if (motionPosition >= 0) {                    // Remember where the motion event started                    final View child = getChildAt(motionPosition - mFirstPosition);                    mMotionViewOriginalTop = child.getTop();                    mMotionPosition = motionPosition;                }                mLastY = y;                break;            }        }        if (mVelocityTracker != null) {            mVelocityTracker.addMovement(vtev);        }        vtev.recycle();        return true;    }

这个部分代码很多,要监听各种各样的触屏事件,但是我们现在只关心手指在屏幕上滑动这一个事件,对应的 ACTION_MOVE 这个动作,所以关注 onTouchMove(ev, vtev) 方法。

    private void onTouchMove(MotionEvent ev, MotionEvent vtev) {        if (mHasPerformedLongPress) {            // Consume all move events following a successful long press.            return;        }        int pointerIndex = ev.findPointerIndex(mActivePointerId);        if (pointerIndex == -1) {            pointerIndex = 0;            mActivePointerId = ev.getPointerId(pointerIndex);        }        if (mDataChanged) {            // Re-sync everything if data has been changed            // since the scroll operation can query the adapter.            layoutChildren();        }        final int y = (int) ev.getY(pointerIndex);        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. If so, we'll enter scrolling mode.                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {                    break;                }                // Otherwise, check containment within list bounds. If we're                // outside bounds, cancel any active presses.                final View motionView = getChildAt(mMotionPosition - mFirstPosition);                final float x = ev.getX(pointerIndex);                if (!pointInView(x, y, mTouchSlop)) {                    setPressed(false);                    if (motionView != null) {                        motionView.setPressed(false);                    }                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?                            mPendingCheckForTap : mPendingCheckForLongPress);                    mTouchMode = TOUCH_MODE_DONE_WAITING;                    updateSelectorState();                } else if (motionView != null) {                    // Still within bounds, update the hotspot.                    final float[] point = mTmpPoint;                    point[0] = x;                    point[1] = y;                    transformPointToViewLocal(point, motionView);                    motionView.drawableHotspotChanged(point[0], point[1]);                }                break;            case TOUCH_MODE_SCROLL:            case TOUCH_MODE_OVERSCROLL:                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);                break;        }    }

这里是根据当前的 mTouchMode 决定,当手指在屏幕上滑动时,TouchMode 就等于 TOUCH_MODE_SCROLL,就会到 scrollIfNeeded .

    private void scrollIfNeeded(int x, int y, MotionEvent vtev) {        int rawDeltaY = y - mMotionY;        int scrollOffsetCorrection = 0;        int scrollConsumedCorrection = 0;        if (mLastY == Integer.MIN_VALUE) {            rawDeltaY -= mMotionCorrection;        }        if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY,                mScrollConsumed, mScrollOffset)) {            rawDeltaY += mScrollConsumed[1];            scrollOffsetCorrection = -mScrollOffset[1];            scrollConsumedCorrection = mScrollConsumed[1];            if (vtev != null) {                vtev.offsetLocation(0, mScrollOffset[1]);                mNestedYOffset += mScrollOffset[1];            }        }        final int deltaY = rawDeltaY;        int incrementalDeltaY =                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;        int lastYCorrection = 0;        if (mTouchMode == TOUCH_MODE_SCROLL) {            if (PROFILE_SCROLLING) {                if (!mScrollProfilingStarted) {                    Debug.startMethodTracing("AbsListViewScroll");                    mScrollProfilingStarted = true;                }            }            if (mScrollStrictSpan == null) {                // If it's non-null, we're already in a scroll.                mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");            }            if (y != mLastY) {                // We may be here after stopping a fling and continuing to scroll.                // If so, we haven't disallowed intercepting touch events yet.                // Make sure that we do so in case we're in a parent that can intercept.                if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&                        Math.abs(rawDeltaY) > mTouchSlop) {                    final ViewParent parent = getParent();                    if (parent != null) {                        parent.requestDisallowInterceptTouchEvent(true);                    }                }                final int motionIndex;                if (mMotionPosition >= 0) {                    motionIndex = mMotionPosition - mFirstPosition;                } else {                    // If we don't have a motion position that we can reliably track,                    // pick something in the middle to make a best guess at things below.                    motionIndex = getChildCount() / 2;                }                int motionViewPrevTop = 0;                View motionView = this.getChildAt(motionIndex);                if (motionView != null) {                    motionViewPrevTop = motionView.getTop();                }                // No need to do all this work if we're not going to move anyway                boolean atEdge = false;                if (incrementalDeltaY != 0) {                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);                }                // Check to see if we have bumped into the scroll limit                motionView = this.getChildAt(motionIndex);                if (motionView != null) {                    // Check if the top of the motion view is where it is                    // supposed to be                    final int motionViewRealTop = motionView.getTop();                    if (atEdge) {                        // Apply overscroll                        int overscroll = -incrementalDeltaY -                                (motionViewRealTop - motionViewPrevTop);                        if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll,                                mScrollOffset)) {                            lastYCorrection -= mScrollOffset[1];                            if (vtev != null) {                                vtev.offsetLocation(0, mScrollOffset[1]);                                mNestedYOffset += mScrollOffset[1];                            }                        } else {                            final boolean atOverscrollEdge = overScrollBy(0, overscroll,                                    0, mScrollY, 0, 0, 0, mOverscrollDistance, true);                            if (atOverscrollEdge && mVelocityTracker != null) {                                // Don't allow overfling if we're at the edge                                mVelocityTracker.clear();                            }                            final int overscrollMode = getOverScrollMode();                            if (overscrollMode == OVER_SCROLL_ALWAYS ||                                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&                                            !contentFits())) {                                if (!atOverscrollEdge) {                                    mDirection = 0; // Reset when entering overscroll.                                    mTouchMode = TOUCH_MODE_OVERSCROLL;                                }                                if (incrementalDeltaY > 0) {                                    mEdgeGlowTop.onPull((float) -overscroll / getHeight(),                                            (float) x / getWidth());                                    if (!mEdgeGlowBottom.isFinished()) {                                        mEdgeGlowBottom.onRelease();                                    }                                    invalidateTopGlow();                                } else if (incrementalDeltaY < 0) {                                    mEdgeGlowBottom.onPull((float) overscroll / getHeight(),                                            1.f - (float) x / getWidth());                                    if (!mEdgeGlowTop.isFinished()) {                                        mEdgeGlowTop.onRelease();                                    }                                    invalidateBottomGlow();                                }                            }                        }                    }                    mMotionY = y + lastYCorrection + scrollOffsetCorrection;                }                mLastY = y + lastYCorrection + scrollOffsetCorrection;            }        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {            if (y != mLastY) {                final int oldScroll = mScrollY;                final int newScroll = oldScroll - incrementalDeltaY;                int newDirection = y > mLastY ? 1 : -1;                if (mDirection == 0) {                    mDirection = newDirection;                }                int overScrollDistance = -incrementalDeltaY;                if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) {                    overScrollDistance = -oldScroll;                    incrementalDeltaY += overScrollDistance;                } else {                    incrementalDeltaY = 0;                }                if (overScrollDistance != 0) {                    overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0,                            0, mOverscrollDistance, true);                    final int overscrollMode = getOverScrollMode();                    if (overscrollMode == OVER_SCROLL_ALWAYS ||                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&                                    !contentFits())) {                        if (rawDeltaY > 0) {                            mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(),                                    (float) x / getWidth());                            if (!mEdgeGlowBottom.isFinished()) {                                mEdgeGlowBottom.onRelease();                            }                            invalidateTopGlow();                        } else if (rawDeltaY < 0) {                            mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),                                    1.f - (float) x / getWidth());                            if (!mEdgeGlowTop.isFinished()) {                                mEdgeGlowTop.onRelease();                            }                            invalidateBottomGlow();                        }                    }                }                if (incrementalDeltaY != 0) {                    // Coming back to 'real' list scrolling                    if (mScrollY != 0) {                        mScrollY = 0;                        invalidateParentIfNeeded();                    }                    trackMotionScroll(incrementalDeltaY, incrementalDeltaY);                    mTouchMode = TOUCH_MODE_SCROLL;                    // We did not scroll the full amount. Treat this essentially like the                    // start of a new touch scroll                    final int motionPosition = findClosestMotionRow(y);                    mMotionCorrection = 0;                    View motionView = getChildAt(motionPosition - mFirstPosition);                    mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0;                    mMotionY =  y + scrollOffsetCorrection;                    mMotionPosition = motionPosition;                }                mLastY = y + lastYCorrection + scrollOffsetCorrection;                mDirection = newDirection;            }        }    }

这里只用关注 if (mTouchMode == TOUCH_MODE_SCROLL) 语句,trackMotionScroll, 这个相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。

    /**     * Track a motion scroll     *     * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion     *        began. Positive numbers mean the user's finger is moving down the screen.     * @param incrementalDeltaY Change in deltaY from the previous event.     * @return true if we're already at the beginning/end of the list and have nothing to do.     */    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;        // "effective padding" In this case is the amount of padding that affects        // how much space should not be filled by items. If we don't clip to padding        // there is no effective padding.        int effectivePaddingTop = 0;        int effectivePaddingBottom = 0;        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {            effectivePaddingTop = listPadding.top;            effectivePaddingBottom = listPadding.bottom;        }         // FIXME account for grid vertical spacing too?        final int spaceAbove = effectivePaddingTop - firstTop;        final int end = getHeight() - effectivePaddingBottom;        final int spaceBelow = lastBottom - end;        final int height = getHeight() - mPaddingBottom - mPaddingTop;        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;        // Update our guesses for where the first and last views are        if (firstPosition == 0) {            mFirstPositionDistanceGuess = firstTop - listPadding.top;        } else {            mFirstPositionDistanceGuess += incrementalDeltaY;        }        if (firstPosition + childCount == mItemCount) {            mLastPositionDistanceGuess = lastBottom + listPadding.bottom;        } else {            mLastPositionDistanceGuess += incrementalDeltaY;        }        final boolean cannotScrollDown = (firstPosition == 0 &&                firstTop >= listPadding.top && incrementalDeltaY >= 0);        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&                lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);        if (cannotScrollDown || cannotScrollUp) {            return incrementalDeltaY != 0;        }        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) {            int top = -incrementalDeltaY;            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {                top += listPadding.top;            }            for (int i = 0; i < childCount; i++) {                final View child = getChildAt(i);                if (child.getBottom() >= top) {                    break;                } else {                    count++;                    int position = firstPosition + i;                    if (position >= headerViewsCount && position < footerViewsStart) {                        // The view will be rebound to new data, clear any                        // system-managed transient state.                        child.clearAccessibilityFocus();                        mRecycler.addScrapView(child, position);                    }                }            }        } else {            int bottom = getHeight() - incrementalDeltaY;            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {                bottom -= listPadding.bottom;            }            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) {                        // The view will be rebound to new data, clear any                        // system-managed transient state.                        child.clearAccessibilityFocus();                        mRecycler.addScrapView(child, position);                    }                }            }        }        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;        mBlockLayoutRequests = true;        if (count > 0) {            detachViewsFromParent(start, count);            mRecycler.removeSkippedScrap();        }        // invalidate before moving the children to avoid unnecessary invalidate        // calls to bubble up from the children all the way to the top        if (!awakenScrollBars()) {           invalidate();        }        offsetChildrenTopAndBottom(incrementalDeltaY);        if (down) {            mFirstPosition += count;        }        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {            fillGap(down);        }        mRecycler.fullyDetachScrapViews();        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {            final int childIndex = mSelectedPosition - mFirstPosition;            if (childIndex >= 0 && childIndex < getChildCount()) {                positionSelector(mSelectedPosition, getChildAt(childIndex));            }        } else if (mSelectorPosition != INVALID_POSITION) {            final int childIndex = mSelectorPosition - mFirstPosition;            if (childIndex >= 0 && childIndex < getChildCount()) {                positionSelector(INVALID_POSITION, getChildAt(childIndex));            }        } else {            mSelectorRect.setEmpty();        }        mBlockLayoutRequests = false;        invokeOnItemScrollListener();        return false;    }

这个方法接收两个参数,deltaY 表示从手指按下时的位置到当前手指位置的距离, incrementalDeltaY 则表示据上次出发 event 事件手指在 Y 方向上位置的改变量,正负表示是向上还是向下。下面会进行一个边界值检测的过程,当 ListView 向下滑动的时候,就会进入一个 for 循环中,从上往下一次获取子 view ,如果该子view 的bottom 值已经小于 top 值了,就说明这个子 view 已经移除屏幕了,所以就会调用 RecycleBin 的 addScrapView 将这个 view 加入到废弃缓存中,并将 count 计数器加1,计数器用于记录有多少个子 view 被移除了屏幕外。

接下来会对当前计数器的值进行一个 detach 操作,他的作用就是把所有移除屏幕的子 view 全部 detach 掉。紧接着调用了 offsetChildrenTopAndBottom(incrementalDeltaY) 操作,这个方法的作用是让 ListView 中所有的子 view 都按照传入的参数值进行相应的偏移,这样就实现了随着手指的滑动,ListView 的内容也会随着滚动的效果。

再判断,如果 ListView 最后一个 view 的底部已经移入了屏幕,或者 ListView 中第一个 view 的顶部移入了屏幕,就会调用 fillGap 方法,所以猜测这个方法是用来加载屏幕外数据的。AbsListView中的fillGap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在ListView中完成的了。这个方法中调用了makeAndAddView() 方法,首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。所以还是会走到 obtainView 中。

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

还有一点就是这里获取到了一个 scrapView ,然后就是将它作为参数传到了 Adapter 的getView,就是之前的 convertView ,这里它就不为空了,所以不用再inflate,因为convertView 就是我们之前利用过的 view,只不过被移除屏幕后进入到了废弃缓存,现在又重新拿出来使用。然后我们只需要把 convertView 中的数据更新成当前位置上应该显示的数据就好了。

这里写图片描述

0 0
原创粉丝点击