美文网首页
ListView源码分析-上篇

ListView源码分析-上篇

作者: leilifengxingmw | 来源:发表于2021-01-31 13:17 被阅读0次

首先扯点别的:准备把ListView和RecyclerView的源码都看一遍,然后记录一下。网上已经有很多相关文章写的很好了,准备对照源码,参考别人的文章,然后整理出自己的文章来,形成自己的体系。

源码版本:28

本篇要点

  1. ListView的缓存类AbsListView.RecycleBin。
  2. 设置适配器后的流程。
  3. ListView的measure、layout、draw流程,其中包含自定义的适配器的getView方法的调用。
  4. 适配器调用notifyDataSetChanged方法后的流程。

ListView的缓存类AbsListView.RecycleBin

public abstract class AbsListView extends AdapterView<ListAdapter> {

    /**
     * 用来存储未被使用到的views,这些views在下一次layout过程中应该会被用到从而避免创建新的views。
     */
    final RecycleBin mRecycler = new RecycleBin();
    
    /**
     * RecycleBin用来在布局过程中重用views。RecycleBin有两级存储:ActiveViews 和 ScrapViews。
     * ActiveViews是那些在布局开始的时候在屏幕上的View。在布局结束的时候,ActiveViews中未被用到的View会被移动到ScrapViews中。
     * ScrapViews是老的View,这些View可能被适配器使用,从而避免创建新的View。
     */
    class RecycleBin {
        
       /**
        * 在布局(layout)开始的时候在屏幕上的Views。在布局开始的时候会将屏幕上的Views添加到mActiveViews中。在布局结束的时候,
        * mActiveViews中的Views会被移动到mScrapViews中。mActiveViews中的Views一个范围内连续的Views。
        * 第一个View的存储在mActiveViews中下标为mFirstActivePosition的位置上。
        */
       private View[] mActiveViews = new View[0];
       /**
        * 未排序的Views可以作为 ConvertView被适配器使用。
        */
       private ArrayList<View>[] mScrapViews;

       /**
        * 当只有一种ViewType类型的时候,mCurrentScrap是mScrapViews的第一个元素。
        */
       private ArrayList<View> mCurrentScrap;
       //...
    }
}

RecycleBin有两级存储:mActiveViews和mScrapViews。mActiveViews存储的是那些在布局(layout)开始的时候在屏幕上的View,在布局结束的时候,mActiveViews中未被用到的View会被移动到mScrapViews中。mScrapViews中还存储着滑出屏幕的的View,这些View可能被适配器使用,从而避免创建新的View。

mScrapViews是一个元素为ArrayList<View>的数组,对应适配器多种ViewType,当只有一种ViewType类型的时候,mCurrentScrap是mScrapViews的第一个元素。

ListView基本的使用方法

<ListView
    android:id="@+id/listView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

自定义适配器

class ListViewAdapter(
        context: Context,
        private val resource: Int,
        lists: ArrayList<MyBean>
) : ArrayAdapter<MyBean>(context, resource, lists) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View
        val holder: ViewHolder
        val bean = getItem(position)
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(resource, parent, false)
            holder = ViewHolder()
            holder.textViewTitle = view.findViewById(R.id.tvTitle)
            holder.textViewDetail = view.findViewById(R.id.tvDetail)
            view.tag = holder
        } else {
            //注释1处,返回的convertView不为null,直接复用
            view = convertView
            holder = view.tag as ViewHolder
        }
        //绑定数据
        holder.textViewTitle?.text = bean?.title
        holder.textViewDetail?.text = bean?.detail
        return view
    }

    //Holder类,用来提高listView的性能
    private class ViewHolder {
        var textViewTitle: TextView? = null
        var textViewDetail: TextView? = null
    }
}

注释1处,返回的convertView不为null,复用。

ListView设置适配器

adapter = ListViewAdapter(this, R.layout.item_list_view, list)
listView.adapter = adapter

基本使用应该都很熟了,不再赘述。

ListView的setAdapter方法

@Override
public void setAdapter(ListAdapter adapter) {
    //移除老的观察者
    if (mAdapter != null && mDataSetObserver != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }
    //注释1处,重置某些变量
    resetList();
    //注释2处,清空缓存中所有等待复用的View
    mRecycler.clear();

    //如果ListView添加了header或者footer,则包装适配器
    if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
        mAdapter = adapter;
    }

    mOldSelectedPosition = INVALID_POSITION;
    mOldSelectedRowId = INVALID_ROW_ID;

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

    if (mAdapter != null) {
        mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
        mOldItemCount = mItemCount;
        //mItemCount,适配器中数据的数量,通常就是我们传入适配器中list的size
        mItemCount = mAdapter.getCount();
        checkFocus();
        //注册新的观察者,观察适配器的数据变化,数据变化后ListView会调用requestLayout方法
        mDataSetObserver = new AdapterDataSetObserver();
        mAdapter.registerDataSetObserver(mDataSetObserver);
        //mRecycler设置适配器ViewType的数量。
        mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

        //...
    } 
    //注释3处,最重要的一步,请求measure、layout、draw
    requestLayout();
}

ListView的resetList方法

@Override
void resetList() {
    //...  
    //调用AbsListView的resetList方法
    super.resetList();
    
    //将布局模式设置为LAYOUT_NORMAL
    mLayoutMode = LAYOUT_NORMAL;
}

AbsListView的resetList方法

void resetList() {
    //移除所有的子View
    removeAllViewsInLayout();
    //将mFirstPosition置为0
    mFirstPosition = 0;
    //mDataChanged置为false
    mDataChanged = false;
    mPositionScrollAfterLayout = null;
    mNeedSync = false;
    mPendingSync = null;
    mOldSelectedPosition = INVALID_POSITION;
    mOldSelectedRowId = INVALID_ROW_ID;
    setSelectedPositionInt(INVALID_POSITION);
    setNextSelectedPositionInt(INVALID_POSITION);
    mSelectedTop = 0;
    mSelectorPosition = INVALID_POSITION;
    mSelectorRect.setEmpty();
    invalidate();
}

resetList方法内部会将mLayoutMode置为LAYOUT_NORMAL,移除所有的子View,将mDataChanged置为false。适配器调用notifyDataSetChanged以后,mDataChanged为true。这里先提一下。

setAdapter方法注释3处,最重要的一步,调用requestLayout方法请求measure、layout、draw。

ListView 的测量过程

ListView的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

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

    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
       
    if (heightMode == MeasureSpec.AT_MOST) {
        //注释1处,决定最终高度,最后一个参数是-1注意一下。
        heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
    }
    //保存宽高信息
    setMeasuredDimension(widthSize, heightSize);

    mWidthMeasureSpec = widthMeasureSpec;
}

我们平时使用ListView的宽高一般都是 MATCH_PARENT,有时候高度会设置为WRAP_CONTENT,这个时候测量模式就是 MeasureSpec.AT_MOST 。注释1处,如果高度的是测量模式是 MeasureSpec.AT_MOST,则会调用measureHeightOfChildren方法。

ListView 的 measureHeightOfChildren 方法。

/**
 * 测量指定范围内的子View的高度,返回的高度包括ListView的padding和分割线的高度。
 * 如果指定了最大高度,那么当测量高度到达最大高度以后测量会停止测量。
 *
 * @param widthMeasureSpec The width measure spec to be given to a child's
 *            {@link View#measure(int, int)}.
 * @param startPosition The position of the first child to be shown.
 * @param endPosition The (inclusive) position of the last child to be
 *            shown. Specify {@link #NO_POSITION} if the last child should be
 *            the last available child from the adapter.
 * @param maxHeight 如果指定范围内子View的累加高度超过了maxHeight,就返回maxHeight。

 * @param disallowPartialChildPosition 通常情况下,返回的高度是否只包括完整的View ,暂时不关注。

 * @return The height of this ListView with the given children.
 */
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
        int maxHeight, int disallowPartialChildPosition) {
    final ListAdapter adapter = mAdapter;
    if (adapter == null) {
        return mListPadding.top + mListPadding.bottom;
    }

    // 指定范围内子View的所有累加高度,包括ListView的 padding 和 分割线高度。
    int returnedHeight = mListPadding.top + mListPadding.bottom;
    final int dividerHeight = mDividerHeight;
    // The previous height value that was less than maxHeight and contained
    // no partial children
    int prevHeightWithoutPartialChild = 0;
    int i;
    View child;

    // mItemCount - 1 since endPosition parameter is inclusive
    endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
    final AbsListView.RecycleBin recycleBin = mRecycler;
    //注释1处,在MeasureSpec.AT_MOST模式下是否在测量过程中将View加入到recycleBin,默认是true
    final boolean recyle = recycleOnMeasure();
    final boolean[] isScrap = mIsScrap;

    for (i = startPosition; i <= endPosition; ++i) {
        //获取子View
        child = obtainView(i, isScrap);

        measureScrapChild(child, i, widthMeasureSpec, maxHeight);

        if (i > 0) {
            // Count the divider for all but one child
            returnedHeight += dividerHeight;
        }

        // 是否在测量过程中将 View 加入到 recycleBin
        if (recyle && recycleBin.shouldRecycleViewType(
                ((LayoutParams) child.getLayoutParams()).viewType)) {
            recycleBin.addScrapView(child, -1);
        }

        returnedHeight += child.getMeasuredHeight();

        if (returnedHeight >= maxHeight) {
            //...
            return maxHeight;
        }
        //...
    }

    // 指定范围内所有的子View累加高度没有超过 maxHeight,就返回returnedHeight。
    return returnedHeight;
}

注释1处,在MeasureSpec.AT_MOST模式下是否在测量过程中将View加入缓存,默认是true。那么就是说在MeasureSpec.AT_MOST模式下,第一次在测量过程中,就会通过obtainView获取View(第一次测量获取的View是新创建的)并加入到缓存中。

这里我们先不关注这种场景,我们以ListView的宽高都是都是 MATCH_PARENT 的情况,即宽高的测量模式都是 MeasureSpec.EXACTLY 模式来分析。所以我们简单的认为ListView 的 onMeasure 方法就是简单的保存了宽高信息即可。

ListView 的布局过程

ListView 的 onLayout 方法

注意:开始第一次布局的时候,此时所有的数据都在适配器中,而ListView是没有子View的(即ListView还没有调用过addViewInLayout方法添加子View),所以 getChildCount = 0 。

AbsListView 的 onLayout 方法。

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

    //标记正在layout,避免重复layout
    mInLayout = true;
    //注释1处,此时获取的 childCount 为 0 。
    final int childCount = getChildCount();
    if (changed) {
    //注释2处,ListView位置或者大小发生了改变,强制子View在下一个layout过程中重新布局
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }
    //注释3处,布局子View
    layoutChildren();

    //...
    //布局结束后,将mInLayout置为false
    mInLayout = false;
}

注释1处,此时获取的childCount 为0 。

注释2处,ListView位置或者大小发生了改变,changed为true。强制子View在下一个layout过程中重新布局。比如我们在第一次布局测量出来ListView的宽高是1080 * 1960,这时候changed就是true。

注释3处,布局子View。

ListView的layoutChildren方法。

@Override
protected void layoutChildren() {
    //如果阻止布局就直接返回。
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }

    mBlockLayoutRequests = true;

    try {
        super.layoutChildren();

        invalidate();

        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }
        //布局子View最上面的坐标
        final int childrenTop = mListPadding.top;
        //布局子View最下面的坐标
        final int childrenBottom = mBottom - mTop - mListPadding.bottom;
        //注释1处,记录ListView当前的子View个数,第一次布局的时候为0。
        final int childCount = getChildCount();

        int index = 0;
        int delta = 0;

        //...

        //只有在调用adapter.notifyDataSetChanged()方法一直到layout()布局结束,dataChanged为true,默认为false
        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }

        //mItemCount是mAdapter.getCount()返回的数据的数量
        if (mItemCount == 0) {
            resetList();
            invokeOnItemScrollListener();
            return;
        } else if (mItemCount != mAdapter.getCount()) {
        //在布局过程中,adapter的数据发生了改变但是没调用notifyDataSetChanged方法会抛出异常。
            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. Make sure your adapter calls notifyDataSetChanged() "
                    + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                    + ") with Adapter(" + mAdapter.getClass() + ")]");
        }

        //...
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        if (dataChanged) {//数据发生了改变,将所有的子View加入到RecycleBin。这些View将来可能会被复用。
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            //注释2处,dataChanged为false,会走到这里
            recycleBin.fillActiveViews(childCount, firstPosition);
        }

        //注释3处,将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。
        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();
        //测量模式默认是LAYOUT_NORMAL,会走到 default分支
        switch (mLayoutMode) {
        //...
        default:
            //注释4处,此时childCount为0
            if (childCount == 0) {
                //mStackFromBottom标记是否从ListView的底部向上填充,默认是false
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    //注释5处
                    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;
        }

        //注释6处,将mActiveViews没有用到的View移动到mScrapViews中。
        recycleBin.scrapActiveViews();

        //...
        //将mLayoutMode置为LAYOUT_NORMAL
        mLayoutMode = LAYOUT_NORMAL;
        //将mDataChanged置为false
        mDataChanged = false;
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }
            //...
    }
}

注释1处,记录ListView当前的子View个数,第一次布局的时候为0。

注释2处,dataChanged为false,RecycleBin调用fillActiveViews方法。

RecycleBin的fillActiveViews方法。


/**
 * 使用AbsListView的所有子View填充 ActiveViews 。
 *
 * @param childCount mActiveViews要持有的View的最少数量。
 * @param firstActivePosition ListView中第一个要存储在mActiveViews的子View在ListView中的位置。
 */
void fillActiveViews(int childCount, int firstActivePosition) {
    if (mActiveViews.length < childCount) {
        mActiveViews = new View[childCount];
    }
    mFirstActivePosition = firstActivePosition;

    //noinspection MismatchedReadAndWriteOfArray
    final View[] activeViews = mActiveViews;
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        //不要将headView和footView加入mActiveViews 
        if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            activeViews[i] = child;
            lp.scrappedFromPosition = firstActivePosition + i;
        }
    }
}

如果ListView已经有子View了,即childCount大于0,就将子View添加到mActiveViews数组中,第一次布局的时候childCount为0,该方法调用无意义。

注释3处,调用detachAllViewsFromParent方法将子View和ListView取消关联。此时ListView中是没有子View的,所以调用此方法也没意义。注意:ViewGroup的子类调用此方法以后getChildCount返回值为0。

注释4处,此时childCount为0。

mStackFromBottom标记是否从ListView的底部向上填充,默认是false,所以会走到注释5处。调用fillFromTop方法。

ListView的fillFromTop方法。

/**
 * 从mFirstPosition开始,从上到下填充ListView。
 *
 * @param nextTop 第一个要被绘制的itemView的top坐标。
 *
 * @return 当前选中的itemView
 */
private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    //调用fillDown方法
    return fillDown(mFirstPosition, nextTop);
}

ListView的fillDown方法

/**
 * 从pos开始向下填充ListView。
 *
 * @param pos 第一个要填充到ListView的位置。
 *
 * @param nextTop 第一个要填充到ListView的位置的itemView的top坐标。
 *
 * @return 如果当前选中的itemView出现在我们的绘制范围内的话,则返回该itemView。否则返回null。
 */
private View fillDown(int pos, int nextTop) {
    View selectedView = null;
    
    //ListView的底部坐标
    int end = (mBottom - mTop);

    //注释1处,如果要填充的itemView的top坐标小于end并且pos小于mItemCount,则循环填充。
    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        //注释2处,创建并将子View添加到ListView中
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
        
        //注释3处,累加nextTop,包含了分割线的高度mDividerHeight
        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        //累加pos
        pos++;
    }

    return selectedView;
}

注释1处,如果要填充的itemView的top坐标小于end并且pos小于mItemCount,则循环填充。mItemCount是适配器中数据的数量。

我们先看下注释3处,累加nextTop,如果要添加分割线的话下一个子View的top坐标是加上了分割线的高度mDividerHeight。这样就是说每个子View之前空出来了一个分割线的高度,然后我们在绘制的时候,在每个子View空出来的位置绘制分割线,然后再绘制子View。

注意:ListView并不是绘制一个子View绘制一个分割线这样交替绘制的。而是先把所有要绘制的分割线绘制出来,然后在绘制子View。

我们回过头来在看注释2处,创建并将子View添加到ListView中。

ListView 的 makeAndAddView 方法。

/**
 * 获取View并将其添加到子View列表中。View可以是新创建的,从未被使用的View转化来的,或者从RecyclerBin中获取的。
 *
 * @param position 该View在ListView的mChildren数组中的位置。
 * @param y 该View被添加到ListView的top坐标或者bottom坐标。
 * @param flow true 将View的上边界和y对齐。即 y是View的top坐标。 false 将View的下边界和y对齐。即 y是View的bottom坐标。
 * @param childrenLeft View的left坐标。
 * @param selected 这个位置上的View是否被选中。

 * @return 该View
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
     
     // 默认情况mDataChanged==false,在调用adapter.notifyDataSetChanged()之后的
     // layout阶段中mDataChanged == true
     if (!mDataChanged) {
     //注释1处,第一次布局的时候childCount为0,getActiveView是获取不到的。
     final View activeView = mRecycler.getActiveView(position);
         if (activeView != null) {
             //找到可直接使用的View,将其添加到ListView中去。
             setupChild(activeView, position, y, flow, childrenLeft, selected, true);
             return activeView;
         }
     }

     //注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。
     final View child = obtainView(position, mIsScrap);
   
     //注释3处 通过obtainView获取到的View需要被重新测量和布局。注意调用setupChild方法的最后一个参数。
     setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

     return child;
}

注释1处,首先从mRecycler的mActiveViews数组中尝试获取可复用的View,在前面layoutChildren方法中,如果mDataChanged为false会尝试把View放到mRecycler的mActiveViews中保存。第一次布局的时候childCount为0,mRecycler的mActiveViews数组是空数组,getActiveView是获取不到的。

注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。

AbsListView的obtainView方法。

/**
 * 获取一个View并让它展示指定位置上的数据。
 *
 * @param position the position to display
 * @param outMetadata
 *
 * @return 返回一个展示了指定位置上的数据的View
 */
View obtainView(int position, boolean[] outMetadata) {
    //...
    //注释1处,先从mRecycler的mScrapViews数组中获取一个在滑动时候废弃保存的子view
    final View scrapView = mRecycler.getScrapView(position);
    //注释2处,调用平时写的adapter的getView()方法获取View,注意第二个参数传入了scrapView
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            //注释3处,如果child != scrapView和不相等,就是说我们无法复用scrapView,
            //那么就再将scrapView保存起来后续复用。
            mRecycler.addScrapView(scrapView, position);
        }
        //...
    }

    //...
    //返回获取到的View
    return child;
}

注释1处,先从mRecycler的mScrapViews数组中获取一个在滑动时候废弃保存的子View。第一个布局的时候是获取不到的。

注释2处,调用适配器的getView方法获取View,注意第二个参数传入了scrapView。


override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View
    val holder: ViewHolder
    val bean = getItem(position)
    //开始convertView为null,创建新的View返回
    if (convertView == null) {
        //注意这里会为创建的View设置ListView的布局参数AbsListView.LayoutParams
        view = LayoutInflater.from(context).inflate(resource, parent, false)
        holder = ViewHolder()
        holder.textViewTitle = view.findViewById(R.id.tvTitle)
        holder.textViewDetail = view.findViewById(R.id.tvDetail)
        view.tag = holder
    } else {
        //convertView不为null的时候,直接使用convertView
        view = convertView
        holder = view.tag as ViewHolder
    }
    //将View与数据绑定以后返回
    holder.textViewTitle?.text = bean?.title
    holder.textViewDetail?.text = bean?.detail
    return view
}

在适配器的getView方法中,如果convertView == null,那么我们需要新创建View,否则的话直接复用convertView,将convertView和新的数据绑定就行。

我们回到makeAndAddView方法的注释3处ListView的setupChild方法。

/**
 * 将一个子View添加到ListView中去,如果必要的话测量子View并将子View布局在合适的位置。
 *
 * @param child the view to add
 * @param position the position of this child
 * @param y 要添加的View的top坐标或者bottom坐标
 * @param flowDown true的话,y参数是要添加的View的top坐标,false,y参数是View的bottom坐标。
 * @param childrenLeft left edge where children should be positioned
 * @param selected {@code true} if the position is selected, {@code false}
 *                 otherwise
 * @param isAttachedToWindow 要添加的View是否已经添加到window上了。
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
            && mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    //注释1处,如果child未被添加到ListView中过,或者选中状态发生了改变,或者调用了child的forceLayout方法,则需要重新测量child。
    final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
            || child.isLayoutRequested();

    // Respect layout params that are already in the view. Otherwise make
    // some up...
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    //child的类型viewType
    p.viewType = mAdapter.getItemViewType(position);
    p.isEnabled = mAdapter.isEnabled(position);

    //...
    
    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        //注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。
        attachViewToParent(child, flowDown ? -1 : 0, p);
        //...
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        //注释3处,将child添加到ListView。
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        //...
    }

    //需要测量child
    if (needToMeasure) {
        //...
        //注释4处,测量Child
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        //清除child的FLAG_FORCE_LAYOUT标记,避免多次测量和布局View
        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;
        //注释5处,布局child
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
        //修正child的坐标
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }

    //...

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

注释1处,如果child未被添加到ListView中过,或者选中状态发生了改变,或者调用了child的forceLayout方法,则需要重新测量child。

注意当ListView的宽高测量模式的都是EXACTLY的情况下,在自己的测量过程中是没有测量child的,所以在第一次调动setupChild会测量子View。

注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。

ViewGroup的attachViewToParent方法。


protected void attachViewToParent(View child, int index, LayoutParams params) {
    child.mLayoutParams = params;
    if (index < 0) {
        index = mChildrenCount;
    }

    //将child加入到ListView的mChildren中
    addInArray(child, index);
    //将child的父View设置为ListView
    child.mParent = this;
    child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
                       & ~PFLAG_DRAWING_CACHE_VALID)
            | PFLAG_DRAWN | PFLAG_INVALIDATED;
    this.mPrivateFlags |= PFLAG_INVALIDATED;
    
}

注释3处,需要调用addViewInLayout将child添加到ListView。

ViewGroup的addViewInLayout方法。再往里面的逻辑我们就不跟了。大致逻辑就是将child添加到ViewGroup的mChildren中,并将child attach 到当前窗口。只有调用完addViewInLayout方法以后,ListView的getChildCount才大于0。

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添加。
    addViewInner(child, index, params, preventRequestLayout);
    child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    return true;
}

setupChild方法,注释4处,测量Child。

注释5处,布局child。

到这里ListView的布局过程就结束了。

注意:第一次布局结束子View已经测量完毕并布局到正确的位置了,接下来就是绘制了。

ListView绘制子View

我们先看ListView的dispatchDraw方法。

@Override
protected void dispatchDraw(Canvas canvas) {
    final int dividerHeight = mDividerHeight;
    final Drawable overscrollHeader = mOverScrollHeader;
    final Drawable overscrollFooter = mOverScrollFooter;
    final boolean drawOverscrollHeader = overscrollHeader != null;
    final boolean drawOverscrollFooter = overscrollFooter != null;
    final boolean drawDividers = dividerHeight > 0 && mDivider != null;
    //需要绘制分割线,则先绘制绘制分割线
    if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
        // Only modify the top and bottom in the loop, we set the left and right here
        final Rect bounds = mTempRect;
        bounds.left = mPaddingLeft;
        bounds.right = mRight - mLeft - mPaddingRight;

        final int count = getChildCount();
        //...
        if (!mStackFromBottom) {
            int bottom = 0;
            //...
            for (int i = 0; i < count; i++) {
                final int itemIndex = (first + i);
                final boolean isHeader = (itemIndex < headerCount);
                final boolean isFooter = (itemIndex >= footerLimit);
                if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
                    final View child = getChildAt(i);
                    bottom = child.getBottom();
                    final boolean isLastItem = (i == (count - 1));

                    if (drawDividers && (bottom < listBottom)
                            && !(drawOverscrollFooter && isLastItem)) {
                        final int nextIndex = (itemIndex + 1);
                        // Draw dividers between enabled items, headers
                        // and/or footers when enabled and requested, and
                        // after the last enabled item.
                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
                                && (nextIndex >= headerCount)) && (isLastItem
                                || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
                                        && (nextIndex < footerLimit)))) {
                            bounds.top = bottom;
                            bounds.bottom = bottom + dividerHeight;
                            //绘制分割线
                            drawDivider(canvas, bounds, i);
                        }
                        //...
                    }
                }
            }
            //...
        } 
        //...
    }
    // 绘制子View
    super.dispatchDraw(canvas);
}

ListView重写了dispatchDraw方法,需要绘制分割线的话,ListView会先绘制分割线,再绘制子View。

注意:ListView并不是绘制一个子View绘制一个分割线这样交替绘制的。而是先把所有要绘制的分割线绘制出来,然后在绘制子View。

当绘制结束以后,ListView就可以正常显示了。

注意:在测试过程中发现,ListView会经过至少两次测量和布局过程。不知道第二次布局是怎么触发的。

class MyListView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ListView(context, attrs, defStyleAttr) {

    private val TAG: String = "MyListView"

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.i(TAG, "onMeasure: measuredWidth = $measuredWidth , measuredHeight =$measuredHeight")
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        Log.i(TAG, "onLayout: ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight changed = $changed")

    }
}

打印日志

ListView两次布局.png

但是我发现只布局一次就能正确显示啊。

package com.hm.viewdemo.widget

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.widget.ListView
import java.util.logging.Logger

/**
 * Created by dumingwei on 2021/1/24.
 *
 * Desc:
 */
class MyListView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ListView(context, attrs, defStyleAttr) {

    private var layoutCount = 0
    private var mMotionY = 0
    private val TAG: String = "MyListView"

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.i(TAG, "onMeasure: childCount ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight")
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //注释1处
        if (layoutCount == 1) {
            return
        }
        layoutCount++
        super.onLayout(changed, l, t, r, b)
        Log.i(TAG, "onLayout: childCount ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight changed = $changed")

    }
}

注释1处,如果布局完一次以后,就直接返回不进行第二次布局。

打印日志

ListView一次布局.png

这里我们就不再纠结一次布局两次布局的问题了,我们也来分析一下在第二次布局过程中layoutChildren方法的逻辑。

@Override
protected void layoutChildren() {
    //如果阻止布局就直接返回。
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }

    mBlockLayoutRequests = true;

    try {
        super.layoutChildren();

        invalidate();

        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }
        //布局子View最上面的坐标
        final int childrenTop = mListPadding.top;
        //布局子View最下面的坐标
        final int childrenBottom = mBottom - mTop - mListPadding.bottom;
        //注释1处,记录ListView当前的子View个数,第二次布局的时候大于0。
        final int childCount = getChildCount();

        int index = 0;
        int delta = 0;

        //...

        //只有在调用adapter.notifyDataSetChanged()方法一直到layout()布局结束,dataChanged为true,默认为false
        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }

        //mItemCount是mAdapter.getCount()返回的数据的数量
        if (mItemCount == 0) {
            resetList();
            invokeOnItemScrollListener();
            return;
        } else if (mItemCount != mAdapter.getCount()) {
        //在布局过程中,adapter的数据发生了改变但是没调用notifyDataSetChanged方法会抛出异常。
            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. Make sure your adapter calls notifyDataSetChanged() "
                    + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                    + ") with Adapter(" + mAdapter.getClass() + ")]");
        }

        //...
        // 将所有的子View加入到RecycleBin。这些View将来可能会被复用。
        // These views will be reused if possible
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        if (dataChanged) {//数据发生了改变,将所有的子View加入到RecycleBin。
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            //注释2处,dataChanged为false,会走到这里
            recycleBin.fillActiveViews(childCount, firstPosition);
        }

        //注释3处,将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。
        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();
        //测量模式默认是LAYOUT_NORMAL,会走到 default分支
        switch (mLayoutMode) {
        //...
        default:
            //注释4处,此时childCount为0条件不满足。
            if (childCount == 0) {
                //mStackFromBottom标记是否从ListView的底部向上填充,默认是false
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    //注释5处
                    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) {
                    //注释6处
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }

        //注释7处,recycleBin将=mActiveViews没有用到的View移动到mScrapViews中。
        recycleBin.scrapActiveViews();

        //...
        //将mLayoutMode置为LAYOUT_NORMAL
        mLayoutMode = LAYOUT_NORMAL;
        //将mDataChanged置为false
        mDataChanged = false;
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }
            //...
    }
}

注释1处,记录ListView当前的子View个数,第二次布局的时候大于0。

注释2处,dataChanged为false,调用RecycleBin的fillActiveViews方法。此时childCount大于0,会将ListView的所有子View加入到mActiveViews中。

/**
 * 使用AbsListView的所有子View填充 ActiveViews 。
 *
 * @param childCount mActiveViews要持有的View的最少数量。
 * @param firstActivePosition ListView中第一个要存储在mActiveViews的子View在ListView中的位置。
 */
void fillActiveViews(int childCount, int firstActivePosition) {
    if (mActiveViews.length < childCount) {
        mActiveViews = new View[childCount];
    }
    mFirstActivePosition = firstActivePosition;

    //noinspection MismatchedReadAndWriteOfArray
    final View[] activeViews = mActiveViews;
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        //不要将headView和footView加入mActiveViews 
        if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            activeViews[i] = child;
            lp.scrappedFromPosition = firstActivePosition + i;
        }
    }
}

注释3处,detachAllViewsFromParent(),将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。也就是说ListView不存在子View了。第一次布局后ListView中的View又都从ListView中移除了放进缓存的mActiveViews中。

然后注释6处条件会满足。

/**
 * 将一个指定的itemView放置在屏幕上的指定位置上,然后从这个位置开始向上和向下填充ListView。
 *
 * @param position 指定的itemView要摆放的位置。
 * @param top 指定的itemView的top坐标。
 *
 * @return 选中的View,如果选中的View在可见区域之外,返回null。
 */
private View fillSpecific(int position, int top) {
    boolean tempIsSelected = position == mSelectedPosition;
    //先填充该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;

    final int dividerHeight = mDividerHeight;
    if (!mStackFromBottom) {//默认情况下不会从ListView的底部开始填充,mStackFromBottom为false
        //向上填充
        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) {
            //纠正LsitView的高度
            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;
    }
}

不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。

ListView的fillUp方法。

/**
 * 从pos开始向上填充ListView。
 *
 * @param pos 第一个要填充到ListView的位置。
 *
 * @param nextBottom 要填充到ListView的位置的View的bottom坐标。
 *
 * @return 当前选中的的View
 */
private View fillUp(int pos, int nextBottom) {
    View selectedView = null;
 
    //这时候end为0
    int end = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        end = mListPadding.top;
    }
    
    //nextBottom大于0并且pos大于0,循环填充
    while (nextBottom > end && pos >= 0) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
        //减小nextBottom,包含分割线的高度mDividerHeight
        nextBottom = child.getTop() - mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        //减小pos,最终pos会减到-1
        pos--;
    }
    //最终pos会减到-1,所以mFirstPosition要加1变成0
    mFirstPosition = pos + 1;
    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
    return selectedView;
}

ListView的fillDown方法上面看过了,这里就不再看了。我们再看一下makeAndAddView方法的逻辑。

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
     
     // 默认情况mDataChanged==false,在调用adapter.notifyDataSetChanged()之后的
     // layout阶段中mDataChanged == true
     if (!mDataChanged) {
     //注释1处,这个时候getActiveView是可以获取到的。
     final View activeView = mRecycler.getActiveView(position);
         if (activeView != null) {
             //找到可直接使用的View,将其添加到ListView中去。
             setupChild(activeView, position, y, flow, childrenLeft, selected, true);
             return activeView;
         }
     }

     //注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。
     final View child = obtainView(position, mIsScrap);
   
     //注释3处 通过obtainView获取到的View需要被重新测量和布局。注意调用setupChild方法的最后一个参数。
     setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

     return child;
}

注释1处,这个时候getActiveView是可以获取到的,因为前面在第二次布局layoutChildren方法中,我们调用了RecycleBin的fillActiveViews方法将ListView的子View加入到了RecycleBin的的mActiveViews中。

RecycleBin的getActiveView方法。

/**
 * @param position 在ListView当中的位置
 */
View getActiveView(int position) {
    int index = position - mFirstActivePosition;
    final View[] activeViews = mActiveViews;
    if (index >=0 && index < activeViews.length) {
        final View match = activeViews[index];
        //注释1处,获取到以后,从mActiveViews移除
        activeViews[index] = null;
        return match;
    }
    return null;
}

注释1处,获取到以后,将View从从mActiveViews移除。

然后注释3处,会调用setupChild方法,并且最后一个参数传入的是true。表示该View是添加到当前的窗口上过了的,可以复用,直接再attach的当前窗口即可。

/**
 * 将一个子View添加到ListView中去,如果必要的话测量子View并将子View布局在合适的位置。
 *
 * @param child the view to add
 * @param position the position of this child
 * @param y 要添加的View的top坐标或者bottom坐标
 * @param flowDown true的话,y参数是要添加的View的top坐标,false,y参数是View的bottom坐标。
 * @param childrenLeft left edge where children should be positioned
 * @param selected {@code true} if the position is selected, {@code false}
 *                 otherwise
 * @param isAttachedToWindow 要添加的View是否已经添加到window上了。
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean isAttachedToWindow) {

    //...
    //注释1处,条件满足
    if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        //注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。
        attachViewToParent(child, flowDown ? -1 : 0, p);

        //...
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        //注释3处,将child添加到ListView。
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        //...
    }
    //...

}

注释1处的条件满足。所以会执行注释2处的attachViewToParent()方法,直接将child关联到ListView即可。

注意:第一次Layout过程则是执行的是注释3处的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout方法。而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent方法。那么由于前面在layoutChildren方法当中调用了detachAllViewsFromParent方法,这样ListView中所有的子View都是处于detach状态的,所以这里直接调用attachViewToParent方法,直接将child关联到ListView即可。

适配器的notifyDataSetChanged方法

我们看一下适配器ArrayAdapter的notifyDataSetChanged方法。

我们的适配器间接继承自BaseAdapter。

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

我们在ListView设置适配器的时候给适配器注册了一个AdapterDataSetObserver。最终会调用到AdapterDataSetObserver的onChanged方法。

 @Override
public void setAdapter(ListAdapter adapter) {

    mDataSetObserver = new AdapterDataSetObserver();
    mAdapter.registerDataSetObserver(mDataSetObserver);
}

最终要看AdapterView.AdapterDataSetObserver的onChanged方法。

class AdapterDataSetObserver extends DataSetObserver {

    @Override
    public void onChanged() {
        //将mDataChanged置为true。
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();
        //最终导致ListView请求重新measure、layout、draw
        requestLayout();
    }
    //...
}

适配器调用notifyDataSetChanged以后,最终导致ListView请求重新measure、layout、draw。

参考链接:

相关文章

网友评论

      本文标题:ListView源码分析-上篇

      本文链接:https://www.haomeiwen.com/subject/sjxmzktx.html