ListView 源码 回收机制分析

  • Post author:
  • Post category:其他


在网上 看了不少关于 ListView 的源码解读
回收机制相关的文章.
但是基本上就是看了就忘
然后自己也去浅浅的看了下 ListView 源码
然后自己 结合 网上看的文章 和自己看的源码 写下了这片文章.

首先看下  ListView 继承关系图:


从上图 可以 基本了解 大致继承结构.
我们这里主要 看 ListView 和 AbsListView
这片文章 不可能 讲的 ListView  所有东西 都面面俱到.
主要是我也没有看的那么细的代码.
我主要 看了一下几点:
  1. view 的回收机制
  2. item 是怎样被布局到 Listview上去
  3. 滑动过程中 是怎样 看不见的View  和 新出现的View
我们先沿着 setAdapter () 这个方法看下去:
@Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        // 回收器 clean
        mRecycler.clear();

        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            // 如果 有head fotter 则 适配一下  都是用 adapter  装饰者模式
            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) {
            // 默认 true
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            // 获取焦点
            checkFocus();

            // 注册新的数据观察着
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            // 回收器 设置 mAdapter 的 类型数量  多数情况下 是 1
            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

            int position;
            if (mStackFromBottom) {
                // 是否是 栈 从底部开始 寻找数据 默认不是
                position = lookForSelectablePosition(mItemCount - 1, false);
            } else {
                // 从0 开始寻找 可以被选中的位置 貌似用处不大
                position = lookForSelectablePosition(0, true);
            }

            // TODO 设置选中位置
            setSelectedPositionInt(position);
            setNextSelectedPositionInt(position);

            if (mItemCount == 0) {
                // Nothing selected
                // 选中状态改变
                checkSelectionChanged();
            }
        } else {
            mAreAllItemsSelectable = true;
            checkFocus();
            // Nothing selected
            checkSelectionChanged();
        }

        // 重新布局
        requestLayout();
    }
关于 选中位置  selectedPosition 相关的代码 我们可以暂时都不去细看 一笔带过,
看源码 需要 沿着一条主线走下去, 一些方法看名字 知道个代码 和主线无关的 不必去过多深究, 不然 这源码看的 能头疼死.
可以看到 setAdapter 主要就是一些 clean 之前数据
然后调用 requestLayout  请求 布局
ok 下面来看布局
先看父View

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    mInLayout = true;

    // 第一次布局画的这里是 0
    final int childCount = getChildCount();
    if (changed) {
        // 如果 布局改变了
        for (int i = 0; i < childCount; i++) {
            // 子VIew 重新布局
            getChildAt(i).forceLayout();
        }
        // 让回收器中的View 页重新布局
        mRecycler.markChildrenDirty();
    }

    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 的OnLayout 方法中可以看到
如果有改变的话,则让所有的View 子View 回收器中的View 都 重新强制布局 forceLayout 方法
还有一个主要方法是 layoutChildren 方法
protected void layoutChildren() {
}
一个空的实现
具体则需要看 ListView 中的layoutChildren 方法
@Override
protected void layoutChildren() {
    // 锁住布局
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }
    // 我开始布局了 直到我这次布局结束, 其他的调用布局的方法 无效
    mBlockLayoutRequests = true;

    try {
        super.layoutChildren();

        invalidate();

        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }

        // 获取 除去padding之后的位置
        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.onStartTemporaryDetach();
                }
            }
            requestFocus();
        }

        // Pull all children into the RecycleBin.
        // These views will be reused if possible
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        // 如果数据发生改变 则所有的View 回收
        // 否则 把View 加入到 activeView 中去
        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();
        // 清除 跳过的View
        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:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    // 默认是走这一步
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    // 从顶部开始布局  这里的 childrenTop  是 ListVIew 顶部的位置 从这个位置 开始布局ziView
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }

        // Flush any cached views that did not get reused above
        recycleBin.scrapActiveViews();

        if (sel != null) {
            // The current selected item should get focus if items are
            // focusable.
            if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
                final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
                        focusLayoutRestoreView != 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;
        }
    }
}
这个方法确实 挺多 挺长的….
但是很多的地方我们都可以直接一眼带过.
基本看我代码上 注释的地方就好
下面说一下主要几点:
// 如果数据发生改变 则所有的View 回收
// 否则 把View 加入到 activeView 中去
if (dataChanged) {
    for (int i = 0; i < childCount; i++) {
        recycleBin.addScrapView(getChildAt(i), firstPosition+i);
    }
} else {
    recycleBin.fillActiveViews(childCount, firstPosition);
}


一开始 childCount 为0 时:

if (childCount == 0) {
    //一开始 没有ziView  走这一步
    if (!mStackFromBottom) {
        // 冲上往下 布局
        final int position = lookForSelectablePosition(0, true);
        setSelectedPositionInt(position);
        // 从顶部开始布局  这里的 childrenTop  是 ListVIew 顶部的位置 从这个位置 开始布局子View
        sel = fillFromTop(childrenTop);
    } else {
        final int position = lookForSelectablePosition(mItemCount - 1, false);
        setSelectedPositionInt(position);
        sel = fillUp(mItemCount - 1, childrenBottom);
    }
}


当有了子View后

// 有子VIew了 走这一步
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);
}
主要的方法 在:
fillFromTop 和 fillSpecific
所以接下来先看 fillFromTop 方法:
private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    // 上面是 处理 第一个位置
    return fillDown(mFirstPosition, nextTop);
}


继续跟进 fillDown 方法

/**
 * Fills the list from pos down to the end of the list view.
 *
 * 可以看到,这里使用了一个while循环来执行重复逻辑,
 * 一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,
 * pos则是刚刚传入的mFirstPosition的值,
 * 而end是ListView底部减去顶部所得的像素值,
 * mItemCount则是Adapter中的元素数量。
 * 因此一开始的情况下nextTop必定是小于end值的,
 * 并且pos也是小于mItemCount值的。
 * 那么每执行一次while循环,pos的值都会加1,
 * 并且nextTop也会增加,
 * 当nextTop大于等于end时,
 * 也就是子元素已经超出当前屏幕了,
 * 或者pos大于等于mItemCount时,
 * 也就是所有Adapter中的元素都被遍历结束了,
 * 就会跳出while循环。
 *
 *
 * 这里 是listView 布局的重点方法
 *
 * @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
 *                表示 下一个位置 的View 顶部
 *
 * @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 在依次增大
    // pos 也在一次增大

    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        // 这里获取View  且这些View 都是测量过的  被添加到  listview中
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        // 下一个 item 的位置
        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        // 位置 增加
        pos++;
    }

    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}
通过这个 方法 就把 一屏 的 View 给布局完了
fillDown 这个方法是从上往下 布局
还有一个从下往上的 布局方法 fillUp
/**
 * Fills the list from pos up to the top of the list view.
 * 往上填充View
 *
 * @param pos The first position to put in the list
 *
 * @param nextBottom The location where the bottom of the item associated
 *        with pos should be drawn
 *                   表示 最底部的坐标
 *
 * @return The view that is currently selected
 */
private View fillUp(int pos, int nextBottom) {
    View selectedView = null;

    int end = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end = mListPadding.top;
    }

    while (nextBottom > end && pos >= 0) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
        nextBottom = child.getTop() - mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos--;
    }

    mFirstPosition = pos + 1;
    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.
 *
 * 获取 要添加的VIew
 *
 * @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
        // 从之前填充过的 actviveView 中获取一个View
        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
    // 如果没有从回收器中获取 那么会去 产生一个VIew
    child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured
    // 这里需要重新测量
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

里面主要有两个方法 setupChild  obtainView 方法
setupChild 方法主要是吧View 被加到 LisitView 中
obtainView  而是 产生一个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.
    // 获取瞬间状态的View
    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.
            // 两个View 不相等 表示 数据重新绑定失败  可能是 adapter 写的不规范导致的, 那么
            // 新生成的 updatedView  放入回收器
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }
        // 获取的是 临时状态列表中的View 设置 为是回收的  然后在后续方法 中 就不用 再次重新测量了
        isScrap[0] = true;

        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    // 获取一个 回收的View  可能是null
    final View scrapView = mRecycler.getScrapView(position);
    // 从adpater  然后 重新绑定数据
    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;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

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

    if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
        child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    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 方法 就是获取View 的主要方法.
从 RecycleBin 的逻辑中 获取View
当然 可能获取到时null
然后调用 adapter的 getView 方法 这个我们熟悉
接下来看 setupChild 方法
/**
 * Add a view as a child and make sure it is measured (if necessary) and
 * positioned properly.
 *
 * 吧 adpater中获取的View 加入到 ListView 中
 *
 * @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();
    }
    // 保存type信息
    p.viewType = mAdapter.getItemViewType(position);

    // 如果是 从recyclebin 中获取的 且不是被强制添加 || 是 footer 和 header
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        // 添加 该View 直接 attached
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }

        //新增 一个VIew 添加
        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) {
        final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
                    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);
}
上面可以看到 如果是 回收的View 那么就直接attached 到 ListView上 通过
attachViewToParent(child, flowDown ? -1 : 0, p); 方法
如果不是回收的view 那么表示是一个新的View 那么就需要重 addViewInLayout 把这个View 加入到 Listview中
接下来看 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
    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) {
        //然后 后续 往上  往下加载View
        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;
    }
}
这个方法其实 就是 从一个位置开始 然后往上 往下 布局  调用 fillUp 和fillDown 方法
ok  现在 基本了解 item 是怎么一步步的布局到  ListVIew 中了
ok  现在来看下一个 问题
我们都知道 ListView 在滑动的过程中
如果一个View 被划出去了, 看不见了 那么这个View 就被回收了
如果一个View  被划进来了, 那么 我们取得是之前回收的View, 然后重新 绑定数据, 而不是却创建一个 新的View
这就是 最基本的  回收逻辑
所以下面 来看代码:
关于滑动这块
我们看 onTouchEvent方法 在 ABSListView中
@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (!isEnabled()) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return isClickable() || isLongClickable();
    }

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


我们主要看 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:
            // 滑动
            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();
                                }
                                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 (incrementalDeltaY != 0) {
    // 跟踪滑动
    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}

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被 移除去了
                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) {
        // 处理移出去的View
        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;
}
这个方法也不少啊..
我们只需要看下面的重点:
for (int i = 0; i < childCount; i++) {
    final View child = getChildAt(i);
    if (child.getBottom() >= top) {
        break;
    } else {
        // 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();
            mRecycler.addScrapView(child, position);
        }
    }
}
可以看到 会去遍历View , 然后如果View 被移除了 会回收
然后有 新的View进来 走的是这里:
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
    // 有新的view 进来 往上 或 往下 填充 View
    fillGap(down);
}


走了 fillGap 方法,我们跟进去看:

/**
 * {@inheritDoc}
 *
 *
 * 往上 或 往下 填充View
 */
@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());
    }
}
基本就是 往上 往下 布局View , 这样行进来的View 就能够被添加进来了
对了 还有看下  listview的滑动
在 trackMotionScroll 中调用了这个方法:
// 把所有的子View 移动到指定位置
offsetChildrenTopAndBottom(incrementalDeltaY);
移动 子View 的方法, 这样就实现了 Listview的滑动.
对了 有一点需要注意的 Listview 不是 和平常 Viewgroup 那样 通过 scrollTo scrollBy 方法来移动的.
ok 移动过重 回收view 和 取 新的View 大致过程就是这样,
代码中很多 但是需要抓住主线看, 其他的代码 与这次主线无关 可以一带而过.
ok 下面 在来说说 RecycleBin 这个类
可以直接 开我在代码上的注释:
/**
 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
 * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
 * start of a layout. By construction, they are displaying current information. At the end of
 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
 * could potentially be used by the adapter to avoid allocating views unnecessarily.
 *
 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
 * @see android.widget.AbsListView.RecyclerListener
 */
class RecycleBin {
    private RecyclerListener mRecyclerListener;

    /**
     * The position of the first view stored in mActiveViews.
     */
    private int mFirstActivePosition;

    /**
     * Views that were on screen at the start of layout. This array is populated at the start of
     * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
     * Views in mActiveViews represent a contiguous range of Views, with position of the first
     * view store in mFirstActivePosition.
     */
    private View[] mActiveViews = new View[0];

    /**
     * Unsorted views that can be used by the adapter as a convert view.
     */
    // 这里是集合数组 数组长度为mViewTypeCount 表示 不同 type对应的 集合
    private ArrayList<View>[] mScrapViews;

    // type数量
    private int mViewTypeCount;

    // 当前type  对应的集合
    private ArrayList<View> mCurrentScrap;

    // 一下表示  三种状态 下的  view
    //Otherwise, we'll have to remove the view and start over.  这种状态的下View  后面都会被删掉
    private ArrayList<View> mSkippedScrap;

    // If the data hasn't changed, we can reuse the views at their old positions.
    private SparseArray<View> mTransientStateViews;

    //If the adapter has stable IDs,we can reuse the view forthe same data.
    private LongSparseArray<View> mTransientStateViewsById;

    // 设置 type 数量  默认是1  很多情况下 也是1
    public void setViewTypeCount(int viewTypeCount) {
        if (viewTypeCount < 1) {
            throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
        }
        //noinspection unchecked
        ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
        for (int i = 0; i < viewTypeCount; i++) {
            scrapViews[i] = new ArrayList<View>();
        }
        mViewTypeCount = viewTypeCount;
        mCurrentScrap = scrapViews[0];
        mScrapViews = scrapViews;
    }

    /**
     * 回收器中的View 也要重新布局
     */
    public void markChildrenDirty() {
        // 让 废弃的View 自己布局 forceLayout 不会调用父类的布局, 所以不会影响 listView 布局情况
        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();
            }
        }
    }

    /**
     * 判断 View 是否 可以被回收 header  和 footer 不会被回收
     * @param viewType
     * @return
     */
    public boolean shouldRecycleViewType(int viewType) {
        return viewType >= 0;
    }

    /**
     * Clears the scrap heap.
     * 清除 废弃的View 且 detach 这些View
     */
    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.
     * <p/>
     * fillActiveViews() 这个方法接收两个参数,
     * 第一个参数表示要存储的view的数量,
     * 第二个参数表示ListView中第一个可见元素的position值。
     * RecycleBin当中使用mActiveViews这个数组来存储View,
     * 调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到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.
     *
     * getActiveView() 这个方法和fillActiveViews()是对应的,
     * 用于从mActiveViews数组当中获取数据。
     * 该方法接收一个position参数,表示元素在ListView当中的位置,
     * 方法内部会自动将position值转换成mActiveViews数组对应的下标值。
     * 需要注意的是,mActiveViews当中所存储的View,
     * 一旦被获取了之后就会从mActiveViews当中移除,
     * 下次获取同样位置的View将会返回null,
     * 也就是说mActiveViews不能被重复利用。
     *
     * @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) {
            // 取走之后 就置为null
            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.
     *
     * 清除Transient  状态的 View
     */
    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();
        }
    }

    /**
     * @return A view from the ScrapViews collection. These are unordered.
     *
     * 用于从废弃缓存中取出一个View,
     * 这些废弃缓存中的View是没有顺序可言的,
     * 因此getScrapView()方法中的算法也非常简单,
     * 就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
     *
     */
    View getScrapView(int position) {
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap < 0) {
            return null;
        }
        // retrieveFromScrap 方法 会尽量找原来的位置的View
        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.
     *
     * addScrapView() 用于将一个废弃的View进行缓存,
     * 该方法接收一个View参数,
     * 当有某个View确定要废弃掉的时候(比如滚动出了屏幕),
     * 就应该调用这个方法来对View进行缓存,
     * RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
     *
     * @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.
            // 如果不可以回收 , 且 不是 head 和 footer 那么 跳过这个View
            // 且 稍后 会完全把这个VIew  删除掉
            if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                getSkippedScrap().add(scrap);
            }
            return;
        }

        //Detach
        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.
        // 不回收瞬间状态的View
        final boolean scrapHasTransientState = scrap.hasTransientState();
        if (scrapHasTransientState) {
            // 如果是 瞬间状态  把它加入到相应的瞬间状态的集合中去
            if (mAdapter != null && mAdapterHasStableIds) {
                // 有唯一的id
                // 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) {
                // 如果数据没有改变, 那么直接加到 mTransientStateViews
                // 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 {
                //否则  跳过这个View  稍后 会删除这个View
                // Otherwise, we'll have to remove the view and start over.
                getSkippedScrap().add(scrap);
            }
        } else {
            // 不是瞬间状态
            // 加到 相应类型的 ViewType 的 view中
            if (mViewTypeCount == 1) {
                // 多数情况下是这个
                mCurrentScrap.add(scrap);
            } else {
                // 当有多个VIewTYpe 类型的时候 使用这个
                mScrapViews[viewType].add(scrap);
            }

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

    /**
     * 获取  回收时 跳过的View 稍后会删除
     * @return
     */
    private ArrayList<View> getSkippedScrap() {
        if (mSkippedScrap == null) {
            mSkippedScrap = new ArrayList<>();
        }
        return mSkippedScrap;
    }

    /**
     * Finish the removal of any views that skipped the scrap heap.
     *
     * 把 之前跳过的View 都删除掉
     */
    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.
     *
     * 吧所有的 mActiveViews 移到用 回收 View 容器中
     */
    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++) {
                // 删除 比 mActiveViews 多出来的view
                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.
     * 为所有的View  绘制北京颜色
     * @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);
            }
        }
    }

    /**
     * 这里的算法 是尽量 找原来位置 相应的 VIew
     * 如果找不到,
     * @param scrapViews
     * @param position
     * @return
     */
    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) {
                    // 如果有瞬间状态的ids
                    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);
    }
}



版权声明:本文为chen930724原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。