ListView原理分析

本文详细剖析了Android中ListView的工作原理,包括RecyclerBin的作用、测量(Measure)、布局(Layout)过程以及事件处理(OnTouchEvent)。重点介绍了RecyclerBin如何实现View的复用,避免内存溢出。ListView的measure过程遵循ViewGroup的规则,而layout过程则相对复杂,涉及到View的添加和布局。在滑动事件处理中,ListView通过trackMotionScroll方法调整子View并填充新的View。通过对ListView的深入了解,开发者可以更好地优化列表性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ListView工作原理分析

ListView可谓是我们在Android开发中使用频率最高的组件之一了,并且,相比TextView、Buton等组件而言,ListView的使用也更复杂,因此,了解ListView的工作原理将使得我们能更好的使用ListView。此外,ListView一般用来以列表形式展示数据,但是在某些情况下,ListView要处理的数据可能多达数万条,但是并不会发生OOM,这也是ListView的特别之处,本篇文章将一步步讲解ListView的工作原理,揭开ListView神秘的面纱。

ListView是AbsListView的子类,AbsListView继承自AdapterView,而AdapterView又继承自ViewGroup,因此,无论ListView如何特别,它也是一个ViewGroup。因此,ListView也要实现自己的traversal过程,也即measure、layout以及draw,measure和draw过程都没有什么特别的,layout过程相对而言比较复杂。此外,ListView还要处理滑动事件,在用户滑动屏幕时进行View的切换。下面我们就一块来看一下ListView的traversal过程以及滑动事件的处理。

RecyclerBin

在分析ListView之前,不得不先了解一下RecyclerBin。RecyclerBin是ListView展示成千上万条数据而不发生OOM的根本所在。RecyclerBin是AbsListView的内部类,它负责View的复用,View在RecycleBin中分两级存储:ActiveViews和ScrapViews。ActiveViews是在进行layout之前正在屏幕上显示的View,layout之后会被放入到ScrapViews中,ActiveViews在存储在数组中;而ScrapViews被无序存储在ArrayList中,它被Adapter使用,当调用Adapter的getView方法时,会从ScrapViews中拿出一个View作为convertView传递给getView,避免分配过多不必要的View。

下面看一下RecyclerBin中主要的方法:

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

    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >=0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }

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

                    lp.scrappedFromPosition = mFirstActivePosition + i;
                    removeDetachedView(victim, false);
                    scrapViews.add(victim);

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

    View getScrapView(int position) {
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap < 0) {
            return null;
        }
        if (mViewTypeCount == 1) {
            return retrieveFromScrap(mCurrentScrap, position);
        } else if (whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
        return null;
    }
  • fillActiveViews方法会将ListView中出了Footer和Header之外的所有的View都添加到mActiveViews中;
  • getView方法用于从mActiveViews中获取一个View,注意,获取之后立即从mActiveViews中移除;
  • scrapActiveViews方法将mActiveViews中的View全部移动到mCurrentScrap中;
  • getScrapView方法从mCurrentScrap中获取一个View(whichScrap默认是0),获取的View是与position相同的一个或者是最后一个;

Measure过程

ListView的measure过程没有什么特别之处,毕竟ListView也是View,遵循继承ViewGroup实现自定义View的套路,而且,大多时候ListView都是match_parent。下面直接看一下onMeasure方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
    }

该方法中SpecMode一般不为UNSPECIFIED,因此childState也总是0,那么ListView的width即为widthSize,也即是MATCH_PARENT的效果;而如果heightMode为AT_MOST,那么高度为heightSize 与数据集中元素个数 * 子View高度 中的最小值:

  • 如果ListView的width为wrap_content,则与match_parent效果相同;
  • 如果ListView的height为wrap_content,则高度为min(heightSize, 数据集中元素个数 * 子View高度);

Layout过程

相比之下,layout过程要稍显复杂。ListView的layout过程实现在父类AbsListView中,看一下AbsListView中的onLayout方法:

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

        mInLayout = true;

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

        layoutChildren();
        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);
        }
    }

可以看到,onLayout方法只是简单的将layout逻辑交给了layoutChildren来处理,而layoutChildren实现为空,很明显,layoutChildren方法是为了让子类来实现自己的布局逻辑。ListView中的layoutChildren方法比较长,因此我对layoutChildren方法进行了精简,只留下了布局逻辑相关的部分,下面我们来看一下精简后的layoutChildren方法:

    protected void layoutChildren() {

        ……//super.layoutChildren()

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

        ……//记录选中的View、处理数据集为空的情况、记录有访问性焦点的View

        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {                               //2
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);               //3
        }

        detachAllViewsFromParent();                                              //4
        recycleBin.removeSkippedScrap();

        switch (mLayoutMode) {
        ……//其它几个case,默认mLayoutMode为LAYOUT_DEFAULT,对应default情况
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);                              //5
                } 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;
        }

        recycleBin.scrapActiveViews();

        ……//移除为使用的Header和Footer、处理View焦点等问题

    }

由于ListView比较特殊,前后两次layout过程差别比较大,因此我们将分别分析第一次、第二次layout 过程。

首先,在进行第一次layout时,还没有View被添加到ListView中,因此1处的childCount为0,而dataChanged也只有在数据集改变时才会为true,因此会执行3处fillActiveViews方法。如前所述,该方法会将显示的View添加到mActiveViews中,但由于此时childCount为0,因此并不会产生什么影响,同样4处的detachAllViewsFromParent方法也不会产生任何影响。

然后,默认布局是从上往下,因此会执行5处的fillFromTop方法,fillFromTop方法里又调用了fillDown,因此我们看一下fillDown方法,参数childrenTop也即mListPadding.top,也即是第一个子View开始的位置:

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

fillDown方法的逻辑很简单,它以nextTop作为下一个View开始的位置,然后通过makeAndAddView添加一个View,并增加nextTop,直到填满ListView或者加载了所有item,也即nextTop >= end || pos >= mItemCount。那么,不难发现,该方法中的makeAndAddView方法大有玄机。

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

首先,如果数据集没有改变,makeAndAddView方法通过RecyclerBin的getActiveView方法获得一个View,然后通过setupChild方法添加View;如果数据集已经改变,getActiveView返回了null,则通过obtainView方法获取View,然后调用setupChild。

显然,第一次layout时,getActiveView一定返回null,因此child会通过obtainView方法获取。下面看一下obtainView方法:

    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 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 {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;

                    // Finish the temporary detach started in addScrapView().
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }

            }
        }

        ……//

        return child;
    }

首先尝试从RecyclerBin中获取一个ScrapView,然后通过mAdapter.getView方法重新为该View绑定数据来复用该View,如果重新绑定数据失败(也即mAdapter.getView返回的View != 传入的scrapView)就会将获得的ScrapView重新放入mScrapViews中。

到这里,不知道大家有没有看到一些熟悉的东西!对,这里的getView就是我们继承BaseAdapter时重写的getView方法,而传入的scrapView就是参数convertView。现在看一个我们实现getView的简单示例:

    public View getView(int position, View convertView, ViewGroup parent) {
        View view;  
        if (convertView == null) {  
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
        } else {  
            view = convertView;  
        }  
        ((TextView)view.findViewById(R.id.text)).setText(getItem(position));
        return view;
    }

可以看到,View的复用就是在getView中实现的,判断convertView是否为null,如果convertView不为null,则为该View重新绑定数据,复用该View。(其实在通过getScrapView获取复用View之前会先尝试获取一个具有transient状态的View,具体逻辑与上面相同。)

由于是第一次layout,因此此时convertView都为null,也即会通过LayoutInflater创建新的View来填满ListView。

回到makeAndAddView中,在通过obtainView获取到View之后,会通过setupView来添加View,看一下setupView方法:

    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();
        }
        p.viewType = mAdapter.getItemViewType(position);
        p.isEnabled = mAdapter.isEnabled(position);

        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
        }

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

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

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

setupChild方法比较长,但是逻辑并不复杂。首先,使用addViewInLayout方法(如果不是一个新创建的View,则会调用attachViewToParent方法将View重新attach到ListView)在onLayout期间将child添加到ListView中,然后如果需要measure的话对child进行measure(在第一次layout期间,由于所有的View都是新创建的,所以都需要measure)。然后调用child.layout()方法将child放到指定位置(如果不是一个新的View,则只需要通过调用offsetLeftAndRight、offsetTopAndBottom来对child进行偏移)。

至此为止,第一次layout过程分析完了,大致流程如下:

  1. 执行recycleBin.fillActiveViews()方法,将所有child添加到mActiveViews中,但childCount为0,无影响;
  2. detachAllViewsFromParent()将所有子View从ListView上detach,同样由于childCount为0,无影响;
  3. fillTopDown -> fillDown,在fillDown中通过makeAndAddView循环获取并添加View来填充ListView;
  4. makeAndAddView中首先尝试调用mRecycler.getActiveView从RecyclerBin中获取View复用,但此时一定返回null,然后通过obtainView获取View,通过setupView添加View;
  5. obtainView首先尝试从mScrapViews中获取View复用,但同样此时回返回null,然后调用Adapter的getView(position, null, this)方法获取View,此时getView方法中会通过LayoutInflater来创建新的View;
  6. setupView中会通过addViewInLayout将child添加到ListView中,由于此时child时新创建的,所有会对child进行measre,然后为child进行layout;
  7. ListView填充满之后,调用recycleBin.scrapActiveViews方法将mActiveViews中的View移动到mScrapView中,同样,此次layout过程中不产生影响;
  8. 完成。

对着上面的layoutChildren的代码,我们再看一下在第二次layout过程中会进行哪些活动,首先,可以确定的是,由于第一次layout过程中会在getView中来创建新的View填满ListView,因此第二次layout过程中childCount肯定不为0:

  1. 执行recycleBin.fillActiveViews()方法,将所有child添加到mActiveViews中,此时mActiveViews一定不为空;
  2. detachAllViewsFromParent()将所有子View从ListView上detach;
  3. fillTopDown -> fillDown,在fillDown中通过makeAndAddView循环获取并添加View来填充ListView;
  4. makeAndAddView中通过recycleBin.getActiveView(),此时getActiveView()返回不为空;
  5. 将activeView传递给setupView进行复用;
  6. setupView中重新attach该View,然后对View进行layout;
  7. 调用recycleBin.scrapActiveViews方法将mActiveViews中为使用的View移动到mScrapView中,以便下次复用;
  8. 完成;

在两次layout中比较特殊的是没有发生滑动,因此通过fillActiveViews填充的mActiveViews正好能够通过getActiveView重新填充回ListView,此时复用的View全部来源于一级缓存mActiveViews中,因此不需要重新绑定数据,而在最后调用recycleBin.scrapActiveViews也不产生影响,因为此时mActiveViews中的View已经全部被复用了。

但是,如果是通过由于产生滑动而进行layout,那么,由于可能有View被滑出ListView,因此mActiveViews中被滑出的View将不能被直接复用,而是在最后被移动到mScrapView中,因此不能全部填充ListView,此时就会复用二级缓存mScrapViews中的View,并在getView中绑定,如果二级缓存也没有View,那么就会在getView中创建一个View。

OnTouchEvent

onTouchEvent实现在AbsListView中,在onTouchEvent中会在接收到Move事件时调用onTouchMove,而onTouchMove又会调用scrollIfNeeded(),而具体的逻辑就实现在scrollIfNeeded方法中,该方法比较长,我们只需要关心下面的部分:

    ……//
    if (incrementalDeltaY != 0) {
        // Coming back to 'real' list scrolling
        if (mScrollY != 0) {
            mScrollY = 0;
            invalidateParentIfNeeded();
        }

        trackMotionScroll(incrementalDeltaY, incrementalDeltaY);

        ……//
    }
    ……//

这里我们需要注意trackMotionScroll方法,在该方法中完成了对子View的处理,下面看一下该方法;

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

    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

    mBlockLayoutRequests = true;

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

    if (down) {
        mFirstPosition += count;
    }

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

    ……//

可以看到,trackMotionScroll方法中将移出了ListView的child直接加到mScrapViews中,并将它们从ListView中detach,然后调用ViewGroup的offsetChildrenTopAndBottom方法完成children的偏移操作,然后更改mFirstPosition的值,因为mFirstPosition指示了显示的第一个View在数据集中对应的位置,有View移出了ListView,该值也应相应的改变。最后,由于有View滑出了ListView,所以要通过fillGap方法来填充空白,fillGap实现在ListView中,最终也是通过fillDown来复用View进行填充。

同样,如果滑动没有造成child移出ListView,那么,count = 0,只需要通过offsetChildrenTopAndBottom方法偏移children即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值