ListView 源码解析

源码解析的习惯也要开始养成呀,ListView 的一些工作流程当时看懂了,可过了一段时间再看彻底忘了,这次又花了时间顺了一遍,所以必须要记下来了,以便跟 RecyclerView 作对比,所以先看 ListView

继承结构

ListView 和 GridView 都继承自 AbListView ,AbListView 继承自 AdapterView,再往上就是 ViewGroup 和 View 了

在使用 ListView 时,我们需要 Adpater 来适配 ListView 的工作,这样用到了适配器模式,ListView 只负责显示数据和处理交互, 至于显示什么数据,这个工作就交给了 Adapter,这样在 ListView 需要数据时就跟 Adapter 来要,减轻了 ListView 的工作量,并为 ListView 可以展示不同的数据提供了实现。

我们学习过 ListView 的时候,都知道 ListView 不但以列表形式展示,并且 item 还能实现复用,这样即使显示上千条数据,内存中 ListView 的 item 的 View 对象也就那么几个,至于 ListView 的 item 复用是如何实现的,今天的解析就是为了搞清楚这个。

RecycleBin

首先我们看一个 AbListView 的内部类,RecycleBin ,这个类在 LisView 工作中起到了关键性作用。主要解析 RecycleBin 的 5 个方法

RecycleBin 中的代码很多,我只选了重要的 5 个方法,这五个方法都加了注释,可以先稍微了解一下,接下来分析 ListView 的时候再回头看会更加清晰。

class AbListView {

class RecycleBin {

    /**
     * 在 ListView 的 item 只有一种布局时,存储废弃 View 的集合
     */
    private ArrayList<View> mCurrentScrap;
    /**
     * 在 ListViwe 有多种布局时,这个数组中的每一项都是一种布局的集合
     */
    private ArrayList<View>[] mScrapViews;
    
    /**
     * 当前 ListView 中显示的 View
     */
    private View[] mActiveViews = new View[0];

    /**
     * 将 ListView 中的 View 都放入 mActivieViews 数组中
     * childCount 要存入的 View 的数量
     * firstActivePosition 第一个可见的 View 的 position
     */
    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++) {// 遍历 AbListView 中的子 View ,把每一个都放入 mActiveViews 中
            View child = getChildAt(i); 
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
         
            if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                
                activeViews[i] = child;
                // Remember the position so that setupChild() doesn't reset state.
                lp.scrappedFromPosition = firstActivePosition + i;
            }
        }
    }   


   /**
    * 与 fillActiveViews 相对,getActiveView 方法作用为从 mActiveViews 中取出相应位置的 View,并移除 mActiveViews 中 position 位置的 View
    * 说明 mActiveViews 中每个位置的 View 只能使用一次,除非重新赋值
    */
    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >=0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }   


   /**
    * 根据 View 在 AbListView 中的 Type ,存储废弃的 View,例如滚动出屏幕的 View
    */
    void addScrapView(View scrap, int position) {
        ...
        final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
        final int viewType = lp.viewType; // 获取 Type 的值 
        ...
        if (mViewTypeCount == 1) {
               mCurrentScrap.add(scrap); // 如果只有一种则添加到 mCurrentScrap 中
        } else {
            mScrapViews[viewType].add(scrap); // 如果有多种,则放入对应的集合中
        }
        ...
     }


   /**
    * @return A view from the ScrapViews collection. 找到废弃 View 的数组中 itemId 跟 position 处 itemId 相同的 View 从数组中移除,并将 View 返回
    */
    View getScrapView(int position) {
        final int whichScrap = mAdapter.getItemViewType(position); // 需要获取的 Viwe 的 Type
        if (whichScrap < 0) {
            return null;
        }
        if (mViewTypeCount == 1) {
            return retrieveFromScrap(mCurrentScrap, position); // 只有一种时,返回 mCurrentScrap 中对应位置 View 
        } else if (whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position); // 多种情况是,返回对应种类集合中的 View
        }
        return null;
    }

   /**
    * 设置 ListView 中 item 的种类,并初始化存储废弃 View 的集合
    */
    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;
    }
}

}

ListView 的工作流程

ListView 在显示时,也是通过 onMeasure,onLayout,onDraw 的流程,onMeasure 过程很简单,因为 ListView 一般都是固定宽高或者宽高最大;onDraw 更简单,绘制的过程都是每个子 View 自己绘制自己,ListViwe 不用管绘制阶段;只有 onLayout 过程是有实际工作意义的,所以我们直接来看 onLayout 方法,onLayout 方法的实现是在 AbListView 中的,onLayout 方法中主要就是调用 layoutChildren 来布局子 View,这个方法就切换到 ListView 中了

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

   ...

    layoutChildren(); // 布局 子 view

   ...
}


@Override
protected void layoutChildren() {
    ...
    final int childCount = getChildCount();
    if (dataChanged) { // 判断 数据集 是否变化
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else { // ListView 刚进来时,进去当然是没变化的,所以执行这里,recycleBin 就是刚开始提到的 RecycleBin 的实例,fillActiveViews 将子 View 添加到 RcycleBin 的响应集合里,由于第一次进来是没有子 View 的,所以这行代码无效果
            recycleBin.fillActiveViews(childCount, firstPosition);
    }
    
    ...
    switch (mLayoutMode) { // 布局模式,一般都是默认
    default:
    if (childCount == 0) { // 第一次 onLayout 时 子 View 个数为 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 {
            ...
        }
        break;        
    }
}

我们来看 layoutChildre 方法,数据没有变化时会调用 recycleBin.fillActiveViews 的方法,这是子 View 的个数为 0 ,所以这行代码无效,接着到了根据布局模式填充数据的阶段,一般都是默认模式。接下来判断从上往下填充活着从下往上填充 ListView,这个是由 ListView 使用者设置的,默认从上往下。

// 从上往下填充
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); // 填充数据
}

private View fillDown(int pos, int nextTop) {
    View selectedView = null;

    ...
    
    // 循环填充数据
    while (nextTop < end && pos < mItemCount) { // 下一个 Viwe 的 top 位置还不到 listView 的底部,并且当前 position 小于数据的数量时继续填充
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        
        // 由 位置,需要添加的 Viwe 的顶部在 ListView 中的位置,padding 值的构造子 View 并添加到 ListView 中
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight; // 计算下一个子 View 的顶部位置
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

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

// 从下往上填充的情况
private View fillUp(int pos, int nextBottom) {
    View selectedView = null;

    int end = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end = mListPadding.top;
    }
    
    // 循环填充
    while (nextBottom > end && pos >= 0) { // 从下往上填充,判断的是下一个 View 的底部是否已经出了屏幕位置,如果出了屏幕则不用添加
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); // 构造 View 并填充到 ListView
        nextBottom = child.getTop() - mDividerHeight; // 下一个 Viwe 的底部位置
        if (selected) {
            selectedView = child;
        }
        pos--;
    }

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

由代码我们看到不管是从下往上填充还是从上往下填充,都会调用一个 makeAndAddView 方法来构造 View 并将其填充到 ListView 中

// 构造 View 并填充 ListView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
        
    if (!mDataChanged) { // 数据集没变化时
        final View activeView = mRecycler.getActiveView(position);  // 从 RecycleBin 中获取当前位置的 View ,由于第一次加载界面,该 View 为空
        if (activeView != null) {
            setupChild(activeView, position, y, flow, childrenLeft, selected, true); 
            return activeView;
        }
    }

    final View child = obtainView(position, mIsScrap); // 在 RecycleBin 中没有需要的 View 时,调用 obtainView 方法来构造 View
    // This needs to be positioned and measured.
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); // 将 View 填充到 ListView

    return child;
}

第一次加载时在 RecycleBin 中不能获取到对象 view, 则通过 obtainView 方法来构造 Viwe,该方法在 AbListView 中.

View obtainView(int position, boolean[] outMetadata) {
    
    ...

    final View scrapView = mRecycler.getScrapView(position); // 从 RecycleBin 中的废弃 View 中取出需要的 View ,由于是第一次加载,返回为空
    
    final View child = mAdapter.getView(position, scrapView, this);  // 由 Adapter 的 getView 方法来获取 View
    
    if (scrapView != null) {
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            ...
        }
    }

    setItemViewLayoutParams(child, position);
    
    return child; // 返回 Viwe
}

obtainView 方法中会首先从 RecycleBin 中的废弃 View 的集合中来获取 View,第一次加载时是获取不到的,所以同 mAdapter.getView 方法来获取 View,getView 方法就是我们定义 Adapter 时重写的那个方法。在 scrapView 为空时,我们会通过 LayoutInflater 来加载一个 View 赋值给 child,最后将child 返回。makeAndAddView 方法中调用 obtainView 获取到 View 之后,在调用 setupChild 方法,其中调用 attachViewToParent 将 Viwe 填充到 ListView 中。加载时的第一次 onLayout 过程结束

由于 Android 界面加载机制,onLayout 方法会调用两次,第一次 onLayout 时我们上面已经分析过了,如果再加载一次,难道数据会添加两次? 我们接着回头看第二次 onLayout 的过程。其中 onLayout 还是会调用 layoutChildren 方法,不过执行的逻辑确跟第一次不同了

@Override
protected void layoutChildren() {
    ...
    final int childCount = getChildCount();
    if (dataChanged) { // 判断 数据集 是否变化
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else { // 这已经是第二季加载,ListView 中已经是有数据了,这时候将 ListView 中的所有 View 都添加到 recycleBind 中的存储 ListView 子 View 的集合中
            recycleBin.fillActiveViews(childCount, firstPosition);
    }
    
    ...
    
    detachAllViewsFromParent(); // 移除 ListView 中所有的 Viwe
        
    ...
    switch (mLayoutMode) { // 布局模式,一般都是默认
    default:
    if (childCount == 0) { // 第一次 onLayout 时 子 View 个数为 0 时的加载
           ...
        } else { // 第二次加载时 childCount 已经不是 0
            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;        
    }
}

第二次 onLayout 时,layoutChildren 方法的执行已经跟第一次不一样,这是会将 ListView 中所有的 View 添加到 RecycleBin 中,然后移除 ListView 中所有的 View,再调用 fillSpecific 方法来填充 ListView 。

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

   ...
}

fillSpecific 方法里面还是会根据 ListView 的填充模式从下往上或者从上往下来填充,不过由于之前已经加载过,被选中的 position 的值可能不为 0 ,这时候会从被选中的位置向两头加载。同样是调用 fillUp 和 fillDown 方法,这两个方法的执行跟第一次 onLayout 没什么区别,都是根据当前视图中最后一个 View 的位置和数据的数量来判断是否需要继续添加 View,不过其内部构造 View 时调用的 makeAndAddView 确有了一点不同。由于 从 RecycleBin 中取到了数据,就不需要调用 obtainView 方法来获取 View ,直接将 RecycleBin 中取到的 view 填充到 ListView 中,通过 setupChild 方法中的 attachViewToParent 方法完成填充。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        
        final View activeView = mRecycler.getActiveView(position); // 由于第二次 onLayout 开始就把 ListView 中原来的 view 添加到 RecycleBin 中了,所有 activeView 有值
        
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true); // 将从 RecycleBin 中取到的值填充到 ListView 中
            return activeView; // 返回 RecycleBin 中得到的 View
        }
    }

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

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

    return child;
}

这样第一次 onLayout 和第二次 onLayout 方法就调用完成了,ListView 显示到界面上时也就可以展示数据了。只有在第一次加载时才从 Adapter 中获取View,第二次加载时首先为 RecycleBin 添加数据,ListView 再需要数据时直接从 RecycleBin 中获取数据即可。

ListView 滑动过程中的视图填充

在 ListView 滑动过程中 ListView 中的视图是会随时变化的,所以我们必须要分析滑动过程中 ListView 的工作过程,滑动中处理主要在 onTouchEvent 方法中。由于滑动过程中的触摸事件是 EVENT_MOVE 所以我们只看移动过程中的代码,接着追踪到了 onTouchMove 方法中

@Override
public boolean onTouchEvent(MotionEvent ev) {
   
    initVelocityTrackerIfNotExists();
    final MotionEvent vtev = MotionEvent.obtain(ev);
    final int actionMasked = ev.getActionMasked();
    ...
    switch (actionMasked) {
        ...
        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
        ...
    }

    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}

ListView 的滑动过程中,在 onTouchMove 中对应的情况为 TOUCH_MODE_SCROLL ,接着追踪到了 scrollFNeeded 方法中,该方法中通过调用 trackMotionScroll 方法来完成滑动过程中的填充任务,trackMotionScroll 方法的两个参数为 deltaY 为从 ACTION_DOWN 到当前位置移动的距离,incrementalDelaY 当前距离上一个事件时发送的距离变化

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    ...
    
    // 如果 incrementalDeltaY 大于 0 ,说明手指下滑,屏幕中 ListView 向上滚动,向下拉状态,要显示的 View 从顶部出现
    
    // 如果 incrementalDeltaY 小于 0 ,说明手指上滑,屏幕中 ListView 向下滚动,向上拉状态,要显示的 View 从底部出现
    
    final boolean down = incrementalDeltaY < 0;
    
    int start = 0;
    int count = 0;
    
    if (down) { // 上拉状态,View 从底部填充,同时 View 从顶部滑出 ListView 的区域
        int top = -incrementalDeltaY; // 手指滑动的长度
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top; // 手指滑动的长度加上 ListViwe 的顶部的 padding 值
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) { // View 的底部加上手指滑动的距离如果比 top 值大,说明该 View 还有部分或整体都在 ListView 中,不能回收
                break;
            } else { // 如果 View 的底部的值加滑动的距离比 top 值小,说明该 View 经过滑动之后会滑出 ListView 的区域,需要回收
                count++; // 需要回收的数量加 1 
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    ...
                    mRecycler.addScrapView(child, position); // 将需要回收的 view 放入 RecycleBin 中的回收集合中
                }
            }
        }
    } else { // 下拉状态,数据从顶部填充,同时 View 从底部移出
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }
    
    if (count > 0) {
        detachViewsFromParent(start, count); // 需要移除的 view 移出 ListView
        mRecycler.removeSkippedScrap();
    }
    
    offsetChildrenTopAndBottom(incrementalDeltaY); // 将还存在的 view 根据滑动距离做出偏移
    
    
    // 判断原来最顶部的 View 的在 ListView 中的高度小于滑动的高度,或者最底部的 View 在 ListView 中的高度小于滑动的高度,这时候必然有 View 滑出屏幕
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down); // 有 View 滑出屏幕是需要新的 View 填充屏幕
    }
    
    
}

trackMotionScroll 方法中,就根据滑动的距离,筛选出经过滑动之后移除屏幕的 View 并添加到 RecycleBin 中存储废弃 View 的集合中,接着会调用 detachViewFromParent 将需要移除的 view 移出 ListView,还会将仍在布局中的 View 根据滑动距离做出移动,最后如果有 View 移出屏幕,则通过 fillGap 方法充新的 View

void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) { // 向上拉,从底部填充 view
        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 { // 向下拉,从顶部填充 view
        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 方法,还记得吗,上面 onLayout 过程中填充 ListView 的方法,其中会调用 makeAndAddView 方法来获取 View

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position); // 由于在 ListView 的第二次 onLayout 过程中将 RecycleBin.getActiveView 中存储的 View 已经全部获取,所以这里会取得 null
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // Make a new view for this position, or convert an unused view if
    // possible.
    final View child = obtainView(position, mIsScrap); // 从而接着从 obtainView 方法中获取 View

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

    return child;
}

这次 obtainView 中的执行跟第一次 onLayout 时就不一样了,从 RecycleBin 中的废弃 View 中取出需要的 View ,由于滑出屏幕的 View 已经添加到RecycleBin 中,所以这里不为 null,在 Adapter.getView 时就可以重用被回收的 View ,实现了 ListView 虽然有千百条数据,但是真正的 item View 确只有很少的几个。

View obtainView(int position, boolean[] outMetadata) {
    
    ...

    final View scrapView = mRecycler.getScrapView(position); // 从 RecycleBin 中的废弃 View 中取出需要的 View ,由于滑出屏幕的 View 已经添加到了 RecycleBin 中,所以这里不为 null
    
    final View child = mAdapter.getView(position, scrapView, this);  // 由 Adapter 的 getView 方法来获取 View,将 scrapView 传入该方法,也就是 AdatperView 的 getView 的 convertView 是有值的,这时我们不会重新构造 View,而是复用 convertView ,这样就实现了 ListView 滑动是的 View 重用   
    
    if (scrapView != null) {
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            ...
        }
    }

    setItemViewLayoutParams(child, position);
    
    return child; // 返回 Viwe
}

到这里,整个 ListView 源码的分析就要结束了,主要的内容是两次 onLayout 过程中的处理,和 ListView 在滑动过程中 View 的回收和复用的过程。

在 ListView 滑动之后,还会调用 invalidate 方法来申请重新绘制,这个过程又会调用依次 onLayout 这时的 onLayout 执行过程就如果显示到屏幕上时第二次调用 onLayout 的过程,会将 View 添加到 RecycleBin ,移除所有 View ,再从 RecycleBin 中获取 View 添加到 ListView 中。整个过程执行结束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容