安卓开发学习之ListView布局流程源码阅读

背景

安卓开发学习之ListView的测量流程源码阅读一文中,我记录了ListView的onMeasure()过程,今天,我继续记录下ListView的onLayout()流程


AbsListView#onLayout

ListView并没有实现onLayout(),所以它调用的是父类AbsListView的onLayout()方法,代码如下

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

        mInLayout = true;

        final int childCount = getChildCount(); // childCount是ListView内的子view数量,第一次加载前,值为0
        if (changed) { // 布局发生变化的话,将listView里每个子view设置为force_layout
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty(); // 回收站里的子view也不例外
        }

        layoutChildren(); // 对子view进行布局

        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

        // TODO: Move somewhere sane. This doesn't belong in onLayout().
        if (mFastScroll != null) { // 处理回调
            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
        }
        mInLayout = false;
}

可以看到,主要是调用了layoutChildren()方法


ListView#layoutChildren

layoutChildren()方法在ListView中有实现,代码如下

   @Override
    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests; // 同步layout请求
        if (blockLayoutRequests) {
            return;
        }

        mBlockLayoutRequests = true;

        try {
            super.layoutChildren(); // 空方法

            invalidate(); // 清空listView

            if (mAdapter == null) { // 不考虑adapter为空的时候
                resetList();
                invokeOnItemScrollListener();
                return;
            }

            final int childrenTop = mListPadding.top; // listView里子view的顶端极限
            final int childrenBottom = mBottom - mTop - mListPadding.bottom; // lisrView里子view的底端极限
            final int childCount = getChildCount(); // listView中所有子view数目,第一次布局前,还没有addView,childCount = 0

            int index = 0;
            int delta = 0;

            View sel;
            View oldSel = null;
            View oldFirst = null;
            View newSel = null;

            // Remember stuff we will need down below
            switch (mLayoutMode) { // 根据layout模式执行不同逻辑,一般是default
            .. // 其他情况
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition; // 上一次选中的子view位置 = 上一次选中的子view位置 - listView中第一个完整显示的子view位置
                if (index >= 0 && index < childCount) {
                    oldSel = getChildAt(index); // 上一次选中的子view
                }

                // Remember the previous first child
                oldFirst = getChildAt(0); // 上一次listView里第一个完整显示的子view

                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition; // 这一次和上一次选中的子view位置之差
                }

                // Caution: newSel might be null
                newSel = getChildAt(index + delta); // 这一次选中的子view
            }

            // 第一次布局时,由于childCount = 0,listView里没有子view,所以oldSel和newSel都是null


            boolean dataChanged = mDataChanged; // 第一次布局时,dataChanged为false
            if (dataChanged) {
                handleDataChanged(); // adapter里的数据发生变化后,调用这个方法,跟布局有关的,是改变了layout_mode,正序显示的话就是force_top,逆序显示则为force_bottom
            }

            // Handle the empty set by removing all views that are visible
            // and calling it a day
            if (mItemCount == 0) { // mItemCount是adapter.getItemCount()返回的,不应该是0
                resetList();
                invokeOnItemScrollListener();
                return;
            } else if (mItemCount != mAdapter.getCount()) { // 这种情况也不应该出现
                .. // 抛异常
            }

            setSelectedPositionInt(mNextSelectedPosition);
            /* 这是ListView爷爷类AdapterView的方法,代码如下
            void setSelectedPositionInt(int position) {
                mSelectedPosition = position; // 更新mSelectedPosition
                mSelectedRowId = getItemIdAtPosition(position); // 判空后,调用adapter.getItemId()方法
             }
             */

            .. // 无障碍处理
           
            .. // 获取焦点并处理

            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition; // listView中第一个完整显示的view
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) { // 数据发生改变
                for (int i = 0; i < childCount; i++) { // 把listView中的子view缓存到回收站中
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else { // 数据没改变,第一次布局的话,childCount为0,这个方法成了摆设,之后的布局它才有用,就是把listView里的子view缓存到了mActiveViews里
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // Clear out old views
            detachAllViewsFromParent(); // 清空所有子view,全部分离掉
            recycleBin.removeSkippedScrap(); // itemTypeCount为1的话,这个方法就是摆设

            switch (mLayoutMode) {
            .. // 其他情况
            case LAYOUT_FORCE_BOTTOM: // 数据源发生变化,逆序显示时
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP: // 数据源发生变化,正序显示时
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown(); // 调整子view的位置
                break;
            .. // 其他情况
            default: // 数据源没有变化,正常是这种情况
                if (childCount == 0) { // 第一次加载的话,childCount = 0
                    if (!mStackFromBottom) { // 是否按着adapter的数据顺序显示,mStackFromBottom为false表示顺序显示,为true表示逆序显示,一般就是顺序
                        final int position = lookForSelectablePosition(0, true); // 正常来说,应该是返回0
                        setSelectedPositionInt(position);
                        /*
                        爷爷类AdapterView里的方法,代码如下
                        void setSelectedPositionInt(int position) {
                            mSelectedPosition = position; // 设置mSelectedPosition
                            mSelectedRowId = getItemIdAtPosition(position); // 设置mSelectedRowId,BaseAdapter一般也就返回的是position
                         }
                         */
                        sel = fillFromTop(childrenTop); // 从childrenTop开始加载子view
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else { // 不是第一次加载,而且数据源没有改变,就进入下面的逻辑,优先将指定位置的view加载,再加载其他的view
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { // mSelectedPosition,没有选中的话就是-1
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) { // 默认是这个分支
                        sel = fillSpecific(mFirstPosition, // oldFirst默认是listView的第一个子view
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }

            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews(); // 清除所有没用到的缓存view,把它们移到回收站中

            // remove any header/footer that has been temp detached and not re-attached
            // 清除没有用到的header或footer
            removeUnusedFixedViews(mHeaderViewInfos);
            removeUnusedFixedViews(mFooterViewInfos);

            // 处理焦点
            .. 

            // 无障碍处理
            ..

            .. // 焦点处理
            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
            .. // 滚动条处理
            mNeedSync = false;
            setNextSelectedPositionInt(mSelectedPosition);

            .. // 滚动条和选中处理
        } finally {
            if (mFocusSelector != null) {
                mFocusSelector.onLayoutComplete();
            }
            if (!blockLayoutRequests) {
                mBlockLayoutRequests = false;
            }
        }
    }

代码很长很复杂,我这里只记录了跟layout相关的步骤,显然,listView的layoutChildren()要分为第一次布局和非第一次布局来分析,那我就分别记录一下,这里我只考虑正序显示


第一次布局

listView的第一次布局,在layoutChildren()里调用的主要方法依次是lookForSelectablePosition()和fillFromTop(),按顺序点进去源码看看

ListView#lookForSelectablePosition

代码如下

    @Override
    int lookForSelectablePosition(int position, boolean lookDown) {
        final ListAdapter adapter = mAdapter;
        .. // 合法性判断

        final int count = adapter.getCount();
        if (!mAreAllItemsSelectable) {
            if (lookDown) { // 进入这里
                position = Math.max(0, position);
                while (position < count && !adapter.isEnabled(position)) {
                    position++; // 找到第一个在adapter里返回enabled的子view,默认情况就是第一个
                }
            } else {
                position = Math.min(position, count - 1);
                while (position >= 0 && !adapter.isEnabled(position)) {
                    position--;
                }
            }
        }

        if (position < 0 || position >= count) {
            return INVALID_POSITION;
        }

        return position;
     }

代码不复杂,只是获取了第一个enable的子view的位置


ListView#fillFromTop

代码如下

private View fillFromTop(int nextTop) {
        // 更新mFirstPosition,传入的nextTop是childrenTop,也就是listView里子view的顶端极限
        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
        if (mFirstPosition < 0) {
            mFirstPosition = 0;
        } // 第一次布局的话,mFirstPosition为0
        return fillDown(mFirstPosition, nextTop);
}

调用了fillDown()方法,代码如下

private View fillDown(int pos, int nextTop) {
        // 给ListView遍历添加指定位置之下的view
        View selectedView = null;

        int end = (mBottom - mTop);
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            end -= mListPadding.bottom;
        } // 确定listView里子view的最底端

        while (nextTop < end && pos < mItemCount) { // 不到底并且不遍历完,所以ListView一次显示后的getChildCount()是可视范围内的子view数目,而不是全部item的数量
            // is this the selected item? 似乎第一次加载,默认adapter里第一个view是选中的,也就是方才说的mSelectedPosition = 0
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

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

        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
        return selectedView;
}

循环调用了makeAndAddView()方法,代码如下

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) { // flowDown()过来的话,flow是true;flowup()过来的话,flow是false
        if (!mDataChanged) { // 第一次加载的话,flow是true,mDataChanged为false
            // Try to use an existing view for this position.
            final View activeView = mRecycler.getActiveView(position); // 第一次加载这里是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.
        // 第一次加载要进行obtainView()
        final View child = obtainView(position, mIsScrap);

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

        return child;
}

关于mRecycler.getActiveView()方法,请参见文章安卓开发学习之ListView缓存策略中常见的方法。第一次加载的话,从回收站里获取的view是空,所以要多调用一个obtainView()方法,这个方法的记录,请参加文章安卓开发学习之ListView的测量流程源码阅读,然后调用了setupChild()方法,代码如下

    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean isAttachedToWindow) {
        // 第一次加载,flowDown是true
        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();
        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
                || child.isLayoutRequested(); // 经过onMeasure()的measureScrapChild()后,child.isLayoutRequested()为true

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

        // Set up view state before attaching the view, since we may need to
        // rely on the jumpDrawablesToCurrentState() call that occurs as part
        // of view attachment.
        // 更新child的selected和pressed状态
        if (updateChildSelected) {
            child.setSelected(isSelected);
        }

        if (updateChildPressed) {
            child.setPressed(isPressed);
        }

        // 设置checked或activated
        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
            if (child instanceof Checkable) {
                ((Checkable) child).setChecked(mCheckStates.get(position));
            } else if (getContext().getApplicationInfo().targetSdkVersion
                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {
                child.setActivated(mCheckStates.get(position));
            }
        }

        // 第一次layout:
        // 经过onMeasure()的measureScrapChild()后,p.forceAdd = true,所以如果子view不是header或footer的话,走的是else分支
        // isAttachedToWindow是mIsScrap[0],这个经过obtainView()后只有从回收站里获取到老的view时,才会是true
        if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
            // 把子view绑定给listView,如果是获取child的下一个view,则是从flowDown()过来的,flowDown为true
            // 如果是获取child的上一个view,则是从flowUp()过来的,flowDown为false

            // If the view was previously attached for a different position,
            // then manually jump the drawables.
            if (isAttachedToWindow
                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                            != position) {
                // 如果子view的位置和之前回收站里的位置不一样,手动跳到当前状态
                child.jumpDrawablesToCurrentState();
            }
        } else { // 第一次layout走这里
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            // 这里才是真正的给ListView添加子view,最终会调用到ViewGroup.addInArray(child, index)方法,把child放在mChildren的最后
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
            // add view in layout will reset the RTL properties. We have to re-resolve them
            child.resolveRtlPropertiesIfNeeded(); // 重新解析子view的展示方向,从左往右还是相反,此处省略
        }

        if (needToMeasure) {
            // 子view会再测量一次
            final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                    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(getMeasuredHeight(),
                        MeasureSpec.UNSPECIFIED);
            }
            child.measure(childWidthSpec, childHeightSpec);
        } else {
            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;
            child.layout(childrenLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childrenLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }

        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
            child.setDrawingCacheEnabled(true);
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

最关键的就是在else里,调用了addViewInLayout()方法,把child加到listView中,此后listView的子view数量,不会是0了。

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

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

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


至此,ListView的第一次布局就完成了


非第一次布局

如果不是第一次布局(不考虑数据源变化),那么就是调用了recycleBin.fillActiveViews()方法和fillSpecific()方法

前者用来缓存当前ListView中的子view,代码参见文章安卓开发学习之ListView缓存策略中常见的方法

后者优先加载listView中指定位置(第一个位置)的子view,然后加载这个子view之前和之后的view(如果有的话),代码如下

    private View fillSpecific(int position, int top) {
        // 传入参数:mFirstPosition和listView里第一项的顶部
        boolean tempIsSelected = position == mSelectedPosition;
        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); // 先加载指定位置的view
        // 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); // 为ListView添加指定位置之上的view,并获取焦点view
            // This will correct for the top of the first view not touching the top of the list
            adjustViewsUpOrDown(); // 调整所有子view的高度,保证让最上面或最下面(正常顺序是最上面)子view不悬空
            below = fillDown(position + 1, temp.getBottom() + dividerHeight); // 为ListView添加指定位置之下的view,并获取焦点view
            int childCount = getChildCount();
            if (childCount > 0) {
                // 如果listView已经显示完了,处理一下底部的空当
                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);
            }
        }

        // 返回焦点view
        if (tempIsSelected) {
            return temp;
        } else if (above != null) {
            return above;
        } else {
            return below;
        }
    }

按照顺序,调用了makeAndAddView()、fillUp()、adjustViewsUpOrDown()、fillDown()、correctTooHight()方法,我们依次来看


makeAndAddView

之前看过,现在再看一次

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) { // flowDown()过来的话,flow是true;flowup()过来的话,flow是false
        if (!mDataChanged) {
            // Try to use an existing view for this position.
            final View activeView = mRecycler.getActiveView(position); // 不是第一次加载,这里不是null
            if (activeView != null) {
                // 走这里
                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);

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

        return child;
    }

少走一步obtainView(),看来虽然之前detachAllViewsFromParents()把所有子view都清除掉了,但也只是从回收站里重新获取,所以省了很多事。我们再进入setupChild()看看

    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean isAttachedToWindow) { // 插入指定位置,flowDown为true
        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();
        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
                || child.isLayoutRequested(); // 经过onMeasure()的measureScrapChild()后,child.isLayoutRequested()为true

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

        // Set up view state before attaching the view, since we may need to
        // rely on the jumpDrawablesToCurrentState() call that occurs as part
        // of view attachment.
        // 更新child的selected和pressed状态
        if (updateChildSelected) {
            child.setSelected(isSelected);
        }

        if (updateChildPressed) {
            child.setPressed(isPressed);
        }

        // 设置checked或activated
        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
            if (child instanceof Checkable) {
                ((Checkable) child).setChecked(mCheckStates.get(position));
            } else if (getContext().getApplicationInfo().targetSdkVersion
                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {
                child.setActivated(mCheckStates.get(position));
            }
        }

        // 之后的layout:
        // 第一次以后的layout,isAttachToWindow直接成了true,p.forceAdd也在第一测量后变成了false,所以走的就是if分支
        if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p); // 如果是child本身,flowDown为true
            // 把子view绑定给listView,如果是获取child的下一个view,则是从flowDown()过来的,flowDown为true
            // 如果是获取child的上一个view,则是从flowUp()过来的,flowDown为false

            // If the view was previously attached for a different position,
            // then manually jump the drawables.
            if (isAttachedToWindow
                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                            != position) {
                // 如果子view的位置和之前回收站里的位置不一样,手动跳到当前状态,忽略
                child.jumpDrawablesToCurrentState();
            }
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            // 这里才是真正的给ListView添加子view,最终会调用到ViewGroup.addInArray(child, index)方法,把child放在mChildren的最后
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
            // add view in layout will reset the RTL properties. We have to re-resolve them
            child.resolveRtlPropertiesIfNeeded();
        }

        if (needToMeasure) {
            // 子view会再测量一次
            final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                    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(getMeasuredHeight(),
                        MeasureSpec.UNSPECIFIED);
            }
            child.measure(childWidthSpec, childHeightSpec);
        } else {
            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;
            child.layout(childrenLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childrenLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }

        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
            child.setDrawingCacheEnabled(true);
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

最关键的是调用了attachViewToParent()方法,因为在layoutChildren()的时侯,把所有子view都detach了,现在就是要attach一下。它的源代码如下

    protected void attachViewToParent(View child, int index, LayoutParams params) {
        child.mLayoutParams = params;

        if (index < 0) {
            index = mChildrenCount;
        }

        addInArray(child, index);
        // 添加子view,flowDown的话,index为mChildrenCount;flowUp的话,index为0
        // 也就是添加到mChildren尾部和头部的区别

        child.mParent = this;
        child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
                        & ~PFLAG_DRAWING_CACHE_VALID)
                | PFLAG_DRAWN | PFLAG_INVALIDATED;
        this.mPrivateFlags |= PFLAG_INVALIDATED;

        if (child.hasFocus()) {
            requestChildFocus(child, child.findFocus());
        }
        // 处理回调
        dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
                && isShown());
    }

返回setupChild()后,就跟第一次布局没什么区别了,一路返回到fillSpecific()方法,接着看fillUp()


fillUp

    private View fillUp(int pos, int nextBottom) {
        // 给ListView遍历添加指定位置及之上的view
        View selectedView = null;

        int end = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            end = mListPadding.top;
        }

        while (nextBottom > end && pos >= 0) {
            // is this the selected item?
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
            nextBottom = child.getTop() - mDividerHeight;
            if (selected) {
                selectedView = child;
            }
            pos--;
        }

        mFirstPosition = pos + 1; // 更新ListView中第一个子view的位置,是第一个能完整显示的子view
        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
        return selectedView;
    }

没啥好说的,就是遍历执行makeAndAddView()方法,只不过这里的flowDown为false,也就是在attachViewToParent里调用addInArray()时,采用头插法


adjustViewsUpOrDown

从fillUp()返回后,调用了adjustViewsUpOrDown()来调整所有子view的高度,代码如下

    private void adjustViewsUpOrDown() {
        final int childCount = getChildCount();
        int delta;

        if (childCount > 0) { // 此时绝不应该是0
            View child;

            if (!mStackFromBottom) { // 正常顺序走这里
                // Uh-oh -- we came up short. Slide all views up to make them
                // align with the top
                child = getChildAt(0);
                delta = child.getTop() - mListPadding.top; // listView中第一个子view到顶端的距离
                if (mFirstPosition != 0) { // 如果listView中第一个子view不是adapter中的第一项
                    // It's OK to have some space above the first item if it is
                    // part of the vertical spacing
                    delta -= mDividerHeight; // 就再减去分割线的距离
                }
                if (delta < 0) {
                    // We only are looking to see if we are too low, not too high
                    delta = 0;
                }
            } else {
                // we are too high, slide all views down to align with bottom
                child = getChildAt(childCount - 1);
                delta = child.getBottom() - (getHeight() - mListPadding.bottom);

                if (mFirstPosition + childCount < mItemCount) {
                    // It's OK to have some space below the last item if it is
                    // part of the vertical spacing
                    delta += mDividerHeight;
                }

                if (delta > 0) {
                    delta = 0;
                }
            }

            if (delta != 0) {
                // 调整listView中所有子view的顶端和底端
                offsetChildrenTopAndBottom(-delta);
            }
        }
    }

最后的offsetChildrenTopAndBottom()方法是ViewGroup里的方法,代码如下

    public void offsetChildrenTopAndBottom(int offset) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        boolean invalidate = false;

        for (int i = 0; i < count; i++) {
            final View v = children[i];
            v.mTop += offset;
            v.mBottom += offset;
            // 硬件加速,忽略
            if (v.mRenderNode != null) {
                invalidate = true;
                v.mRenderNode.offsetTopAndBottom(offset);
            }
        }

        // 硬件加速,忽略
        if (invalidate) {
            invalidateViewProperty(false, false);
        }
        // 通知回调
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }

原来就是更新了子view的top和bottom


fillDown

调整完后,调用了fillDown()方法,加载指定位置之下的view,代码如下

    private View fillDown(int pos, int nextTop) {
        // 给ListView遍历添加指定位置之下的view
        View selectedView = null;

        int end = (mBottom - mTop);
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            end -= mListPadding.bottom;
        } // 确定可视范围内子view的最底端

        while (nextTop < end && pos < mItemCount) { // 不到底并且不遍历完,所以ListView一次显示后的getChildCount()是可视范围内的子view数目,而不是全部item的数量
            // is this the selected item? 似乎第一次加载,默认adapter里第一个view是选中的,也就是方才说的mSelectedPosition = 0
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

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

        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
        return selectedView;
    }

也没啥,只不过把flowDown设成true传给了makeAndAddView(),最后在addInArray()中使用了尾插法


correctTooHigh

从flowDown()方法返回后,如果childCount > 0,也就是子view数目不为0,就调用correctTooHigh()方法,处理底部空当,代码如下

    private void correctTooHigh(int childCount) {
        // First see if the last item is visible. If it is not, it is OK for the
        // top of the list to be pushed up.
        int lastPosition = mFirstPosition + childCount - 1; // listView中最后一个view的位置
        if (lastPosition == mItemCount - 1 && childCount > 0) { // 如果listView已经显示完了

            // Get the last child ...
            final View lastChild = getChildAt(childCount - 1);

            // ... and its bottom edge
            final int lastBottom = lastChild.getBottom(); // 最后一个子view的底部

            // This is bottom of our drawable area
            final int end = (mBottom - mTop) - mListPadding.bottom; // 子view的底部极限

            // This is how far the bottom edge of the last view is from the bottom of the
            // drawable area
            int bottomOffset = end - lastBottom; // 底部偏移量
            View firstChild = getChildAt(0); // listView中第一个子view
            final int firstTop = firstChild.getTop(); // 第一个子view的顶部

            // Make sure we are 1) Too high, and 2) Either there are more rows above the
            // first row or the first row is scrolled off the top of the drawable area
            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
                // 底部太高,并且listView中第一个子view不是adapter的第一项,即便是第一项,也太低
                if (mFirstPosition == 0) {
                    // Don't pull the top too far down
                    // listView中第一个子view是adapter的第一项的话(此时adapter的item太少,不用滑就能显示全部),偏移量bottomOffset
                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
                    // paddingTop - firstTop < 0 表示listView里第一个子view太靠下了,上面出现空当,此时bottomOffset为负
                }
                // Move everything down
                offsetChildrenTopAndBottom(bottomOffset); // 整体位置向下偏移bottomOffset(不一定真的往下偏移)
                if (mFirstPosition > 0) {
                    // Fill the gap that was opened above mFirstPosition with more rows, if
                    // possible
                    // 如果listView中第一个子view不是adapter的第一项,也就是发生了滑动,就把上面的空余部分补齐
                    fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight);
                    // Close up the remaining gap
                    adjustViewsUpOrDown(); // 如果是正序显示,并且把上面空余部分补齐后,最上方又出现空当,就补齐上面的空当。如果是逆序显示,就补齐下面的空当
                }

            }
        }
    }

注释已经写得很清楚了,这样fillSpecific()就执行完了,非第一次布局也就结束了。

至于layoutChildren()剩下的一些和缓存有关的方法(比如recycleBin.removeSkippedScrap()、recycleBin.scrapActiveViews()等等),参见文章安卓开发学习之ListView缓存策略中常见的方法


结语

ListView的布局流程大体就这样,很复杂,值得反复品味。最后关于listView的绘制流程,请参见文章安卓开发学习之ListView的绘制流程源码阅读

猜你喜欢

转载自blog.csdn.net/qq_37475168/article/details/80350067