安卓开发学习之ListView的测量流程源码阅读

背景

这几天我在看Android测绘方面的源码,今日突发奇想,想看看ListView的测绘流程,所以就从onMeasure()开始,进行阅读


ListView#onMeasure

ListView的onMeasure()方法源码如下

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        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(); // adapter里的子项数量
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap); // 获取子view,由于这里还没有addView(),所以这时只能从adapter里获取第1项子view

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize); // 测量子view

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0); // 这里的判断条件:前者默认就是true,后者只要子view的viewType>=0就是true,而viewType默认就是0
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
}

先是调用了父类AbsListView的onMeasure()方法,代码如下

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mSelector == null) {
            useDefaultSelector(); // 设置selector
        }
        // 设置列表的内间距
        final Rect listPadding = mListPadding;
        listPadding.left = mSelectionLeftPadding + mPaddingLeft;
        listPadding.top = mSelectionTopPadding + mPaddingTop;
        listPadding.right = mSelectionRightPadding + mPaddingRight;
        listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;

        // Check if our previous measured size was at a point where we should scroll later.
        if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { // 这个判断一般会进来
            final int childCount = getChildCount();
            final int listBottom = getHeight() - getPaddingBottom(); // list的底部
            final View lastChild = getChildAt(childCount - 1);
            final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; // 最后一个的子view的底部
            mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount &&
                    lastBottom <= listBottom;
            // 是否需要强制滚到底部,为true的条件:前一个分支:mLastHandledItemCount被赋值成了itemCount,所以条件就是当前列表显示的第一个位置 + 子view数目 >= itemCount
            //                                 后一个分支:最后一个子view的底部<=列表显示的底部,也就是最后一个子view显示完全或腾空了
        }
     }

很简单,返回ListView.onMeasure()后,进入判断:如果itemCount正常>0,并且父view给listView的宽高指示有一个是unspecified,就调用obtainView()方法获取子项view,这里只会获取第一项,因为在这里还没有进行addView()操作,ListView里不会有子view,只能通过adapter获取第一项子view的高度,以便至少显示一项子view。


AbsListView#obtainView

那我们进入obtainView()方法,这是父类AbsListView的方法,源码如下

View obtainView(int position, boolean[] outMetadata) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        outMetadata[0] = false; // outMetadata[0]的真假性表示当前view是否从回收站里回收出来的

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position); // 从回收站中获取这个位置的短暂标记的view(标志位标记,一般为null)
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) { // view的type没有变的话,重新绑定数据
                final View updatedView = mAdapter.getView(position, transientView, this); // 调用adapter.getView()方法,绑定数据

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position); // 为新view检查并设置layoutParams
                    mRecycler.addScrapView(updatedView, position); // 更新回收站里的这个位置的view
                }
            }

            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            transientView.dispatchFinishTemporaryDetach();
            return transientView;
        }

        // 正常情况下,回收站里没有这个位置上的transiantView

        final View scrapView = mRecycler.getScrapView(position); // 尝试从回收站中获取这个位置的view
        final View child = mAdapter.getView(position, scrapView, this); // 还是调用adapter的getView()方法
        if (scrapView != null) {
            if (child != scrapView) { // 如果这个view和从回收站里拿出来的不是一个东西,就说明这个view是这个位置上的第一个view
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position); // 更新回收站中这个位置的view
            } else if (child.isTemporarilyDetached()) { // 是一回事,并且只是暂时和list分离了
                outMetadata[0] = true; // 设置标志位

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach(); // 结束分离状态,有焦点的话,捕获焦点
            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint); // 背景色
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position); // 检查并设置layoutParams

        if (AccessibilityManager.getInstance(mContext).isEnabled()) { // 无障碍处理,很少用到
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        return child;
}

我们不关注transientView的情况,直接从下面的mRecycler.getScrapView()开始看,这个方法请参见文章安卓开发学习之ListView缓存策略中常见的方法

然后我们一路返回,返回到AbsListView#obtainView(),继续往下读,如果我们从adapter里获取的view和从回收站里获取的view不是一码事,那就把从adapter里获取的view添加进回收站,调用的是mRecycler.addScrapView(),代码参见文章安卓开发学习之ListView缓存策略中常见的方法

好,然后再返回到AbsListView中,这个方法里最后一个比较重要的方法就是setItemViewLayoutParams(),这是用来设置view的布局参数信息,这个LayoutParams是AbsListView自己的LayoutParams,继承了View.LayoutParams,里面添加了view在回收站里的位置等信息,这个方法代码如下

private void setItemViewLayoutParams(View child, int position) {
        final ViewGroup.LayoutParams vlp = child.getLayoutParams();
        LayoutParams lp;
        if (vlp == null) {
            lp = (LayoutParams) generateDefaultLayoutParams();
        } else if (!checkLayoutParams(vlp)) {
            lp = (LayoutParams) generateLayoutParams(vlp);
        } else {
            lp = (LayoutParams) vlp;
        }

        if (mAdapterHasStableIds) { // mAdapterHasStableIds变量,如果adapter继承BaseAdapter的话,默认为false
            lp.itemId = mAdapter.getItemId(position);
        }
        lp.viewType = mAdapter.getItemViewType(position);
        lp.isEnabled = mAdapter.isEnabled(position); // BaseAdapter默认是true
        if (lp != vlp) {
          child.setLayoutParams(lp);
        }
}

如果子view的LayoutParams是空,AbsListView调用generateDefaultLayoutParams()方法生成一个宽度match_parent,高度wrap_content,view_type是0的AbsListView.LayoutParams对象

如果子view的LayoutParams不是AbsListView.LayoutParams类型的,就调用generateLayoutParams()方法根据子view的布局参数信息new一个AbsListView.LayoutParams对象

然后进行viewType和isEnabled的赋值后,必要的话,重新设置子view的布局参数

完事后,AbsListView.obtainView()方法就差不多了。


ListView#measureScrapChild

返回到ListView.onMeasure()后,执行measureScrapChild()进行子view的测量,代码如下

private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
        LayoutParams p = (LayoutParams) child.getLayoutParams();
        if (p == null) { // 如果子view没有layoutParam,就构造一个宽高都是内容包裹的AbsListvView.LayoutParams给它
            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
            child.setLayoutParams(p);
        }
        p.viewType = mAdapter.getItemViewType(position);
        p.isEnabled = mAdapter.isEnabled(position);
        p.forceAdd = true;

        final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else { 
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);

        // Since this view was measured directly aginst the parent measure
        // spec, we must measure it again before reuse.
        child.forceLayout(); // 设置标志位
}

先是调用了ViewGroup.getChildMeasureSpec()方法以获取子view的宽度spec,这个方法的源码阅读参见文章安卓开发学习之View测量的内置常用方法

而后根据子view的layoutParams里的height是否大于0,分别调用Measure.makeMeasureSpec/makeSafeMeasureSpec()方法,传入参数分别是子view的参数高度、精确模式和listView的父view指定的高度、unspecified模式,关于这两个方法,请参见文章安卓开发学习之MeasureSpec

最后调用child.measure()方法进行测量,这个方法请参见文章Android开发学习之View测量绘制流程源码阅读记录

然后设置个标志位,就返回了ListView.onMeasure()。


ListView#measureHeightOfChildren

之后进入一个比较容易出问题的一步,根据heightMode还决定ListView的高度是否只有一个子view的高度,这个如果父view是scrollView的话,heightMode会是unspecified,所以由ScrollView包裹的ListView,只会显示第一项,而网上的解决方案是自定义view继承ListView,覆写onMeasure(),把heightMode设为at_most,原因就在这儿,强制调用measureHeightOfChildren()方法,获取正确的高度,这个方法源代码如下

final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            int maxHeight, int disallowPartialChildPosition) {
        // widthMeasureSpec, 0, NO_POSITION, heightSize, -1
        final ListAdapter adapter = mAdapter;
        if (adapter == null) { // 适配器为空,就不存在子view了
            return mListPadding.top + mListPadding.bottom;
        }

        // Include the padding of the list
        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; // 一般情况就是adapter.count() - 1
        final AbsListView.RecycleBin recycleBin = mRecycler;
        final boolean recyle = recycleOnMeasure(); // true
        final boolean[] isScrap = mIsScrap;

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap); // 这里就是获取每一个子view了

            measureScrapChild(child, i, widthMeasureSpec, maxHeight);

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

            // Recycle the view before we possibly return from the method
            if (recyle && recycleBin.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                recycleBin.addScrapView(child, -1); // 这里每一个子view的回收站位置信息都是-1,所以这种情况下,AbsListView.obtainView()中从回收站获取的view,都是最近被回收的view
            }

            returnedHeight += child.getMeasuredHeight(); // 累积高度

            if (returnedHeight >= maxHeight) { // 如果子view的高度和已经不小于父view指定的高度了,返回的是父view指定的高度,而不是子view高度之和
                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
                            && (returnedHeight != maxHeight) // i'th child did not fit completely
                        ? prevHeightWithoutPartialChild
                        : maxHeight; // 这里会返回maxHeight,而不是子view高度之和
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { // 这里进不来
                prevHeightWithoutPartialChild = returnedHeight;
            }
        }

        // At this point, we went through the range of children, and they each
        // completely fit, so return the returnedHeight
        return returnedHeight; // 子view高度之和小于父view指定的高度,方可返回子view高度之和
}

注释写的很清晰,我就不过多解释了


结语

关于ListView的测量流程就看到这儿,关于它的布局流程,比较复杂,参见这篇文章安卓开发学习之ListView的布局流程源码阅读

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页