ListView源码分析缓存机制

转载请注明出处:http://blog.csdn.net/kiddteb/article/details/52573411

前言

  • 中秋假期结束了,愉快的假期之后又要踏上愉快的编程之路了。为了自己学习留下印记,留下文章提供以后回顾复习,有错误希望请指出,谢谢。

分析

  • 我们都会怎么继承一个baseadapter,然后重写那几个方法,再调用相关的setAdpater()方法, 接着item 就显示在手机屏幕上了。然而有些人却不知道背后的缓存机制,也就是 getview()中的回收情况。我们开始从源码角度看一下,首先看源码一定要先明确自己看源码的目的是什么,想在当中了解什么,不然几千几万行的代码不可能一行一行地去全部了解,我们只需要了解当中自己需要了解的那一部分就好了。
  • 先了解一下ListView的继承关系吧。
    ListView–>AbsView–>AdapterView–>ViewGroup
    这就方便我们到时候去追踪和分析函数。

RecycleBin

  • 在分析ListView之前有个东西是首先要了解的,那就是RecycleBin,而在了解RecycleBin之前ActiveView和ScrapView又是先要了解的。另外再插一句,在阅读源码的时候,有时候看看方法的注释,很受用。因为大多数情况下你读懂了英文注释,就能大致了解这个方法是干什么用的,所以你就可以在这个基础上掌握自己要的线索。
/**
         * Views that were on screen at the start of layout. This array is populated at the start of
         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
         * Views in mActiveViews represent a contiguous range of Views, with position of the first
         * view store in mFirstActivePosition.
         */
        private View[] mActiveViews = new View[0];

        /**
         * Unsorted views that can be used by the adapter as a convert view.
         */
        private ArrayList<View>[] mScrapViews;

一看就大概能知道,这两个对象是干什么用的了,mActiveView:它存放的是在每次开始布局之前位于屏幕上的那些item,在布局结束后所有存在于mActiveViews的item都会移到mScrapViews,mActiveViews从mFirstActivePosition位置开始到屏幕所能显示下的最大item数目而且存放的内容是连续的;mScrapView:是一个无序排列的列表,而且可以被传入Adapter中作为一个convert view。

RecycleBin当中还有几个相当重要的函数,但是现在先不列举出来,不然感觉会有点不知所措。先结合ListView来看看


  • 其实,我们了解这个机制,主要想关注两个方面,一个是ListView的Item如何被RecycleBin回收,第二个是如何从RecycleBin中取出Item到ListView。说到底ListView也离不开绘图,说到绘图也就离不开onMeasure,onLayout,还有onDraw这些,那么先来看看onLayout吧。想一想GridView,因为它们存在通用性,那么onLayout一定是在它们的父类AbsListView中实现的。
  • 在onLayout方法中(源码省略不贴),getChildAt(i).forceLayout()强制所有Item进行重绘。最重要的还是当中的一行代码layoutChildren()。layoutChildren()的具体是在ListView当中
@Override
    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (blockLayoutRequests) {
            return;
        }

        mBlockLayoutRequests = true;

        try {
            super.layoutChildren();

            invalidate();

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

            final int childrenTop = mListPadding.top;
            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
            final int childCount = getChildCount();

            int index = 0;
            int delta = 0;

            View sel;
            View oldSel = null;
            View oldFirst = null;
            View newSel = null;

            // Remember stuff we will need down below
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                index = mNextSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    newSel = getChildAt(index);
                }
                break;
            case LAYOUT_FORCE_TOP:
            case LAYOUT_FORCE_BOTTOM:
            case LAYOUT_SPECIFIC:
            case LAYOUT_SYNC:
                break;
            case LAYOUT_MOVE_SELECTION:
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    oldSel = getChildAt(index);
                }

                // Remember the previous first child
                oldFirst = getChildAt(0);

                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition;
                }

                // Caution: newSel might be null
                newSel = getChildAt(index + delta);
            }


            boolean dataChanged = mDataChanged;
            if (dataChanged) {
                handleDataChanged();
            }

            // Handle the empty set by removing all views that are visible
            // and calling it a day
            if (mItemCount == 0) {
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "ListView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only from "
                        + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
                        + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }

            setSelectedPositionInt(mNextSelectedPosition);

            // Remember which child, if any, had accessibility focus.
            final int accessibilityFocusPosition;
            final View accessFocusedChild = getAccessibilityFocusedChild();
            if (accessFocusedChild != null) {
                accessibilityFocusPosition = getPositionForView(accessFocusedChild);
                accessFocusedChild.setHasTransientState(true);
            } else {
                accessibilityFocusPosition = INVALID_POSITION;
            }

            // Ensure the child containing focus, if any, has transient state.
            // If the list data hasn't changed, or if the adapter has stable
            // IDs, this will maintain focus.
            final View focusedChild = getFocusedChild();
            if (focusedChild != null) {
                focusedChild.setHasTransientState(true);
            }

            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // Clear out old views
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }

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

            if (sel != null) {
                final boolean shouldPlaceFocus = mItemsCanFocus && hasFocus();
                final boolean maintainedFocus = focusedChild != null && focusedChild.hasFocus();
                if (shouldPlaceFocus && !maintainedFocus && !sel.hasFocus()) {
                    if (sel.requestFocus()) {
                        // Successfully placed focus, clear selection.
                        sel.setSelected(false);
                        mSelectorRect.setEmpty();
                    } else {
                        // Failed to place focus, clear current (invalid) focus.
                        final View focused = getFocusedChild();
                        if (focused != null) {
                            focused.clearFocus();
                        }
                        positionSelector(INVALID_POSITION, sel);
                    }
                } else {
                    positionSelector(INVALID_POSITION, sel);
                }
                mSelectedTop = sel.getTop();
            } else {
                // If the user's finger is down, select the motion position.
                // Otherwise, clear selection.
                if (mTouchMode == TOUCH_MODE_TAP || mTouchMode == TOUCH_MODE_DONE_WAITING) {
                    final View child = getChildAt(mMotionPosition - mFirstPosition);
                    if (child != null)  {
                        positionSelector(mMotionPosition, child);
                    }
                } else {
                    mSelectedTop = 0;
                    mSelectorRect.setEmpty();
                }
            }

            if (accessFocusedChild != null) {
                accessFocusedChild.setHasTransientState(false);

                // If we failed to maintain accessibility focus on the previous
                // view, attempt to restore it to the previous position.
                if (!accessFocusedChild.isAccessibilityFocused()
                    && 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();
                    }
                }
            }

            if (focusedChild != null) {
                focusedChild.setHasTransientState(false);
            }

            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;
            }
        }
    }
  • 源码很长,但只需要看关键的地方。第110-116行的判断,DataChanged的判断,在第一次绘制的时候布局的元素都还没出来,DataChange又是默认为false的,所以执行的是第二个。另外一说,DataChange这个变量的改变是指数据源的变化,也就是当我们使用adapter.notifyDataSetChanged方法的时候回使它变为true。回归正题,执行第二个就会执行recycleBin.fillActiveViews(childCount, firstPosition);这个方法,这个就是RecycleBin关键的函数之一
/**
         * Fill ActiveViews with all of the children of the AbsListView.
         *
         * @param childCount The minimum number of views mActiveViews should hold
         * @param firstActivePosition The position of the first view that will be stored in
         *        mActiveViews
         */
        void fillActiveViews(int childCount, int firstActivePosition) {
            //......
        }

我们又看看注释,大概也能知道这个方法的作用:根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。因为是第一次绘制,所以孩子的数目为0,那么调用这个方法暂时还没有效果,因此接着看下去。

// Clear out old views
detachAllViewsFromParent();

这行代码主要是将ListView当中的所有子View清除掉,保证第二次Layout过程不会产生一份重复的数据。由于还是第一次绘制,孩子数目为0,所以这个方法还没有作用,到时在后面的第二次绘制的时候再了解它吧。那又接着往下看是一个switch判断,在默认布局模式的情况下会执行default:那就先来看看重点先,其它的省略先。

if (childCount == 0) {
     if (!mStackFromBottom) {
         final int position = lookForSelectablePosition(0, true);
         setSelectedPositionInt(position);
         sel = fillFromTop(childrenTop);
     } else {
         final int position = lookForSelectablePosition(mItemCount - 1, false);
         setSelectedPositionInt(position);
         sel = fillUp(mItemCount - 1, childrenBottom);
     }
} 

这块代码主要是根据方向来决定如何绘制,其实功能差不多只是方向问题,那我们就只是跟踪一下fillFromTop方法吧。进去这个方法之后看看注释Fills the list from top to bottom, starting with mFirstPosition很容易读懂就是从上到下绘制,但是实际的操作是在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;
        }

        while (nextTop < end && pos < mItemCount) {
            // is this the selected item?
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

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

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

在第20-30行中有个while循环,在那里就是从屏幕的第一个元素开始,遍历填充每个元素。当中的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
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);

        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

由于之前那个fillActiveViews方法没有起作用,所以是调用了32行的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) {
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            // Scrap view implies temporary detachment.
            isScrap[0] = true;
            return transientView;
        }

        final View scrapView = mRecycler.getScrapView(position);
        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;

                // Clear any system-managed transient state so that we can
                // recycle this view and bind it to different data.
                if (child.isAccessibilityFocused()) {
                    child.clearAccessibilityFocus();
                }

                child.dispatchFinishTemporaryDetach();
            }
        }

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

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

        if (mAdapterHasStableIds) {
            final ViewGroup.LayoutParams vlp = child.getLayoutParams();
            LayoutParams lp;
            if (vlp == null) {
                lp = (LayoutParams) generateDefaultLayoutParams();
            } else if (!checkLayoutParams(vlp)) {
                lp = (LayoutParams) generateLayoutParams(vlp);
            } else {
                lp = (LayoutParams) vlp;
            }
            lp.itemId = mAdapter.getItemId(position);
            child.setLayoutParams(lp);
        }

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

由于是第一次绘制,所以第20行和39行返回的View是null的,所以执行第40行的时候,是不是感觉熟悉了一点,那就是我们平时在Adapter中重写的geiView方法。由于第二个参数是null,所以要inflate加载布局,这个时候回比较慢,但是到后面就不用再次加载了,运行起来也会流畅很多,这也就是缓存机制的好处之一。执行完obtainView之后,获得View,然后调用setupChild方法,第一次绘制是调用里面的addViewInLayout,主要作用是将View添加到ListView当中,就不贴源码出来了

到这里第一次绘制就基本结束了,那么接下来的绘制跟第一次又有什么不同呢?我们重新来看看layoutChild方法,由于孩子数目不再为0,那么将执行fillActiveViews,将孩子Item添加到mActiveView当中,接着调用detachAllViewsFromParent();来清楚旧的View,目的在上面已经说了。接着在default:中执行fillSpecific方法,看看这方法的注释Put a specific item at a specific location on the screen and then build up and down from there 意思是会从指定的位置开始加载Child item。接着又执行makeAndAddView这次的逻辑又是不一样的了,调用mRecycler.getActiveView的时候返回的不是空,所以在这里是直接调用setupChild,所以就避免了执行obtainView从而避免再次加载inflate布局。当中的setupChild()执行的逻辑又有点不一样了,执行的是attachViewToParent(),其实作用是差不多的,区别不过是,向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,想将detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。还记得之前调用的detachAllViewsFromParent()方法吗?就是在那里,使得孩子View变成detach状态了。


  • 最后稍微总结函数的跟踪过程吧,有点多。
    onLayout–>layoutChild–>fillActiveViews(–>fillSpecific–>)fillFromTop–>fillDown–>makeAndAddView–>getActiveView–>setupChild–>attachViewToParent

讲了这么多其实现在才是最关键的最核心的地方,在滑动的时候如何缓存?看过上篇文章就知道,我们的交互事件会被分发到onTouchEvent当中,而我们只需要关注滑动的时候,所以只需要看ACTION_MOVE这一部分,然后我们进行方法追踪,onTouchMove–>scrollIfNeede–>trackMotionScroll,看看方法名字就能大概知道是滑动跟踪的意思,有时候我们在自己命名自己方法名字的时候可以模仿一下源码的写法,这样别人在阅读自己的代码的时候,一眼就大概能知道你这个方法是什么作用的,提高自己的代码可读性。现在我们来看看这个关键的方法里面干了什么。

/**
     * Track a motion scroll
     *
     * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
     *        began. Positive numbers mean the user's finger is moving down the screen.
     * @param incrementalDeltaY Change in deltaY from the previous event.
     * @return true if we're already at the beginning/end of the list and have nothing to do.
     */
    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return true;
        }

        final int firstTop = getChildAt(0).getTop();
        final int lastBottom = getChildAt(childCount - 1).getBottom();

        final Rect listPadding = mListPadding;

        // "effective padding" In this case is the amount of padding that affects
        // how much space should not be filled by items. If we don't clip to padding
        // there is no effective padding.
        int effectivePaddingTop = 0;
        int effectivePaddingBottom = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            effectivePaddingTop = listPadding.top;
            effectivePaddingBottom = listPadding.bottom;
        }

         // FIXME account for grid vertical spacing too?
        final int spaceAbove = effectivePaddingTop - firstTop;
        final int end = getHeight() - effectivePaddingBottom;
        final int spaceBelow = lastBottom - end;

        final int height = getHeight() - mPaddingBottom - mPaddingTop;
        if (deltaY < 0) {
            deltaY = Math.max(-(height - 1), deltaY);
        } else {
            deltaY = Math.min(height - 1, deltaY);
        }

        if (incrementalDeltaY < 0) {
            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
        } else {
            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
        }

        final int firstPosition = mFirstPosition;

        // Update our guesses for where the first and last views are
        if (firstPosition == 0) {
            mFirstPositionDistanceGuess = firstTop - listPadding.top;
        } else {
            mFirstPositionDistanceGuess += incrementalDeltaY;
        }
        if (firstPosition + childCount == mItemCount) {
            mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
        } else {
            mLastPositionDistanceGuess += incrementalDeltaY;
        }

        final boolean cannotScrollDown = (firstPosition == 0 &&
                firstTop >= listPadding.top && incrementalDeltaY >= 0);
        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
                lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

        if (cannotScrollDown || cannotScrollUp) {
            return incrementalDeltaY != 0;
        }

        final boolean down = incrementalDeltaY < 0;

        final boolean inTouchMode = isInTouchMode();
        if (inTouchMode) {
            hideSelector();
        }

        final int headerViewsCount = getHeaderViewsCount();
        final int footerViewsStart = mItemCount - getFooterViewsCount();

        int start = 0;
        int count = 0;

        if (down) {
            int top = -incrementalDeltaY;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                if (child.getBottom() >= top) {
                    break;
                } else {
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        if (child.isAccessibilityFocused()) {
                            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.
                        if (child.isAccessibilityFocused()) {
                            child.clearAccessibilityFocus();
                        }
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        }

        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

        mBlockLayoutRequests = true;

        if (count > 0) {
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        }

        // invalidate before moving the children to avoid unnecessary invalidate
        // calls to bubble up from the children all the way to the top
        if (!awakenScrollBars()) {
           invalidate();
        }

        offsetChildrenTopAndBottom(incrementalDeltaY);

        if (down) {
            mFirstPosition += count;
        }

        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }

        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
            final int childIndex = mSelectedPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(mSelectedPosition, getChildAt(childIndex));
            }
        } else if (mSelectorPosition != INVALID_POSITION) {
            final int childIndex = mSelectorPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(INVALID_POSITION, getChildAt(childIndex));
            }
        } else {
            mSelectorRect.setEmpty();
        }

        mBlockLayoutRequests = false;

        invokeOnItemScrollListener();

        return false;
    }

这个方法传入了两个参数,deltaY表示从手指最初按下时的位置到当前手指所处的位置,而incrementalDeltaY则表示相邻两次在Y方向上位置的改变量,那就可以从这个参数的正负值就可以判断我们手指滑动的方向了。正数表示向上滑,负数则是向下滑。第96行和119行都是判断有没有子View被滑出屏幕,如果有的话,则调用RecycleBin.addScrapView方法将这个View加入到废弃缓存当中。addScrapView也是RecycleBin关键方法之一

/**
         * 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) {
            //。。。
        }

我们又看看注释,大概也能知道这个方法的作用:将一个废弃的View进行缓存,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存。其实,RecycleBin中还有一个mCurrentScrap来存储废弃的View。仔细留意addScrapView方法中有

。。。
if (mViewTypeCount == 1) {
     mCurrentScrap.add(scrap);
}
。。。

那么mViewTypeCount == 1 又是怎么确定的呢?

public void setViewTypeCount(int viewTypeCount) {
            。。。
            mViewTypeCount = viewTypeCount;
            。。。
        }

RecycleBin中还有这个方法,其实我们都知道,在Adapter中可以通过重写一个getViewTypeCount()来判定ListView中有几种类型的数据项,setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。因为这用得不多,如果不知道,其实也不很影响学习这个机制。

  • 我们现在回过头来想想,有一个子View被滑出了屏幕,那是不是应该有一个子View被添加到屏幕呢?那这个逻辑在哪个方法里面呢?还是来trackMotionScroll看看,在153行-155行当中,有个if判断,判断是否底部有个View滑入屏幕,或者顶部有个View滑入屏幕,如果是就调用fillGap方法,这个方法的具体实现是在ListView里面的,但是我们可以看看它再父类里面的英文注释
/**
     * Fills the gap left open by a touch-scroll. During a touch scroll, children that
     * remain on screen are shifted and the other ones are discarded. The role of this
     * method is to fill the gap thus created by performing a partial layout in the
     * empty space.
     *
     * @param down true if the scroll is going down, false if it is going up
     */
    abstract void fillGap(boolean down);
void fillGap(boolean down) {
        final int count = getChildCount();
        if (down) {
            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());
        }
    }

在这个方法中,可以看到熟悉的地方,fillDown和fillUp,这又跟上面的流程差不多,但是有一个地方需要注意,就是在makeAndAddView里面调用getActiveView返回的是null。只要你细心看看这个方法,就会发现,用过一次之后会将View置为null,也就是不能重复利用,所以还是要走obtainView流程,从通过getScrapView中获取废弃的View,如果没有就要inflate一个。所以呢不管你有多条数据需要显示,ListView中的子View来来回回就那几个。


总结

  • ListView的缓存机制大概就结束了,RecyclerView类似的缓存机制,下次再总结起来比较一下。
    这里写图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值