zoukankan      html  css  js  c++  java
  • ListView源码分析

    参考

    https://blog.csdn.net/guolin_blog/article/details/44996879

    ListView的缓存机制

    ListView只承担交互和展示工作,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。

    RecycleBin

    那么在开始分析ListView的源码之前,还有一个东西是我们提前需要了解的,就是RecycleBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。

    其实RecycleBin的代码并不多,只有300行左右,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。那我们来看一下RecycleBin中的主要代码,如下所示:

    class RecycleBin {
       /**
        * 此接口有一个方法 void onMovedToScrapHeap(View view);当itemview被移到了Scrap缓存中时会调用此方法
        */
        private RecyclerListener mRecyclerListener;
    
        /**
         * mActiveViews的第一个元素 在 listView中的position
         */
        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.
        * 在布局开始时显示在屏幕上的itemViews。
        * 该数组在布局开始时被填充,在布局结束时 mActiveViews 中的所有视图都被移动到 mScrapViews。
        * mActiveViews中的视图表示一个连续的视图范围,第一个视图存储的位置位于mFirstActivePosition中。
         */
        private View[] mActiveViews = new View[0];
    
        /**
         * Unsorted views that can be used by the adapter as a convert view.
        * 不同viewType的itemView 存储在不同的List中。
         */
        private ArrayList<View>[] mScrapViews;
    
        private int mViewTypeCount;
    
       /**
        * 当viewTypeCount==1时才用,表示mScrapViews[0]。
        */
       private ArrayList<View> mCurrentScrap;
    
       public void setViewTypeCount(int viewTypeCount) {
          if (viewTypeCount < 1) {
             throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
          }
          // noinspection unchecked
          ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
          for (int i = 0; i < viewTypeCount; i++) {
             scrapViews[i] = new ArrayList<View>();
          }
          mViewTypeCount = viewTypeCount;
          mCurrentScrap = scrapViews[0];
          mScrapViews = scrapViews;
       }
    
    
        /**
         * Fill ActiveViews with all of the children of the AbsListView.
         *
         * @param childCount          The minimum number of views mActiveViews should hold
         * @param firstActivePosition The position of the first view that will be stored in mActiveViews
         */
        void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;
            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;
                }
            }
        }
    
        /**
         * 获取与指定位置对应的ActiveView。如果找到该view,它将从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) {
                final View match = activeViews[index];
                activeViews[index] = null;
                return match;
            }
            return null;
        }
    
        /**
         * Put a view into the ScapViews list. These views are unordered.
         *
         * @param scrap The view to add
         */
        void addScrapView(View scrap) {
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }
            // Don't put header or footer views or views that should be ignored into the scrap heap
            int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    removeDetachedView(scrap, false);
                }
                return;
            }
            if (mViewTypeCount == 1) {
                dispatchFinishTemporaryDetach(scrap);
                mCurrentScrap.add(scrap);
            } else {
                dispatchFinishTemporaryDetach(scrap);
                mScrapViews[viewType].add(scrap);
            }
    
            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
        }
    
        /**
         * @return A view from the ScrapViews collection. These are unordered.
         */
        View getScrapView(int position) {
            ArrayList<View> scrapViews;
            if (mViewTypeCount == 1) {
                scrapViews = mCurrentScrap;
                int size = scrapViews.size();
                if (size > 0) {
                    return scrapViews.remove(size - 1);
                } else {
                    return null;
                }
            } else {
                int whichScrap = mAdapter.getItemViewType(position);
                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
                    scrapViews = mScrapViews[whichScrap];
                    int size = scrapViews.size();
                    if (size > 0) {
                        return scrapViews.remove(size - 1);
                    }
                }
            }
            return null;
        }
    }

    这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。

    下面就可以开始来分析ListView的工作原理了。

    第一次Layout

    View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了

    ListView的onMeasure中并没有对itemview进行measure,只设置了自己的测量值,用一个Rect记录了自己的padding值:

    final Rect listPadding = mListPadding;
    listPadding.left = mSelectionLeftPadding + mPaddingLeft;
    listPadding.top = mSelectionTopPadding + mPaddingTop;
    listPadding.right = mSelectionRightPadding + mPaddingRight;
    listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;

    ListView真正对itemview进行measure也是在onLayout中。

    ListView.onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
       //  mLayoutHeight = getHeight();
        super.onLayout(changed, l, t, r, b);
    
        mInLayout = true;
    
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            // 对scrapView都执行forceLayout();
            mRecycler.markChildrenDirty();
        }
    
        layoutChildren();
    
        mInLayout = false;
    }

    View.forceLayout

    public void forceLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
    
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
    }

    forceLayout和requestLayout的区别就是,

    • forceLayout只把自己标记,
    • requestLayout会追溯到ViewRootImpl,这期间经历的view都会被标记,并请求一次Traversals

    接着上边的onLayout,

    ListView.layoutChildren

    @Override
     protected void layoutChildren() {
         final boolean blockLayoutRequests = mBlockLayoutRequests;
         if (!blockLayoutRequests) {
             mBlockLayoutRequests = true;
         } else {
             return;
         }
         
         try {
           // 空方法
             super.layoutChildren();
             
             invalidate();
             
             if (mAdapter == null) {
                 resetList();
                 invokeOnItemScrollListener();
                 return;
             }
             
             int childrenTop = mListPadding.top;
             int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
    
    // 首先可以确定的是,ListView当中目前还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此getChildCount()方法得到的值肯定是0。
             int childCount = getChildCount();
    
             int index = 0;
             int delta = 0;
             View sel;
             View oldSel = null;
             View oldFirst = null;
             View newSel = null;
             ...
    
             boolean dataChanged = mDataChanged;
             if (dataChanged) {
                 handleDataChanged();
             }
    
             // Handle the empty set by removing all views that are visible and calling it a day
             // setAdapter中会设置mItemCount
             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. [in ListView(" + getId() + ", " + getClass()
                         + ") with Adapter(" + mAdapter.getClass() + ")]");
             }
             setSelectedPositionInt(mNextSelectedPosition);
             
             // Pull all children into the RecycleBin.
             // These views will be reused if possible
             final int firstPosition = mFirstPosition;
    
             final RecycleBin recycleBin = mRecycler;
             
             // Don't put header or footer views into the Recycler. Those are already cached in mHeaderViews;
    //dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入else执行逻辑,调用RecycleBin的fillActiveViews()方法
             if (dataChanged) {
                 for (int i = 0; i < childCount; i++) {
                     recycleBin.addScrapView(getChildAt(i));
                 }
             } else {
    // 按理来说,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
                 recycleBin.fillActiveViews(childCount, firstPosition);
             }
             
             ...
             
             // Clear out old views
             detachAllViewsFromParent();
             
    
    //接下来会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第140行的default语句当中。
             switch (mLayoutMode) {
                 case xxx:
                   ...
          
                 default:
    //而下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到fillFromTop()方法,具体看下边
                     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();
             
             ...
    
             mLayoutMode = LAYOUT_NORMAL;
             mDataChanged = false;
             mNeedSync = false;
             
             setNextSelectedPositionInt(mSelectedPosition);
             
             updateScrollIndicators();
             if (mItemCount > 0) {
                 checkSelectionChanged();
             }
             
             invokeOnItemScrollListener();
             
         } finally {
             if (!blockLayoutRequests) {
                 mBlockLayoutRequests = false;
             }
         }
     }

    ListView.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);
    }

    ListView.fillDown

    private View fillDown(int pos, int nextTop) {
       View selectedView = null;
       int end = (getBottom() - getTop()) - 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++;
       }
       return selectedView;
    }

    这个方法表示从nextTop到listView底部加载itemView,pos表示从第几个item开始。

    一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出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) {
    
    //尝试从RecycleBin当中快速获取一个activeView,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行
          // Try to use an exsiting view for this position
          child = mRecycler.getActiveView(position);
          if (child != null) {
             // Found it -- we're using an existing child
             // This just needs to be positioned
             setupChild(child, position, y, flow, childrenLeft, selected, true);
             return child;
          }
       }
    
    //会调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。
       // 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;
    }

    接着看obtainView

    /**
     * Get a view and have it show the data associated with the specified
     * position. This is called when we have already discovered that the view is
     * not available for reuse in the recycle bin. The only choices left are
     * converting an old view or making a new one.
     *
     * @param position
     *            The position to display
     * @param isScrap
     *            Array of at least 1 boolean, the first entry will become true
     *            if the returned view was taken from the scrap heap, false if otherwise.
     *
     * @return A view displaying the data associated with the specified position
     */
    View obtainView(int position, boolean[] isScrap) {
       isScrap[0] = false;
       View scrapView;
       scrapView = mRecycler.getScrapView(position);
       View child;
       if (scrapView != null) {
          child = mAdapter.getView(position, scrapView, this);
          if (child != scrapView) {
             mRecycler.addScrapView(scrapView);
             if (mCacheColorHint != 0) {
                child.setDrawingCacheBackgroundColor(mCacheColorHint);
             }
          } else {
             isScrap[0] = true;
             dispatchFinishTemporaryDetach(child);
          }
       } else {
          child = mAdapter.getView(position, null, this);
          if (mCacheColorHint != 0) {
             child.setDrawingCacheBackgroundColor(mCacheColorHint);
          }
       }
       return child;
    }

    obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。

    那么我们还是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。

    接着再看setupChild

    
    
    /**
     * Add a view as a child and make sure it is measured (if necessary) and positioned properly.
     *
     * @param child The view to add
     * @param position The position of this child
     * @param y The y position relative to which this view will be positioned
     * @param flowDown If true, align top edge to y. If false, align bottom edge to y.
     * @param childrenLeft Left edge where children should be positioned
     * @param selected Is this position selected?
     * @param recycled Has this view been pulled from the recycle bin? If so it does not need to be remeasured.
     */
    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
                      boolean selected, boolean recycled) {
       final boolean isSelected = selected && shouldShowSelector();
       final boolean updateChildSelected = isSelected != child.isSelected();
       final int mode = mTouchMode;
       final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position;
       final boolean updateChildPressed = isPressed != child.isPressed();
    
       final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
    
       // Respect layout params that are already in the view. Otherwise make some up...
       // noinspection unchecked
       AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
       if (p == null) {
          p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0);
       }
       p.viewType = mAdapter.getItemViewType(position);
    
    //由于recycled 为false,type也不是header/footer,所以会进入else中的addViewInLayout,addViewInLayout代码在下边
       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);
       }
    
    //接着就会对这个itemView进行measure,layout
       if (needToMeasure) {
          int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width);
          int lpHeight = p.height;
          int childHeightSpec;
          if (lpHeight > 0) {
             childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
          } else {
             childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
          }
          child.measure(childWidthSpec, childHeightSpec);
       } else {
          cleanupLayoutState(child);
       }
    
       final int w = child.getMeasuredWidth();
       final int h = child.getMeasuredHeight();
       final int childTop = flowDown ? y : y - h;
       if (needToMeasure) {
          final int childRight = childrenLeft + w;
          final int childBottom = childTop + h;
          child.layout(childrenLeft, childTop, childRight, childBottom);
       } else {
          child.offsetLeftAndRight(childrenLeft - child.getLeft());
          child.offsetTopAndBottom(childTop - child.getTop());
       }
    
       if (mCachingStarted && !child.isDrawingCacheEnabled()) {
          child.setDrawingCacheEnabled(true);
       }
    }

    那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。那么到此为止,第一次Layout过程结束。

    ViewGroup.addViewInLayout

    protected boolean addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        child.mParent = null;
        addViewInner(child, index, params, preventRequestLayout);
        child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        return true;
    }
    
    private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {
    
       ...
    
        if (child.getParent() != null) {
            throw new IllegalStateException("The specified child already has a parent. " +
                    "You must call removeView() on the child's parent first.");
        }
    
       ...
    
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
    
        if (preventRequestLayout) {
            child.mLayoutParams = params;
        } else {
            child.setLayoutParams(params);
        }
    
        if (index < 0) {
            index = mChildrenCount;
        }
    
        addInArray(child, index);
    
        // tell our children
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }
    
        final boolean childHasFocus = child.hasFocus();
        if (childHasFocus) {
            requestChildFocus(child, child.findFocus());
        }
    
        AttachInfo ai = mAttachInfo;
        if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
            boolean lastKeepOn = ai.mKeepScreenOn;
            ai.mKeepScreenOn = false;
            child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
            if (ai.mKeepScreenOn) {
                needGlobalAttributesUpdate(true);
            }
            ai.mKeepScreenOn = lastKeepOn;
        }
    
        if (child.isLayoutDirectionInherited()) {
            child.resetRtlProperties();
        }
    
        dispatchViewAdded(child);
    
        ...
    }

      

    第二次Layout

    第一次onLayout后把itemview显示在当前屏幕上,如果之后的一些操作触发了listView的requestLayout导致需要再次执行layout,因为所有的itemview的添加处理都是在onLayout中处理的,那么之后的逻辑是怎么避免一些重复的操作的呢?具体看代码。

    ListView.layoutChildren

    @Override
     protected void layoutChildren() {
         final boolean blockLayoutRequests = mBlockLayoutRequests;
         if (!blockLayoutRequests) {
             mBlockLayoutRequests = true;
         } else {
             return;
         }
         
         try {
           // 空方法
             super.layoutChildren();
             
             invalidate();
             
             if (mAdapter == null) {
                 resetList();
                 invokeOnItemScrollListener();
                 return;
             }
             
             int childrenTop = mListPadding.top;
             int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
    
    // 调用getChildCount()方法来获取子View的数量,只不过现在得到的值不会再是0了,而是ListView中一屏可以显示的子View数量,
    因为我们刚刚在第一次Layout过程当中向ListView添加了这么多的子View
             int childCount = getChildCount();
    
             int index = 0;
             int delta = 0;
             View sel;
             View oldSel = null;
             View oldFirst = null;
             View newSel = null;
             ...
    
             boolean dataChanged = mDataChanged;
             if (dataChanged) {
                 handleDataChanged();
             }
    
             // Handle the empty set by removing all views that are visible and calling it a day
             // setAdapter中会设置mItemCount
             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. [in ListView(" + getId() + ", " + getClass()
                         + ") with Adapter(" + mAdapter.getClass() + ")]");
             }
             setSelectedPositionInt(mNextSelectedPosition);
             
             // Pull all children into the RecycleBin.
             // These views will be reused if possible
             final int firstPosition = mFirstPosition;
    
             final RecycleBin recycleBin = mRecycler;
             
             // Don't put header or footer views into the Recycler. Those are already cached in mHeaderViews;
    //dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入else执行逻辑,调用RecycleBin的fillActiveViews()方法
             if (dataChanged) {
                 for (int i = 0; i < childCount; i++) {
                     recycleBin.addScrapView(getChildAt(i));
                 }
             } else {
    //这次效果可就不一样了,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。
                 recycleBin.fillActiveViews(childCount, firstPosition);
             }
             
             ...
             
    //接下来将会是非常非常重要的一个操作,在第113行调用了detachAllViewsFromParent()方法。这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。
             // Clear out old views
             detachAllViewsFromParent();
             
    
    //接下来会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第140行的default语句当中。
             switch (mLayoutMode) {
                 case xxx:
                   ...
          
                 default:
    //childCount 由于不再等于0了,因此会进入到else语句当中
                     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 {
    而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,看下边fillSpecific代码。
                         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;
             }
             
    接着把activeView中没用到的移到ScrapView中缓存
             // Flush any cached views that did not get reused above
             recycleBin.scrapActiveViews();
             
             ...
    
             mLayoutMode = LAYOUT_NORMAL;
             mDataChanged = false;
             mNeedSync = false;
             
             setNextSelectedPositionInt(mSelectedPosition);
             
             updateScrollIndicators();
             if (mItemCount > 0) {
                 checkSelectionChanged();
             }
             
             invokeOnItemScrollListener();
             
         } finally {
             if (!blockLayoutRequests) {
                 mBlockLayoutRequests = false;
             }
         }
     }

    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 temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
    
        // Possibly changed again in fillUp if we add rows above this one.
        mFirstPosition = position;
        View above;
        View below;
        final int dividerHeight = mDividerHeight;
    
        if (!mStackFromBottom) {
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            // This will correct for the top of the first view not touching the top of the list
            adjustViewsUpOrDown();
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                correctTooHigh(childCount);
            }
        } else {
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            // This will correct for the bottom of the last view not touching the bottom of the list
            adjustViewsUpOrDown();
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                correctTooLow(childCount);
            }
        }
        if (tempIsSelected) {
            return temp;
        } else if (above != null) {
            return above;
        } else {
            return below;
        }
    }

    fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。

    再次回到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) {
    
    //仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。
    那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。
          // Try to use an exsiting view for this position
          child = mRecycler.getActiveView(position);
          if (child != null) {
             // Found it -- we're using an existing child
             // This just needs to be positioned
    注意setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的
             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;
    }

    接着再看setupChild

    可以看到,setupChild()方法的最后一个参数是recycled,然后在第32行会对这个变量进行判断,由于recycled现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。

    ViewGroup.attachViewToParent

    protected void attachViewToParent(View child, int index, LayoutParams params) {
        child.mLayoutParams = params;
    
        if (index < 0) {
            index = mChildrenCount;
        }
    
        addInArray(child, index);
    
        child.mParent = this;
        child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK & ~PFLAG_DRAWING_CACHE_VALID)
                                | PFLAG_DRAWN | PFLAG_INVALIDATED;
    
        this.mPrivateFlags |= PFLAG_INVALIDATED;
    
        if (child.hasFocus()) {
            requestChildFocus(child, child.findFocus());
        }
        dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown());
    }

    经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。

    滑动加载更多数据

    经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。

    由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:

    AbsListView.onTouchEvent()

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isEnabled()) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return isClickable() || isLongClickable();
        }
        final int action = ev.getAction();
        View v;
    
        int deltaY;
    
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
    
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                mActivePointerId = ev.getPointerId(0);
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                int motionPosition = pointToPosition(x, y);
    
                ...
    
                mMotionX = x;
                mMotionY = y;
                mMotionPosition = motionPosition;
                mLastY = Integer.MIN_VALUE;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final int y = (int) ev.getY(pointerIndex);
                deltaY = y - mMotionY;
                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
                        startScrollIfNeeded(deltaY);
                        break;
                    case TOUCH_MODE_SCROLL:
                        if (y != mLastY) {
                            deltaY -= mMotionCorrection;
                            int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
                            // 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
                            if (atEdge && getChildCount() > 0) {
                                // Treat this like we're starting a new scroll from the current position. 
                               // This will let the user start scrolling back into content immediately 
                               // rather than needing to scroll back to the point where they hit the limit first.
                                int motionPosition = findMotionRow(y);
                                if (motionPosition >= 0) {
                                    final View motionView = getChildAt(motionPosition - mFirstPosition);
                                    mMotionViewOriginalTop = motionView.getTop();
                                }
                                mMotionY = y;
                                mMotionPosition = motionPosition;
                                invalidate();
                            }
                            mLastY = y;
                        }
                        break;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                switch (mTouchMode) {
                    case TOUCH_MODE_DOWN:
                    case TOUCH_MODE_TAP:
                    case TOUCH_MODE_DONE_WAITING:
                        ...
                        mTouchMode = TOUCH_MODE_REST;
                        break;
                    case TOUCH_MODE_SCROLL:
                        final int childCount = getChildCount();
                        if (childCount > 0) {
                            if (mFirstPosition == 0
                                    && getChildAt(0).getTop() >= mListPadding.top
                                    && mFirstPosition + childCount < mItemCount
                                    && getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom) {
                                mTouchMode = TOUCH_MODE_REST;
                                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                            } else {
                                //此时会去执行fling操作
                                final VelocityTracker velocityTracker = mVelocityTracker;
                                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                                final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                                if (Math.abs(initialVelocity) > mMinimumVelocity) {
                                    if (mFlingRunnable == null) {
                                        mFlingRunnable = new FlingRunnable();
                                    }
                                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                                    mFlingRunnable.start(-initialVelocity);
                                } else {
                                    mTouchMode = TOUCH_MODE_REST;
                                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                                }
                            }
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        }
                        break;
                }
    
    
                setPressed(false);
                // Need to redraw since we probably aren't drawing the selector
                // anymore
                invalidate();
                final Handler handler = getHandler();
                if (handler != null) {
                    handler.removeCallbacks(mPendingCheckForLongPress);
                }
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                mActivePointerId = INVALID_POINTER;
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
               ...
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
               ...
                break;
            }
        }
        return true;
    }

    这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。

    可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。

    这样的话,代码就应该会走到调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。

    trackMotionScroll

    /**
     * @param deltaY 表示从down到此次触摸事件移动的距离,当然是由正负之分的
     * @param incrementalDeltaY 表示此次触摸事件 相对于上一次触摸事件移动的距离,当然是由正负之分的
     * @return
     */
    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;
       final int spaceAbove = listPadding.top - firstTop;
       final int end = getHeight() - listPadding.bottom;
       final int spaceBelow = lastBottom - end;
       final int height = getHeight() - getPaddingBottom() - getPaddingTop();
       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;
       if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
          // Don't need to move views down if the top of the first position is already visible
          return true;
       }
       if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
          // Don't need to move views up if the bottom of the last position is already visible
          return true;
       }
    
    
       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) {
          // 表示手指向上滑动,列表向下加载
    
          final int top = listPadding.top - incrementalDeltaY;
          for (int i = 0; i < childCount; i++) {
             final View child = getChildAt(i);
    
             if (child.getBottom() >= top) {
                // 表示此时该itemView还没有完全移出屏幕,那就直接break,因为后边的肯定也都没移出屏幕
                break;
             } else {
                // 在此逻辑里表示itemView已经移出屏幕,需要被回收进ScrapView中
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                   mRecycler.addScrapView(child);
                }
             }
          }
       } else {
          // 表示手指向下滑动,列表向上加载
    
          final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
          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) {
                   mRecycler.addScrapView(child);
                }
             }
          }
       }
    
       mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
       
       mBlockLayoutRequests = true;
       
       if (count > 0) {
          detachViewsFromParent(start, count);
       }
       
       // 通过此方法来滑动listView,并不是scrollTo/scrollBy
       offsetChildrenTopAndBottom(incrementalDeltaY);
       
       if (down) {
          mFirstPosition += count;
       }
       invalidate();
       final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
       
       // 表示移动的距离导致有itemView移出屏幕,并且需要有新的itemView填补空缺,看下边fillGap代码
       if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
          fillGap(down);
       }
       
       ...
       
       mBlockLayoutRequests = false;
       invokeOnItemScrollListener();
       awakenScrollBars();
       return false;
    }

    ListView.fillGap

    void fillGap(boolean down) {
        final int count = getChildCount();
        if (down) {
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : getListPaddingTop();
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } else {
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - getListPaddingBottom();
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }

    down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,在之前通过把移出屏幕的itemView添加到了scrapViews中,而activeViews中又没有,所以再次执行makeAndAddView时,会从scrapViews中获取一个复用view,然后调用adapter.getView(position, scrapView, listView)来获取执行一次。

    所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

    问题

    ListView滑动/滚动是否实通过Scroll实现的

    虽然ListView内部使用了Scroller来计算滚动,但最终并不是通过scrollTo/scrollBy来实现滑动的,而通过调用offsetChildrenTopAndBottom(int offset)来实现滑动的,也即是通过修改子view的top和bottom实现滑动的。

    ViewGroup.offsetChildrenTopAndBottom

    public void offsetChildrenTopAndBottom(int offset) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        boolean invalidate = false;
    
        for (int i = 0; i < count; i++) {
            final View v = children[i];
            v.mTop += offset;
            v.mBottom += offset;
            if (v.mRenderNode != null) {
                invalidate = true;
                v.mRenderNode.offsetTopAndBottom(offset);
            }
        }
    
        if (invalidate) {
            invalidateViewProperty(false, false);
        }
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }

    ActiveViews和scrapViews的作用

    • activeViews

    在数据集没有发生改变时,触发了layout(例如调用了setSelection),在layout开始时会把当前屏幕上的itemView暂存到此List中,

    再次从此list获取后直接使用此itemview,就不用执行adapter.getView的操作。

    • scrapViews

    是在滑动listview时,滑出屏幕的itemView会被加入到此list中,或者在数据集发生改变时当前屏幕上的itemView也会全都暂存到此list中,

    再次从此list中获取的itemview需要进行adapter.getView的操作。

  • 相关阅读:
    python之字典方法
    python之字符串方法
    python strip()方法使用
    Airtest自动化测试工具介绍
    selenium 环境配置
    一个自定义线程池的小Demo
    简单工厂模式
    BootStrap入门_创建第一个例子
    MongoDB的索引
    MongoDB的查询
  • 原文地址:https://www.cnblogs.com/muouren/p/11706491.html
Copyright © 2011-2022 走看看