ListView实现分析

一直都想写一篇文章分析下ListView的实现,总是忙,一直拖到现在,快到年底了,写出来希望能帮助一些面试跳槽的人。
当然写这篇文章也是有原因的,当时有同事在面试的时候,被对方要求当场实现一个ListView,同事简单的答了一些实现原理后,很显然对方不满意,经过几轮PK后,就有了不河蟹的结局。
不欢而散,哈哈。听说后当时我想着要把ListView源码仔细分析后,我自己也去面试试试,可是拖到了现在也没机会了。

GridViewListView都继承自AbsListView,他俩的实现也比较类似,这里就不说GridView了。只说一下ListView
首先看一下ListView的文档:

/*
 * Implementation Notes:
 *
 * Some terminology:
 *
 *     index    - index of the items that are currently visible
 *     position - index of the items in the cursor
 */


/**
 * A view that shows items in a vertically scrolling list. The items
 * come from the {@link ListAdapter} associated with this view.
 */

然后再说一下ListView的继承关系ListView extends AbsListView extends AdapterView<ListAdapter> extends ViewGroup extends View
ListView与其他空间不一样,不是拖到界面上就能用,而是要通过设置适配器来添加条目。这就是AdapterView的主要功能。既然通过适配器来操作数据。
这里自然想到的就是MVC设计模式。

  • Data - M
  • ListView - V
  • Adapter - C

到这里我们写罗列一下我们将要分析的内容:

  • ListViewAdapter间的调用。
  • ListView上下滑动时操作及缓存。

我们平时使用ListView的时候都是调用setAdapter()方法,所以我们这里就从该方法入手:

    /**
     * Sets the data behind this ListView.
     *
     * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
     * depending on the ListView features currently in use. For instance, adding
     * headers and/or footers will cause the adapter to be wrapped.
     *
     * @param adapter The ListAdapter which is responsible for maintaining the
     *        data backing this list and for producing a view to represent an
     *        item in that data set.
     *
     * @see #getAdapter() 
     */
    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            // 取消之前注册过的Adapter
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
        // 清空所有的数据
        resetList();    
        // The data set used to store unused views that should be reused during the next layout to avoid creating new ones.
        // 从注释中可以很明显的看出`View`的复用是通过`RecycleBin`类来实现的。它就决定了为什么`ListView`可以显示很多数据缺不会内存溢出。
        mRecycler.clear();

        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            // 如果有HeaderView或者FooterView就把Adapter重新封装下,这是什么设计模式?-装饰
            // mHeaderViewInfos和mFooterViewInfos分别是header view和foooter view装饰类FixedViewInfo的集合。
            // 会在addHeaderView()和addFooterView()中进行添加。
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

        mOldSelectedPosition = INVALID_POSITION;
        mOldSelectedRowId = INVALID_ROW_ID;

        // AbsListView#setAdapter will update choice mode states.
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            checkFocus();

            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver); 
            // 将viewtypecount设置给RecycleBin
            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

            int position;
            // mStackFromBottom Indicates whether the list is stacked from the bottom edge or the top edge.
            if (mStackFromBottom) {
                position = lookForSelectablePosition(mItemCount - 1, false);
            } else {
                position = lookForSelectablePosition(0, true);
            }
            setSelectedPositionInt(position);
            setNextSelectedPositionInt(position);

            if (mItemCount == 0) {
                // Nothing selected
                checkSelectionChanged();
            }
        } else {
            mAreAllItemsSelectable = true;
            checkFocus();
            // Nothing selected
            checkSelectionChanged();
        }
        // 请求layout了,这个就很重要了啊...我们稍后看一下。
        requestLayout();
    }

    /**
     * The list is empty. Clear everything out.
     */
    @Override
    void resetList() {
        // The parent's resetList() will remove all views from the layout so we need to
        // cleanup the state of our footers and headers
        clearRecycledState(mHeaderViewInfos);
        clearRecycledState(mFooterViewInfos);
        // The list is empty. Clear everything out.
        super.resetList();

        mLayoutMode = LAYOUT_NORMAL;
    }

    private void clearRecycledState(ArrayList<FixedViewInfo> infos) {
        if (infos != null) {
            final int count = infos.size();

            for (int i = 0; i < count; i++) {
                final View child = infos.get(i).view;
                final LayoutParams p = (LayoutParams) child.getLayoutParams();
                if (p != null) {
                    // 这里p就是AbsListView.LayoutParams。对于recycledHeaderFooter在注释中是这样说的. 
                    /**
                     * When this boolean is set, the view has been added to the AbsListView
                     * at least once. It is used to know whether headers/footers have already
                     * been added to the list view and whether they should be treated as
                     * recycled views or not.
                     */
                    p.recycledHeaderFooter = false;
                }
            }
        }
    }

我们看到setAdapter()方法中会调用requestLayout()方法。而requestLayout()方法会调用onLayout()方法,但是我们在ListView中找不到onLayout()方法。
那就去他的父类AbsListView中找。我们接着来看一下AbsListView.onLayout()方法的实现。

/**
 * 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;

    final int childCount = getChildCount();
    if (changed) {
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }

    // 调用layoutChildren()方法。
    layoutChildren();
    mInLayout = false;

    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

    // TODO: Move somewhere sane. This doesn't belong in onLayout().
    if (mFastScroll != null) {
        mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
    }
}

我们看一下AbsListView.layoutChildren()方法的实现:

/**
 * Subclasses must override this method to layout their children.
 */
protected void layoutChildren() {
}

实现是空的,文档说的很明白了,子类必须要去实现该方法,这也是应该的,因为ListViewGridView的展现是不一样的.所以我们看一下ListView.layoutChildren()方法的实现:

@Override
protected void layoutChildren() {
    // When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
    // This is used to layout the children during a layout pass.
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }

    // 开始了,置为true
    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
        // mLayoutMode的注释为Controls how the next layout will happen,默认为LAYOUT_NORMAL
        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);
        }

        // mDataChanged变量标记Adapter中数据是否发生变化
        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)) {
                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();
        }

        // Pull all children into the RecycleBin.
        // These views will be reused if possible
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        // RecycleBin控制着整个item的复用,等最后我们再仔细分析下RecycleBin类
        if (dataChanged) {
            // 如果数据发生变化,就把现在的View都添加到一个废弃的View进行缓存,RecycleBin.addScrapView()方法就是缓存一些废弃的View
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            // 调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
            recycleBin.fillActiveViews(childCount, firstPosition);
        }

        // Clear out old views
        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();
        // mLayoutMode默认是LAYOUT_NORMAL
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
            // 第一次调用layoutChildren方法的时候是还没有layout的,所以这时候childCount是0
            if (childCount == 0) {
                // mStackFromBottom的注释Indicates whether the list is stacked from the bottom edge or the top edge.
                if (!mStackFromBottom) {
                    // 默认的布局方式是从上往下的。
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    // 调用fillFromTop方法。
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                // 如果已经有条目了就会走到这里
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    // 没有选中的条目
                    // TODO 一会先分析上面的fillFromTop部分,把其全部分析完后再回来分析这里。
                    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 != 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.onFinishTemporaryDetach();
        }

        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }
        mNeedSync = false;
        setNextSelectedPositionInt(mSelectedPosition);

        updateScrollIndicators();

        if (mItemCount > 0) {
            checkSelectionChanged();
        }

        invokeOnItemScrollListener();
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

上面看到会调用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;
    }
    // 调用fillDown()方法。
    return fillDown(mFirstPosition, nextTop);
}

接下来我们看一下fillDown()方法的实现:

/**
 * 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 = (mBottom - mTop);
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end -= mListPadding.bottom;
    }
    // 用一个循环一直去填充,nextTop与end的比较来判断是否超过当前屏幕了,这就是为什么ListView就算有一万条最开始载入也不卡,因为他只初始化一屏啊。
    // 以后都是边滑动边显示啊,说到这里一会我们还要看一下手势部分的处理。pos和mItemCount的比较来判断当前是否已经把所有条目填充完
    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        // 调用makeAndAddView方法
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}

接下来看一下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 existing 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
            // 最后一个参数为true说明是复用的
            setupChild(child, position, y, flow, childrenLeft, selected, true);

            return child;
        }
    }

    // Make a new view for this position, or convert an unused view if possible
    // 一会我们先看一下obtainView方法,这个方法其实就是创建一个childView
    // obtainView方法中会更改mIsScrap[0]的值,以便下面setupChild()中使用。
    child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured
    // 把childView添加到listview中,这里最后一个参数会是false
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

makeAndAddView()方法里面就是通过RecycleBin来复用View,如果不存在可以复用的View时就创建一个新的。
那我们就先看一下`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) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

    isScrap[0] = false;

    // Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        // Scrap view implies temporary detachment.
        isScrap[0] = true;
        return transientView;
    }
    // getScrapView()方法会从废弃View的缓存中去取,一旦View移除了屏幕就会被加到该废弃缓存中,所以他就是当View移除屏幕了就加到该缓存中
    // 显示的时候再从该缓存中取,就这样进行了复用。
    final View scrapView = mRecycler.getScrapView(position);
    // 调用Adapter.getView()方法了。并且将scrapView作为converview的参数传入。这个方法都挺熟,就不说了。
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else {
            isScrap[0] = true;

            child.dispatchFinishTemporaryDetach();
        }
    }

    if (mCacheColorHint != 0) {
        child.setDrawingCacheBackgroundColor(mCacheColorHint);
    }

    if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
        child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    }
    // 设置该view的layoutparams
    setItemViewLayoutParams(child, position);

    if (AccessibilityManager.getInstance(mContext).isEnabled()) {
        if (mAccessibilityDelegate == null) {
            mAccessibilityDelegate = new ListItemAccessibilityDelegate();
        }
        if (child.getAccessibilityDelegate() == null) {
            child.setAccessibilityDelegate(mAccessibilityDelegate);
        }
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);

    return child;
}

到这里我们就把obtainView()方法都看完了,接下来我们再看一下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) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
            mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    // 这里就是Adapter中getItemViewType的调用处
    p.viewType = mAdapter.getItemViewType(position);

    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
            p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        // 复用的或者headerView及footerView等调用attachViewToParent方法,把之前detach的View重新attach到ViewGroup上
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        // 第一次都是通过该方法来把View添加到ViewGroup中
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }

    if (updateChildSelected) {
        child.setSelected(isSelected);
    }

    if (updateChildPressed) {
        child.setPressed(isPressed);
    }

    if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
        if (child instanceof Checkable) {
            ((Checkable) child).setChecked(mCheckStates.get(position));
        } else if (getContext().getApplicationInfo().targetSdkVersion
                >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            child.setActivated(mCheckStates.get(position));
        }
    }

    if (needToMeasure) {
        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);
    }

    if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
            != position) {
        child.jumpDrawablesToCurrentState();
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

通过分析我们看到核心的部分就是attachViewToParent()方法和addViewInLayout()方法,这两个方法都是父类ViewGroup中的方法,
我们分别来看一下:

/**
 * Attaches a view to this view group. Attaching a view assigns this group as the parent,
 * sets the layout parameters and puts the view in the list of children so that
 * it can be retrieved by calling {@link #getChildAt(int)}.
 * <p>
 * This method is intended to be lightweight and makes no assumptions about whether the
 * parent or child should be redrawn. Proper use of this method will include also making
 * any appropriate {@link #requestLayout()} or {@link #invalidate()} calls.
 * For example, callers can {@link #post(Runnable) post} a {@link Runnable}
 * which performs a {@link #requestLayout()} on the next frame, after all detach/attach
 * calls are finished, causing layout to be run prior to redrawing the view hierarchy.
 * <p>
 * This method should be called only for views which were detached from their parent.
 *
 * @param child the child to attach
 * @param index the index at which the child should be attached
 * @param params the layout parameters of the child
 *
 * @see #removeDetachedView(View, boolean)
 * @see #detachAllViewsFromParent()
 * @see #detachViewFromParent(View)
 * @see #detachViewFromParent(int)
 */
protected void attachViewToParent(View child, int index, LayoutParams params) {
    child.mLayoutParams = params;

    if (index < 0) {
        index = mChildrenCount;
    }

    // 把该view添加到数组中,就像注释所说的为了能够让`getChildAt()`方法能获取到。
    addInArray(child, index);

    child.mParent = this;
    child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
                    & ~PFLAG_DRAWING_CACHE_VALID)
            | PFLAG_DRAWN | PFLAG_INVALIDATED;
    this.mPrivateFlags |= PFLAG_INVALIDATED;

    if (child.hasFocus()) {
        requestChildFocus(child, child.findFocus());
    }
}

复用的处理看完后,我们看一下新创建的childView如何被添加到ListView上,我们看一下addViewInLayout()方法:

/**
 * Adds a view during layout. This is useful if in your onLayout() method,
 * you need to add more views (as does the list view for example).
 *
 * If index is negative, it means put it at the end of the list.
 *
 * @param child the view to add to the group
 * @param index the index at which the child must be added
 * @param params the layout parameters to associate with the child
 * @param preventRequestLayout if true, calling this method will not trigger a
 *        layout request on child
 * @return true if the child was added, false otherwise
 */
protected boolean addViewInLayout(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    child.mParent = null;
    // 这个方法最终会调用addView方法添加childView
    addViewInner(child, index, params, preventRequestLayout);
    child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    return true;
}

到这里已经把上面fillFromTop()方法的部分都分析完了。
不要忘了我们上面还留了一个TODO的部分,就是对于已经有数据的部分调用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,他里面会调用fillUp()fillDown()方法,又会走上面的流程,所以这里就不分析了。

好了,到这里我们就基本已经分析完了,但是感觉好像好少了点什么?   

这里少了不是一点,是两点:

  • 手势滑动过程中ListView的复用处理
  • RecycleBin中复用的具体实现

一个个的来,先看一下手势滑动部分,这个当然是从onTouchEvent()方法看了,
手势这一块不太清楚的可以看我之前的一篇文章
Android Touch事件分发详解

发现ListView没有重写onTouchEvent()方法,这也好理解,因为GridView也有类似的滑动功能,所以去父类AbsListView中看.
我们看一下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) {
        boolean intercepted = mFastScroll.onTouchEvent(ev);
        if (intercepted) {
            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;
}

接着看一下onTouchMove()方法:

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    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:
            // 滑动部分的处理,传入当前位置的x,y坐标
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

再看一下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();
                                }
                                invalidate(0, 0, getWidth(),
                                        mEdgeGlowTop.getMaxHeight() + getPaddingTop());
                            } else if (incrementalDeltaY < 0) {
                                mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
                                        1.f - (float) x / getWidth());
                                if (!mEdgeGlowTop.isFinished()) {
                                    mEdgeGlowTop.onRelease();
                                }
                                invalidate(0, getHeight() - getPaddingBottom() -
                                        mEdgeGlowBottom.getMaxHeight(), getWidth(),
                                        getHeight());
                            }
                        }
                    }
                }
                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();
                        }
                        invalidate(0, 0, getWidth(),
                                mEdgeGlowTop.getMaxHeight() + getPaddingTop());
                    } else if (rawDeltaY < 0) {
                        mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),
                                1.f - (float) x / getWidth());
                        if (!mEdgeGlowTop.isFinished()) {
                            mEdgeGlowTop.onRelease();
                        }
                        invalidate(0, getHeight() - getPaddingBottom() -
                                mEdgeGlowBottom.getMaxHeight(), getWidth(),
                                getHeight());
                    }
                }
            }

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

这部分代码逻辑牵扯太多,有点晕呼呼的,记着看一下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 {
                // 如果这个View的底部位置已经小于top值了,说明这个view已经移除屏幕了,不可见了
                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();
                    // 只要该view移除屏幕就把该view添加到废弃的缓存中
                    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;

    // count记录了当前已经移除屏幕的view个数
    if (count > 0) {
        // 把已经移除的View从ViewGroup总detach掉
        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();
    }

    // 这个方法让所有的子view都按照这个参数的距离大小进行改变,这样就实现了移动也就是滑动的功能。
    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
        mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        // 第一个View的顶部移入了屏幕或者最后一个View的底部移入了屏幕
        fillGap(down);
    }

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

继续看一下offsetChildrenTopAndBottom()方法的,在AbsListView中没有重写该方法,具体要看其父类ViewGroup中的实现: `

/**
 * Offset the vertical location of all children of this view by the specified number of pixels.
 *
 * @param offset the number of pixels to offset
 *
 * @hide
 */
public void offsetChildrenTopAndBottom(int offset) {
    final int count = mChildrenCount;
    final View[] children = mChildren;
    boolean invalidate = false;

    for (int i = 0; i < count; i++) {
        // 把所有的view都移动指定的距离
        final View v = children[i];
        v.mTop += offset;
        v.mBottom += offset;
        if (v.mRenderNode != null) {
            invalidate = true;
            v.mRenderNode.offsetTopAndBottom(offset);
        }
    }

    if (invalidate) {
        invalidateViewProperty(false, false);
    }
    notifySubtreeAccessibilityStateChangedIfNeeded();
}

再看一下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);

发现在AbsListView中该方法是抽象的,所以我们要再ListView中找一下他的具体实现类:

    /**
     * {@inheritDoc}
     */
    @Override
    void fillGap(boolean down) {
        final int count = getChildCount();
        if (down) {
            int paddingTop = 0;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                paddingTop = getListPaddingTop();
            }
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                    paddingTop;
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } else {
            int paddingBottom = 0;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                paddingBottom = getListPaddingBottom();
            }
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                    getHeight() - paddingBottom;
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }

可以看到他会分别根据向下滑动还是向上滑动去调用fillDownfillUp两个方法,这两个方法之前我们都分析过了,就不再继续看了。

到这里基本就把滑动部分的处理都看完了。
还剩最后一个问题就是RecycleBin的实现,他是AbsListView中的一个内部类。

直接上代码了,他的注释也说的非常清楚,我就把不好理解的地方简单说一下:

/**
 * 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];

    /**
     * 为什么是个数组呢?因为ListView会有多种不同的ViewType啊。
     * Unsorted views that can be used by the adapter as a convert view.
     */
    private ArrayList<View>[] mScrapViews;

    private int mViewTypeCount;
    // mScrapViews数组中的第一个元素,方便在ViewType是1的时候使用
    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");
        }
        // 根据ViewType的数量来创建,因为ViewType可以有多中类型,每种不同类型的View肯定要分开单独进行缓存和复用的。
        //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;
    }

    // 方法名说明了一切
    public void markChildrenDirty() {
        if (mViewTypeCount == 1) {
            final ArrayList<View> scrap = mCurrentScrap;
            final int scrapCount = scrap.size();
            for (int i = 0; i < scrapCount; i++) {
                scrap.get(i).forceLayout();
            }
        } else {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                final ArrayList<View> scrap = mScrapViews[i];
                final int scrapCount = scrap.size();
                for (int j = 0; j < scrapCount; j++) {
                    scrap.get(j).forceLayout();
                }
            }
        }
        if (mTransientStateViews != null) {
            final int count = mTransientStateViews.size();
            for (int i = 0; i < count; i++) {
                mTransientStateViews.valueAt(i).forceLayout();
            }
        }
        if (mTransientStateViewsById != null) {
            final int count = mTransientStateViewsById.size();
            for (int i = 0; i < count; i++) {
                mTransientStateViewsById.valueAt(i).forceLayout();
            }
        }
    }

    public boolean shouldRecycleViewType(int viewType) {
        return viewType >= 0;
    }

    /**
     * Clears the scrap heap.
     */
    void clear() {
        if (mViewTypeCount == 1) {
            final ArrayList<View> scrap = mCurrentScrap;
            clearScrap(scrap);
        } else {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                final ArrayList<View> scrap = mScrapViews[i];
                clearScrap(scrap);
            }
        }

        clearTransientStateViews();
    }

    /**
     * Fill ActiveViews with all of the children of the AbsListView.
     * 将View存储到mActiveViews数组中
     * @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.注释说了获取完之后就会从mActiveViews中移除,这是很好理解的
     * 因为获取了就是说这个View已经显示到当前的ListView中了,肯定不能再复用他了。
     *
     * @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;
    }

    View getTransientStateView(int position) {
        if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
            long id = mAdapter.getItemId(position);
            View result = mTransientStateViewsById.get(id);
            mTransientStateViewsById.remove(id);
            return result;
        }
        if (mTransientStateViews != null) {
            final int index = mTransientStateViews.indexOfKey(position);
            if (index >= 0) {
                View result = mTransientStateViews.valueAt(index);
                mTransientStateViews.removeAt(index);
                return result;
            }
        }
        return null;
    }

    /**
     * Dumps and fully detaches any currently saved views with transient
     * state.
     */
    void clearTransientStateViews() {
        final SparseArray<View> viewsByPos = mTransientStateViews;
        if (viewsByPos != null) {
            final int N = viewsByPos.size();
            for (int i = 0; i < N; i++) {
                removeDetachedView(viewsByPos.valueAt(i), false);
            }
            viewsByPos.clear();
        }

        final LongSparseArray<View> viewsById = mTransientStateViewsById;
        if (viewsById != null) {
            final int N = viewsById.size();
            for (int i = 0; i < N; i++) {
                removeDetachedView(viewsById.valueAt(i), false);
            }
            viewsById.clear();
        }
    }

    /**
     * 从废弃View的缓存中获取,只要移动出屏幕之后就都会被加到废弃View的缓存中 
     * @return A view from the ScrapViews collection. These are unordered.
     */
    View getScrapView(int position) {
        if (mViewTypeCount == 1) {
            return retrieveFromScrap(mCurrentScrap, position);
        } else {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap >= 0 && 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) {
            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)) {
            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);

        // 对一些瞬态View的处理
        // 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<View>();
                }
                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<View>();
                }
                mTransientStateViews.put(position, scrap);
            } else {
                // Otherwise, we'll have to remove the view and start over.
                if (mSkippedScrap == null) {
                    mSkippedScrap = new ArrayList<View>();
                }
                mSkippedScrap.add(scrap);
            }
        } else {
            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                mScrapViews[viewType].add(scrap);
            }

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

    /**
     * Finish the removal of any views that skipped the scrap heap.
     */
    void removeSkippedScrap() {
        if (mSkippedScrap == null) {
            return;
        }
        final int count = mSkippedScrap.size();
        for (int i = 0; i < count; i++) {
            removeDetachedView(mSkippedScrap.get(i), false);
        }
        mSkippedScrap.clear();
    }

    /**
     * Move all views remaining in mActiveViews to mScrapViews.
     */
    void scrapActiveViews() {
        final View[] activeViews = mActiveViews;
        final boolean hasListener = mRecyclerListener != null;
        final boolean multipleScraps = mViewTypeCount > 1;

        ArrayList<View> scrapViews = mCurrentScrap;
        final int count = activeViews.length;
        for (int i = count - 1; i >= 0; i--) {
            final View victim = activeViews[i];
            if (victim != null) {
                final AbsListView.LayoutParams lp
                        = (AbsListView.LayoutParams) victim.getLayoutParams();
                final int whichScrap = lp.viewType;

                activeViews[i] = null;

                if (victim.hasTransientState()) {
                    // Store views with transient state for later use.
                    victim.dispatchStartTemporaryDetach();

                    if (mAdapter != null && mAdapterHasStableIds) {
                        if (mTransientStateViewsById == null) {
                            mTransientStateViewsById = new LongSparseArray<View>();
                        }
                        long id = mAdapter.getItemId(mFirstActivePosition + i);
                        mTransientStateViewsById.put(id, victim);
                    } else if (!mDataChanged) {
                        if (mTransientStateViews == null) {
                            mTransientStateViews = new SparseArray<View>();
                        }
                        mTransientStateViews.put(mFirstActivePosition + i, victim);
                    } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                        // The data has changed, we can't keep this view.
                        removeDetachedView(victim, false);
                    }
                } else if (!shouldRecycleViewType(whichScrap)) {
                    // Discard non-recyclable views except headers/footers.
                    if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                        removeDetachedView(victim, false);
                    }
                } else {
                    // Store everything else on the appropriate scrap heap.
                    if (multipleScraps) {
                        scrapViews = mScrapViews[whichScrap];
                    }

                    victim.dispatchStartTemporaryDetach();
                    lp.scrappedFromPosition = mFirstActivePosition + i;
                    scrapViews.add(victim);

                    if (hasListener) {
                        mRecyclerListener.onMovedToScrapHeap(victim);
                    }
                }
            }
        }

        pruneScrapViews();
    }

    /**
     * Makes sure that the size of mScrapViews does not exceed the size of
     * mActiveViews, which can happen if an adapter does not recycle its
     * views. Removes cached transient state views that no longer have
     * transient state.
     */
    private void pruneScrapViews() {
        final int maxViews = mActiveViews.length;
        final int viewTypeCount = mViewTypeCount;
        final ArrayList<View>[] scrapViews = mScrapViews;
        for (int i = 0; i < viewTypeCount; ++i) {
            final ArrayList<View> scrapPile = scrapViews[i];
            int size = scrapPile.size();
            final int extras = size - maxViews;
            size--;
            for (int j = 0; j < extras; j++) {
                removeDetachedView(scrapPile.remove(size--), false);
            }
        }

        final SparseArray<View> transViewsByPos = mTransientStateViews;
        if (transViewsByPos != null) {
            for (int i = 0; i < transViewsByPos.size(); i++) {
                final View v = transViewsByPos.valueAt(i);
                if (!v.hasTransientState()) {
                    removeDetachedView(v, false);
                    transViewsByPos.removeAt(i);
                    i--;
                }
            }
        }

        final LongSparseArray<View> transViewsById = mTransientStateViewsById;
        if (transViewsById != null) {
            for (int i = 0; i < transViewsById.size(); i++) {
                final View v = transViewsById.valueAt(i);
                if (!v.hasTransientState()) {
                    removeDetachedView(v, false);
                    transViewsById.removeAt(i);
                    i--;
                }
            }
        }
    }

    /**
     * Puts all views in the scrap heap into the supplied list.
     */
    void reclaimScrapViews(List<View> views) {
        if (mViewTypeCount == 1) {
            views.addAll(mCurrentScrap);
        } else {
            final int viewTypeCount = mViewTypeCount;
            final ArrayList<View>[] scrapViews = mScrapViews;
            for (int i = 0; i < viewTypeCount; ++i) {
                final ArrayList<View> scrapPile = scrapViews[i];
                views.addAll(scrapPile);
            }
        }
    }

    /**
     * Updates the cache color hint of all known views.
     *
     * @param color The new cache color hint.
     */
    void setCacheColorHint(int color) {
        if (mViewTypeCount == 1) {
            final ArrayList<View> scrap = mCurrentScrap;
            final int scrapCount = scrap.size();
            for (int i = 0; i < scrapCount; i++) {
                scrap.get(i).setDrawingCacheBackgroundColor(color);
            }
        } else {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                final ArrayList<View> scrap = mScrapViews[i];
                final int scrapCount = scrap.size();
                for (int j = 0; j < scrapCount; j++) {
                    scrap.get(j).setDrawingCacheBackgroundColor(color);
                }
            }
        }
        // Just in case this is called during a layout pass
        final View[] activeViews = mActiveViews;
        final int count = activeViews.length;
        for (int i = 0; i < count; ++i) {
            final View victim = activeViews[i];
            if (victim != null) {
                victim.setDrawingCacheBackgroundColor(color);
            }
        }
    }

    private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
        final int size = scrapViews.size();
        if (size > 0) {
            // See if we still have a view for this position or ID.
            for (int i = 0; i < size; i++) {
                final View view = scrapViews.get(i);
                final AbsListView.LayoutParams params =
                        (AbsListView.LayoutParams) view.getLayoutParams();

                if (mAdapterHasStableIds) {
                    final long id = mAdapter.getItemId(position);
                    if (id == params.itemId) {
                        return scrapViews.remove(i);
                    }
                } else if (params.scrappedFromPosition == position) {
                    final View scrap = scrapViews.remove(i);
                    clearAccessibilityFromScrap(scrap);
                    return scrap;
                }
            }
            final View scrap = scrapViews.remove(size - 1);
            clearAccessibilityFromScrap(scrap);
            return scrap;
        } else {
            return null;
        }
    }

    private void clearScrap(final ArrayList<View> scrap) {
        final int scrapCount = scrap.size();
        for (int j = 0; j < scrapCount; j++) {
            removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
        }
    }

    private void clearAccessibilityFromScrap(View view) {
        view.clearAccessibilityFocus();
        view.setAccessibilityDelegate(null);
    }

    private void removeDetachedView(View child, boolean animate) {
        child.setAccessibilityDelegate(null);
        AbsListView.this.removeDetachedView(child, animate);
    }
}

到此为止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值