ListView 源码分析

前言

虽然现在在展示数据的时候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 还是很有必要的,通过了解 ListView,既可以帮助理解更加复杂的 RecyclerView,也可以更进一步地理解 ListView 和 RecyclerView 的区别。本文将基于 API28 分析 ListView 源码。

RecycleBin

RecycleBin 是 AbsListView 中的一个内部类,所以继承于 AbsListView 的子类,也就是 ListView 和 GridView,都可以使用这个类。RecycleBin 机制是 ListView 能够实现成百上千条数据都不会 OOM 的一个重要原因。

类注释

    /**
     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
     * start of a layout. By construction, they are displaying current information. At the end of
     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
     * could potentially be used by the adapter to avoid allocating views unnecessarily.
     */

通过类注释,可以得知:

RecycleBin 有两个重要的存储:ActiveViews 和 ScrapViews。ActiveViews 是在布局开始时在屏幕上显示的那些视图。在布局结束时,ActiveViews 中的所有视图都降级为 ScrapViews。ScrapViews 是适配器可能使用的旧视图,避免不必要地重新分配视图。

主要成员变量

    private View[] mActiveViews = new View[0];

    // 之所以使用集合数组,是因为可能有多种类型的 item,同一类型的废弃 item 放在同一 list 中
    private ArrayList<View>[] mScrapViews;
    
    // 指向 scrapViews[0](只有一种类型 item 的时候使用它)
    private ArrayList<View> mCurrentScrap;
    
    private ArrayList<View> mSkippedScrap;

主要方法

先看一下主要的方法:

  • void fillActiveViews(int childCount, int firstActivePosition):第一个参数表示要存储的 View 的数量,第二个参数表示 ListView 中第一个可见元素的索引。调用该方法后就可以根据参数将 ListView 中的指定元素存储到 mActiveViews 数组中。
  • View getActiveView(int position):根据索引获取相应的 ActiveView,获取到后就将该 View 从 ActiveViews 从移除,下次再获取该位置的 ActiveView,将会返回 false,也就是说 ActiveView 不能被重复利用。
  • void addScrapView(View scrap, int position):该方法将一个废弃(比如滚动出了屏幕)的 View 缓存起来。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 来存储废弃的 View。
  • View getScrapView(int position):根据索引找到对应类型的 ScrapViews,并从中获取一个 ScrapView 返回。
  • public void setViewTypeCount(int viewTypeCount):Adapter 可以重写 getViewTypeCount() 方法来表示 ListView 有几种类型的 item,而 setViewTypeCount 根据类型数来初始化 mScrapViews 数组,mCurrentScrap 指向第 0 号数组,所以如果只有一种类型,就可以使用 mCurrentScrap;如果有多种类型,就使用 mScrapViews。

onLayout

View 的三大流程中,对于 ListView 而言,onMeasure 并没有什么特别的,因为它终归是一个 View,占用的空间最多也就是整个屏幕。onDraw 也没有什么意义,因为 ListView 本身并不负责绘制,绘制的任务交由子元素自己完成。ListView 大部分的神奇功能都是在 onLayout 中完成的,因此下面分析一些 ListView 的 onLayout过程。

ListView 并没有重写 onLayout 方法,重写 onLayout 的逻辑在其父类 AbsListView 中:

AbsListView#onLayout

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

        layoutChildren();

        // ...
    }

主要看 layoutChildren 方法,该方法对子元素进行布局,该方法在 AbsListView 是一个空方法,ListView 重写了该方法:

ListView#layoutChildren

    @Override
    protected void layoutChildren() {
		// ...

        try {
			// ...
			
			final int childrenTop = mListPadding.top;
			// 当前拥有的子 View 个数,第一次 layout 时子 View 个数为 0
            final int childCount = getChildCount();

			// ...

			// 在调用 adapter.notifyDatasetChanged() 方法时,dataChanged 为 true
			// 默认情况下,dataChanged 为 false
            boolean dataChanged = mDataChanged;

			// ...

            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
				// 将当前所有 item 的 View 添加到 RecycleBin 的 ScrapViews 中保存起来
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
				// 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // 清除所有子 View
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

			// 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
            switch (mLayoutMode) {
			// ...
			
            default:
				// 第一次 onLayout 时,childCount 为 0
                if (childCount == 0) {
					// 判断布局是从上往下还是从下往上,默认为从上往下,进入 if 块
                    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);
                    }
                } 
				// 非第一次 onLayout,childCount 不为 0,包括两种情况:
				// 1. 首次布局中的第二次 onLayout
				// 2. 后续已经存在子 View,但数据发送改变时,例如调用了 adapter.nitifyDatasetChanged()
				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;
            }

            // 将未使用的 ActiveViews 移动到 ScrapViews 中
            recycleBin.scrapActiveViews();

			// ...
			
			// 布局完成后,重置 layoutMode 和 mDataChanged 
            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
	
			// ...
        } // ...
    }

该方法较长,只列出了主要代码。重点看 switch 块,这里根据 layoutMode 进行布局,一般走 default。现在先分析第一次 onLayout 的情况,默认从上往下布局,调用 fillFromTop 方法:

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

该方法先保证 mFirstPosition 的合理性,之后调用了 fillDown 方法:

ListView#fillDown

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

        int end = (mBottom - mTop);
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            end -= mListPadding.bottom;
        }

		// 当子元素超出当前屏幕或全部子元素遍历完时,退出循环
        while (nextTop < end && pos < mItemCount) {
            boolean selected = pos == mSelectedPosition;
			// 添加子 View
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

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

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

重点看 makeAndAddView 方法,该方法用于添加子 View

ListView#makeAndAddView

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
			// 先尝试从 ActiveView 中获取
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }

		// 通过 obtainView 方法获取子 View
        final View child = obtainView(position, mIsScrap);

		// 测量和放置子 View
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

该方法先从 RecycleBin 的 ActiveViews 或通过 obtainView 方法获取子 View,再通过 setupChild 方法测量和放置子 View。

第一次 layout 时,RecycleBin 并没有缓存 ActiveViews,所以只能通过 obtainView 方法获取子 View,ListView 并没有该方法,该方法在其父类 AbsListView 中

AbsListView#obtainView

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

		// 从 ScrapViews 中获取一个 scrapView
        final View scrapView = mRecycler.getScrapView(position);
		// 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
				// 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }

            }
        }
        // ...

        return child;
    }

在该方法,先从 ScrapViews 中获取一个 scrapView,之后调用 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入。

在第一次 layout 中,由于 scrapView 为 null,所以所有的子 View 都是通过 LayoutInflater 的 inflate 方法加载出来的,相对比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

第二次 layout

在某些手机版本中(9.0 版本好像没有这种情况),View 在展示到界面上时会经历两次 onLayout。如果 ListView 进行了两次 onLayout 的话,就会存在一份重复的元素了。因此 ListView 在 layoutChildren 中对第二次 layout 做了处理,非常巧妙地解决了这个问题。

下面就来分析一些 ListView 的第二次 layout 过程,首先看 layoutChildren 方法中的变化:

ListView#layoutChildren

    @Override
    protected void layoutChildren() {
		// ...

        try {
			// ...

			// 当前拥有的子 View 个数,第二次 layout 时子 View 个数不为 0
            final int childCount = getChildCount();

			// ...

			// 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
            recycleBin.fillActiveViews(childCount, firstPosition);

            // 清除所有子 View
            detachAllViewsFromParent();

			// 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
            switch (mLayoutMode) {
			// ...
			
            default:
                if (childCount == 0) {// ...}
                // 第二次 layout 时进入 else 块
				else {
				    // 一开始没有选中 item,mSelectedPosition 的值为 -1
				    // 所以不会进入 if 块,而是调用 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;
            }
			// ...
        } // ...
    }

在第二次 layout 中,子 View 数量不为 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除所有旧的子 View。由于子 View 数量不为 0,之后会调用 fillSpecific 方法:

ListView#fillSpecific

    private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
		
		// 获取并设置当前 position 的子 View
        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;

		// 以 position 为中心,分别向上和向下获取并设置其他子 View
        if (!mStackFromBottom) {
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            adjustViewsUpOrDown();
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                correctTooHigh(childCount);
            }
        } else {
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            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;
        }
    }

该方法先设置当前 position 的子 View,然后以 position 为中心,分别向上和向下设置其他子 View。由于第二次 layout 时传入的 position 就是第一个子 View 的位置,所以和第一次 layout 的布局顺序是差不多的。获取并设置子 View 还是通过 makeAndAddView 方法。

ListView#makeAndAddView

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
			// 第二次 layout 时,可以从 ActiveViews 中获取到子 View
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }
        
        // ... 
    }

这里和第一次 layout 不同的是,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

小结

第一次 layout 时,由于当前子 View 数量为 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都没有缓存,所以只能在 Adapter 的 getView 方法中,通过 LayoutInflate 的 inflate 方法加载子 View,相对来说比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

在某些手机版本中,第一次显示 ListView 时可能会发生两次 layout。和第一次 layout 过程不同,在进行第二次 layout 时,子 View 数量不为 0,就可以先将所有子View 添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除旧的子 View,之后再次设置新的子 View 时,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

(注:在 Android 9.0 版本中,Button 显示时调用了两次 onMeasure、一次 onLayout、两次 onDraw;TextView 显示时调用了两次 onMeasure、一次 onLayout、一次 onDraw;ListView 会调用多次 onMeasure、一次 onLayout、多次 onDraw。所以在 9.0 版本并不会发生第二次 layout。)

滑动加载更多数据

上面 layout 过程分析的只是加载第一页的数据,如果有很多数据,剩下的数据将会在滑动过程中加载。下面将分析一下滑动加载数据的过程。

该过程涉及到事件分发,所以是从 AbsListView 的 onTouchEvent 方法开始,滑动对应 ACTION_MOVE,所以接下来调用 onTouchMove 方法,里面又有一个 switch 语句判断 mTouchMode,这里对应 TOUCH_MODE_SCROLL,所以接下来调用 scrollIfNeeded 方法,里面又继续调用 trackMotionScroll 方法。

下面看一下 trackMotionScroll 方法:

AbsListView#trackMotionScroll

    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
		// ...

		// incrementalDeltaY < 0,说明是向下滑动(这里指内容,手指是向上滑动的)
        final boolean down = incrementalDeltaY < 0;

		// getHeaderViewsCount 和 getFooterViewsCount 默认返回 0
        final int headerViewsCount = getHeaderViewsCount();
        final int footerViewsStart = mItemCount - getFooterViewsCount();

        int start = 0;	// 开始移除的索引
        int count = 0; 	// 移除的数量

		// 向下滑动
        if (down) {
            int top = -incrementalDeltaY;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
			// 从上往下遍历子 View
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
				// 如果该子 View 的 bottom 值大于等于滑动的距离
				// 说明该子 View 以及其后的 View 都在屏幕上,退出循环
                if (child.getBottom() >= top) {
                    break;
                } 
				// 如果该子 View 的 bottom 值小于滑动的距离,说明该子 View 已经不在屏幕上
				else { 
                    count++;
                    int position = firstPosition + i;	// 该子 View 的索引
					// 将不在屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        } else {
			// 向上滑动,和向下滑动的过程相似,也是将不在屏幕上的子 View 添加进 RecycleBin 的 ScrapViews 中
			// ...
        }

		// 将不在屏幕的子 View 全部 detach 掉
        if (count > 0) {
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        }

		// 让所有的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果
        offsetChildrenTopAndBottom(incrementalDeltaY);
        
        // 向下滑动时,更新 mFirstPosition(之后填充布局时会用到)
        if (down) {
            mFirstPosition += count;
        }

        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
		// 如果第一个 View 的顶部或最后一个 View 的底部移入屏幕
		// 说明要加载屏幕外的数据来填充布局
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }
	
		// ...

        return false;
    }

该方法首先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后调用 fillGap 方法加载屏幕外的数据来填充布局,fillGap 在 AbsListView 是一个抽象方法,ListView 中有具体实现。

ListView#fillGap

    @Override
    void fillGap(boolean down) {
        final int count = getChildCount();
		// 如果是向下滑动,就通过 fillDown 方法从上往下添加子 View
        if (down) {
            int paddingTop = 0;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                paddingTop = getListPaddingTop();
            }
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                    paddingTop;
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } 
		// 如果是向下滑动,就通过 fillUp 方法从下往上添加子 View
		else {
            int paddingBottom = 0;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                paddingBottom = getListPaddingBottom();
            }
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                    getHeight() - paddingBottom;
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }

该方法根据滑动方向,调用 fillDown 或 fillUp 方法添加子 View,无论调用拿个方法,最终都是调用 makeAndAddView 方法:

ListView#makeAndAddView

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
			// 先尝试从 ActiveViews 中获取
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }

		// 通过 obtainView 方法获取子 View
        final View child = obtainView(position, mIsScrap);

		// 测量和放置子 View
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

先从 RecycleBin 的 ActiveViews 中获取,如果还没有进行第二次 layout 的话,是可以获取到的,如果已经进行过第二次 layout,那么就获取不到了,因为第二次 layout 的时候已经从 ActiveViews 中拿到过子 View,而 ActiveViews 不能重复利用,所以就获取不到了。

如果 ActiveViews 获取不到,就会调用 obtainView 方法获取:

AbsListView#obtainView

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

		// 从 ScrapViews 中获取一个 scrapView
        final View scrapView = mRecycler.getScrapView(position);
		// 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
				// 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }

            }
        }
        // ...

        return child;
    }

这次和第一次 layout 的情况不一样,因为之前把移除屏幕的子 View 添加到了 ScrapViews 中,所以现在就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以利用这个缓存 View,不用再 inflate 一个子 View 了。

小结

ListView 在滑动时,先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并从父布局中 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后通过加载屏幕外的数据来填充布局,这时就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以重复利用这个缓存 View,无需再重新 inflate 一个子 View。

Adapter 相关

ListView 只是负责展示各子 View,各子 View 具体如何填充数据是交由 Adapter 来完成的。ListView 通过 setAdapter 方法和 Adapter 建立联系。先看一下该方法:

ListView#setAdapter

    @Override
    public void setAdapter(ListAdapter adapter) {
		
		// 如果之前绑定过 Adapter,先取消注册 AdapterDataSetObserver
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();

		// 设置新的 Adapter
        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
			// 如果 ListView 有 headerView 或 footerView,需包装传入的 adapter
            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

        // AbsListView#setAdapter will update choice mode states.
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
			// 设置 item 个数
            mItemCount = mAdapter.getCount();
            checkFocus();

			// 生成并在 Adapter 中注册 AdapterDataSetObserver,用于通知数据源的改变
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

            //...
        } // ...

        // 进行视图重绘
        requestLayout();
    }

在该方法中,ListView 绑定传入的 adapter,并为 adapter 注册 AdapterDataSetObserver,用于通知数据源的改变。 最后调用 requestLayout 方法,该方法最终会调用 ListView 的 onLayout,来到第一次 onLayout 的过程。

如果数据源发生了改变,想要更新 ListView 的时候,我们会调用 Adapter 的 notifyDataSetChanged 方法:

BaseAdapter#notifyDataSetChanged

    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

又调用了 DataSetObservable 的 notifyChanged:

    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

这里的 mObservers 定义在 DataSetObservable 的父类 Observable 中:

    protected final ArrayList<T> mObservers = new ArrayList<T>();

mObservers 的元素是从哪里来的呢?要从 setAdapter 的这一句说起:

    mAdapter.registerDataSetObserver(mDataSetObserver);

这一句最终调用了 Observable 的 registerObserver 方法:

    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

可以看到,这里将 Adapter 注册的 AdapterDataSetObserver 添加进了 mObservers 中。

所以饶了一大圈,Adapter 的 notifyDataSetChanged 方法最终调用了 AdapterDataSetObserver(AdapterView 的一个内部类)的 onChanged 方法:

AdapterDataSetObserver#onChanged

    @Override
    public void onChanged() {
        // 将 mDataChanged 属性设置为 true
        mDataChanged = true;
        mOldItemCount = mItemCount;
        // 更新 item 数量
        mItemCount = getAdapter().getCount();
        
        // ...
        
        // 最后进行视图重绘
        requestLayout();
    }

在该方法中,首先将 mDataChanged 属性设置为 true,并更新 item 数量,最后进行视图重绘,在 onLayout 中更新子 View。

写在最后

到此为止,对于 ListView 的 分析就告一段落了。在分析 ListView 的过程,发现经常遇到也是最重要的就是 onLayout 过程以及 RecycleBin 机制。

无论是设置 Adapter 还是通知 Adapter 更新数据的过程,最终都会回到视图重绘,也就是 onLayout。而 onLayout 过程也会根据是第一次 layout、第二次 layout 还是数据源改变的情况从不同途径获取子 View,是通过 inflate 加载还是从 RecycleBin 的 ActiveViews 或 ScrapViews 获取。

RecycleBin 对子 View 的回收也是 ListView 的一个重点或是巧妙之处,在第二次 layout 时,会把子 View 添加到 RecycleBin 的 ActiveViews 中,之后获取新的子 View 时就可以直接从 ActiveViews 获取。在滑动过程中,滑出屏幕的子 View 又会被添加到 RecycleBin 的 ScrapViews 中,在之后填充布局时重新利用。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值