ListView 源码解析

来源:互联网 发布:ubuntu 命令不提示 编辑:程序博客网 时间:2024/05/29 19:26

源码解析的习惯也要开始养成呀,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 中了

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    super.onLayout(changed, l, t, r, b);   ...    layoutChildren(); // 布局 子 view   ...}@Overrideprotected 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 并填充 ListViewprivate 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 方法,不过执行的逻辑确跟第一次不同了

@Overrideprotected 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 方法中

@Overridepublic 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 中。整个过程执行结束。

原创粉丝点击