ListView原理解析

  • setAdapter

    ListView是根据数据来生成UI的,而数据是交给adapter来管理的,我们从setAdapter方法入手。

    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
    
        resetList();
        mRecycler.clear();
          
          //封装HeaderView和FooterView到Adapter中
        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 = mAdapter.getCount();
            checkFocus();
    
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
    
            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
    
            int position;
            if (mStackFromBottom) {
                position = lookForSelectablePosition(mItemCount - 1, false);
            } else {
                position = lookForSelectablePosition(0, true);
            }
            setSelectedPositionInt(position);
            setNextSelectedPositionInt(position);
    
            if (mItemCount == 0) {
                // Nothing selected
                checkSelectionChanged();
            }
        } else {
            mAreAllItemsSelectable = true;
            checkFocus();
            // Nothing selected
            checkSelectionChanged();
        }
    
        requestLayout();
    }
    

    首先,resetList方法逻辑走完会把所有的已添加的View(包括header和footer)都移除:

    @Override
    void resetList() {
        // The parent's resetList() will remove all views from the layout so we need to
        // cleanup the state of our footers and headers
        clearRecycledState(mHeaderViewInfos);
        clearRecycledState(mFooterViewInfos);
    
        super.resetList();
    
        mLayoutMode = LAYOUT_NORMAL;
    }
    

    super.resetList()也就是AbsListView的resetList方法:

    /**
     * The list is empty. Clear everything out.
     */
    void resetList() {
        removeAllViewsInLayout();
        mFirstPosition = 0;
        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();
    }
    

    这里会调用一次invalidate方法同步清空UI。

    最后会调用requestLayout方法,按照requestLayout方法的逻辑,我们先来看onMeasure方法。

  • 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();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);
    
            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);
    
            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());
    
            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 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;
    }
    

    在这里测量ListView的宽高,可以看出这么几个信息,宽是通过第一个View的宽来定的(然后加上竖直滚动条的宽度),这就说明ListView一定是等宽的,而且可以看出ListView都是按照竖直方向的List设计的;高度是通过measureHeightOfChildren方法测量的,注意如果是MeasureSpec.UNSPECIFIED模式则以第一个View的高度加上上下内边距和上下渐变边界来算作高度,measureHeightOfChildren是在“合法的”MeasureSpec.AT_MOST模式时调用,也就是wrap_content时,可以理解,除此之外,EXACTLY模式(精确尺寸或match_parent)的话直接使用heightSize即

    可。

  • measureHeightOfChildren

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    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;
        }
    
        // 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;
        final AbsListView.RecycleBin recycleBin = mRecycler;
        final boolean recyle = recycleOnMeasure();
        final boolean[] isScrap = mIsScrap;
    
        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);
    
            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);
            }
    
            returnedHeight += child.getMeasuredHeight();
    
            if (returnedHeight >= maxHeight) {
                // 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;
            }
    
            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;
    }
    

    上面传进来的endPosition是NO_POSITION,所以这里取adapter.getCount()-1作为endPosition,然后通过obtainView方法循环获取View,每一项通过returnedHeight += child.getMeasuredHeight()把所有的View的高度加起来(这也说明item的高度是可以不同的),此外除了第一项之外,每一项还要加上divider的高度,最后的处理就是判断是否超出ListView设置的最大高度,如果超出了则可以根据disallowPartialChildPosition判断如果在此项之前有“可以未全部显示的View”的话符合条件的话则返回此项之前已得的总高度,否则返回maxHeight,这里其实disallowPartialChildPosition在onMeasure中是硬编码的“-1”,所以若超出最大高度只会返回最大高度,应该是设计者预留的功能。

  • obtainView

    obtainView方法是AbsListView中的方法:

    View obtainView(int position, boolean[] outMetadata) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
    
        outMetadata[0] = false;
    
        // 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);
        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)) {
                final View updatedView = mAdapter.getView(position, transientView, this);
    
                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }
    
            outMetadata[0] = true;
    
            // Finish the temporary detach started in addScrapView().
            transientView.dispatchFinishTemporaryDetach();
            return transientView;
        }
    
        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else if (child.isTemporarilyDetached()) {
                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);
        }
    
          //L1
        setItemViewLayoutParams(child, position);
    
        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;
    }
    

    前面的步骤是关于View复用的,我们先看普通情况,child是通过mAdapter.getView方法获取的,这就是我们自定义Adapter的时候需要重写这个方法来构造Item View的原因,L1处的setItemViewLayoutParams方法为:

    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) {
            lp.itemId = mAdapter.getItemId(position);
        }
        lp.viewType = mAdapter.getItemViewType(position);
        lp.isEnabled = mAdapter.isEnabled(position);
        if (lp != vlp) {
          child.setLayoutParams(lp);
        }
    }
    

    可以看到mAdapter.getItemId、mAdapter.getItemViewType的获取,generateLayoutParams方法会包装成AbsListView.LayoutParams对象,这个类继承自ViewGroup.LayoutParams,不过多了几个像viewType这样的属性,那么现在我们知道child的LayoutParams持有了mAdapter的相关方法返回的值。

    mRecycler是RecycleBin(回收站),是AbsListView的一个内部类,前面会先调用mRecycler.getTransientStateView(position)来尝试取可复用的View,如果和mAdapter当前位置的viewtype相同则表示可以复用,但是依然会通过mAdapter.getView拿到View判断是否和之前的是同一个View,如果不是则调用mRecycler.addScrapView保存。因为现在是初始化时期,所以复用这里有些不容易理解,因为此时还不知道mRecycler里面的数据是怎么来的,我们只知道列表的第一个元素是在onMeasure中保存的:

    if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
            ((LayoutParams) child.getLayoutParams()).viewType)) {
        mRecycler.addScrapView(child, 0);
    }
    

    mRecycler.shouldRecycleViewType方法判断只要viewType的值不小于0即可,默认为0,所以这里可以知道,如果要设置多布局ListView,则Adapter中返回的ItemViewType不能小于0,否则影响复用。addScrapView方法如下:

    void addScrapView(View scrap, int position) {
        final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
        if (lp == null) {
            // Can't recycle, but we don't know anything about the view.
            // Ignore it completely.
            return;
        }
    
        lp.scrappedFromPosition = position;
    
        // Remove but don't scrap header or footer views, or views that
        // should otherwise not be recycled.
        final int viewType = lp.viewType;
        if (!shouldRecycleViewType(viewType)) {
            // Can't recycle. If it's not a header or footer, which have
            // special handling and should be ignored, then skip the scrap
            // heap and we'll fully detach the view later.
            if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                getSkippedScrap().add(scrap);
            }
            return;
        }
    
        scrap.dispatchStartTemporaryDetach();
    
        // The the accessibility state of the view may change while temporary
        // detached and we do not allow detached views to fire accessibility
        // events. So we are announcing that the subtree changed giving a chance
        // to clients holding on to a view in this subtree to refresh it.
        notifyViewAccessibilityStateChangedIfNeeded(
                AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
    
        // Don't scrap views that have transient state.
        final boolean scrapHasTransientState = scrap.hasTransientState();
        if (scrapHasTransientState) {
            if (mAdapter != null && mAdapterHasStableIds) {
                // If the adapter has stable IDs, we can reuse the view for
                // the same data.
                if (mTransientStateViewsById == null) {
                    mTransientStateViewsById = new LongSparseArray<>();
                }
                mTransientStateViewsById.put(lp.itemId, scrap);
            } else if (!mDataChanged) {
                // If the data hasn't changed, we can reuse the views at
                // their old positions.
                if (mTransientStateViews == null) {
                    mTransientStateViews = new SparseArray<>();
                }
                mTransientStateViews.put(position, scrap);
            } else {
                // Otherwise, we'll have to remove the view and start over.
                clearScrapForRebind(scrap);
                getSkippedScrap().add(scrap);
            }
        } else {
            clearScrapForRebind(scrap);
            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                mScrapViews[viewType].add(scrap);
            }
    
            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
        }
    }
    

    首先,viewtype小于0的且不是头或脚view的放到mSkippedScrap中,复用的时候会跳过这些View。scrap.hasTransientState()查询是否有临时状态(这个需要View调用setTransientState方法设置,默认不会有),如果有的话根据需要放到mTransientStateViewsById或mTransientStateViews中(mAdapterHasStableIds = mAdapter.hasStableIds();),否则根据viewType的数量放到不同的集合里,如果只有一种type则放到mCurrentScrap(它是一个ArrayList),不止一种就放到mScrapViews[viewType]中(它是一个ArrayList的数组),以viewType为key,即相当于每一种viewType都有一个mCurrentScrap集合来存放。

  • onLayout

    onLayout方法呢,在ListView中没有重写,直接找AbsListView的onLayout方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
    
        mInLayout = true;
    
        final int childCount = getChildCount();
        if (changed) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }
    
        layoutChildren();
    
        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;
    }
    

    可以看到,getChildAt(i).forceLayout()会修改当前所有子View的重新布局标志位,mRecycler.markChildrenDirty()会调用待复用的子view的forceLayout修改需要重新布局的标识位:

    public void markChildrenDirty() {
        if (mViewTypeCount == 1) {
            final ArrayList<View> scrap = mCurrentScrap;
            final int scrapCount = scrap.size();
            for (int i = 0; i < scrapCount; i++) {
                scrap.get(i).forceLayout();
            }
        } else {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                final ArrayList<View> scrap = mScrapViews[i];
                final int scrapCount = scrap.size();
                for (int j = 0; j < scrapCount; j++) {
                    scrap.get(j).forceLayout();
                }
            }
        }
        if (mTransientStateViews != null) {
            final int count = mTransientStateViews.size();
            for (int i = 0; i < count; i++) {
                mTransientStateViews.valueAt(i).forceLayout();
            }
        }
        if (mTransientStateViewsById != null) {
            final int count = mTransientStateViewsById.size();
            for (int i = 0; i < count; i++) {
                mTransientStateViewsById.valueAt(i).forceLayout();
            }
        }
    }
    

    看一下layoutChildren方法,这个方法是又子类重写的,去ListView找,因为这个方法里处理了包括初始化、数据更新、数据移除、数据添加等很多情况的layout逻辑,所以代码很长,我们取和setAdapter逻辑路线相关的第二个switch中的default代码块:

    default:
        if (childCount == 0) {
            if (!mStackFromBottom) {
                final int position = lookForSelectablePosition(0, true);
                setSelectedPositionInt(position);
                sel = fillFromTop(childrenTop);
            } else {
                final int position = lookForSelectablePosition(mItemCount - 1, false);
                setSelectedPositionInt(position);
                sel = fillUp(mItemCount - 1, childrenBottom);
            }
        } else {
            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;
    

    childCount是通过getChildCount方法获取的,如果是第一次加载的时候是为0的,比如这里的setAdapter逻辑之前是把子view全都移除了,所以这里会走childCount=0的逻辑分支。

    这里的fillFromTop、fillUp、fillSpecific都是用来添加子View的,mStackFromBottom标志着是否按照从下往上的顺序添加View,所以这三个方法的作用分别是按照从上往下的顺序添加、按照从下往上的顺序添加和更新某一个确定位置的View,这里我们来看默认的初始化时的按照从上往下的顺序来添加的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);
    }
    

    可见就是找出最顶部的位置,正常是0,nextTop是前面传过来的,就是ListView的上方内边距的top,这里可以知道,ListView设置的上内边距其实最终是作为第一个View的顶部的,这也可以理解,滚动的时候是整个ListView的内部整体滚动而不是上方留了个不动的边距,这个方法调用了fillDown方法:

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    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) {
            // is this the selected item?
            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);
        return selectedView;
    }
    

    可以看到这里有一个while来循环调用makeAndAddView方法,注意while循环里有两个条件,第一个条件是表示只添加屏幕内能显示出来的(部分也算)Item,第二个条件是Adapter中getCount方法返回的数量之内,selected标志着当前View是否是选中的,最终会返回选中的View,之后在layoutChildren中执行后面和选中相关的逻辑,这里看一下makeAndAddView方法:

    @UnsupportedAppUsage
    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
            // Try to use an existing view for this position.
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                // Found it. We're reusing an existing child, so it just needs
                // to be positioned like a scrap view.
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }
    
        // Make a new view for this position, or convert an unused view if
        // possible.
        final View child = obtainView(position, mIsScrap);
    
        // This needs to be positioned and measured.
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    
        return child;
    }
    

    这里会先调用mRecycler.getActiveView方法从mActiveViews中尝试找是否已经生成过:

    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >=0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }
    

    这个方法在滚动的时候重新layout的时候才会生效,因为初始化的时候是没有任何子View的。如果没找到就会调用obtainView方法创建新的View,获取或者生成的View通过setupChild方法添加到当前ListView中:

    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();
        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();
        }
        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.
        if (updateChildSelected) {
            child.setSelected(isSelected);
        }
    
        if (updateChildPressed) {
            child.setPressed(isPressed);
        }
    
        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));
            }
        }
    
        if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
    
            // If the view was previously attached for a different position,
            // then manually jump the drawables.
            if (isAttachedToWindow
                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                            != position) {
                child.jumpDrawablesToCurrentState();
            }
        } else {
              //Code1
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            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) {
            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);
    }
    

    可以看到,selected最终用在了child.setSelected(isSelected),然后不是header或footer的话会走到Code1处,会调用addViewInLayout方法:

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

    这里又会调用addViewInner方法,在addViewInner方法里又会调用addInArray(child, index)方法:

    private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;
        final int size = children.length;
        if (index == count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, size);
                children = mChildren;
            }
            children[mChildrenCount++] = child;
        } else if (index < count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, index);
                System.arraycopy(children, index, mChildren, index + 1, count - index);
                children = mChildren;
            } else {
                System.arraycopy(children, index, children, index + 1, count - index);
            }
            children[index] = child;
            mChildrenCount++;
            if (mLastTouchDownIndex >= index) {
                mLastTouchDownIndex++;
            }
        } else {
            throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
        }
    }
    

    可以看到,在这里把创建的View添加到mChildren中,mChildrenCount也是在这里通过自增的方式变化。

    至此,我们知道在onLayout流程中创建的所有的Item。

    addViewInner走完之后,回到addViewInLayout方法之后会执行:

    child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    

    然后下面会执行到offsetLeftAndRight和offsetTopAndBottom方法,里面会执行invalidateXxx相关方法从而触发View的onDraw方法进行绘制。

    前面的getActiveView方法获取的数组是在layoutChildren中通过fillActiveViews方法填充的,他发生在fillXxx系列方法之前,正如前面说的,这个方法在滚动时候重新onLayout的时候才有用,初始化时候是没有任何子View的:

    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();
            // Don't put header or footer views into the scrap heap
            if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
                //        However, we will NOT place them into scrap views.
                activeViews[i] = child;
                // Remember the position so that setupChild() doesn't reset state.
                lp.scrappedFromPosition = firstActivePosition + i;
            }
        }
    }
    
  • 滚动

    ListView中没有重写onTouchEvent方法,我们看AbsListView中的,在这个方法中,我们只看和子View相关的部分,关于滑动手势、速度等先不考虑。

    首先,深拷贝一个当前的MotionEvent对象:

    final MotionEvent vtev = MotionEvent.obtain(ev);
    

    然后使用这个新的MotionEvent记录位移数据:

    final int actionMasked = ev.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);
    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            onTouchDown(ev);
            break;
        }
    
        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
          ... ...
    }
    

    mNestedYOffset记录的是距第一次按下时的位置的y轴位移距离。

    再看onTouchMove方法:

    private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
        if (mHasPerformedLongPress) {
            // Consume all move events following a successful long press.
            return;
        }
    
        int pointerIndex = ev.findPointerIndex(mActivePointerId);
        if (pointerIndex == -1) {
            pointerIndex = 0;
            mActivePointerId = ev.getPointerId(pointerIndex);
        }
    
        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }
    
        final int y = (int) ev.getY(pointerIndex);
    
        switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                // Check if we have moved far enough that it looks more like a
                // scroll than a tap. If so, we'll enter scrolling mode.
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                    break;
                }
                // Otherwise, check containment within list bounds. If we're
                // outside bounds, cancel any active presses.
                final View motionView = getChildAt(mMotionPosition - mFirstPosition);
                final float x = ev.getX(pointerIndex);
                if (!pointInView(x, y, mTouchSlop)) {
                    setPressed(false);
                    if (motionView != null) {
                        motionView.setPressed(false);
                    }
                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                            mPendingCheckForTap : mPendingCheckForLongPress);
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                    updateSelectorState();
                } else if (motionView != null) {
                    // Still within bounds, update the hotspot.
                    final float[] point = mTmpPoint;
                    point[0] = x;
                    point[1] = y;
                    transformPointToViewLocal(point, motionView);
                    motionView.drawableHotspotChanged(point[0], point[1]);
                }
                break;
            case TOUCH_MODE_SCROLL:
            case TOUCH_MODE_OVERSCROLL:
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }
    

    因为第一次进入这个方法是在down之后,所以此时的mTouchMode是TOUCH_MODE_DOWN(在onTouchDown中修改的),然后进入startScrollIfNeeded方法:

    private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
        // Check if we have moved far enough that it looks more like a
        // scroll than a tap
        final int deltaY = y - mMotionY;
        final int distance = Math.abs(deltaY);
        final boolean overscroll = mScrollY != 0;
        if ((overscroll || distance > mTouchSlop) &&
                (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
            createScrollingCache();
            if (overscroll) {
                mTouchMode = TOUCH_MODE_OVERSCROLL;
                mMotionCorrection = 0;
            } else {
                mTouchMode = TOUCH_MODE_SCROLL;
                mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
            }
            removeCallbacks(mPendingCheckForLongPress);
            setPressed(false);
            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
            if (motionView != null) {
                motionView.setPressed(false);
            }
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
            // Time to start stealing events! Once we've stolen them, don't let anyone
            // steal from us
            final ViewParent parent = getParent();
            if (parent != null) {
                parent.requestDisallowInterceptTouchEvent(true);
            }
            scrollIfNeeded(x, y, vtev);
            return true;
        }
    
        return false;
    }
    

    这里会判断滑动距离是否符合滚动的最小距离(根据是否超出手指按下的范围),之前在onTouchEvent中已经通过startNestedScroll(SCROLL_AXIS_VERTICAL)设置了mNestedScrollAxes,overscroll是判断是否是嵌套滑动引发的滚动,这里会把mTouchMode修改为TOUCH_MODE_OVERSCROLL或TOUCH_MODE_SCROLL,最后调用scrollIfNeeded方法,这个方法里:

    int rawDeltaY = y - mMotionY;
    final int deltaY = rawDeltaY;
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    

    mMotionY是DOWN事件时的y坐标,mLastY等于Integer.MIN_VALUE的时候表示的是手指按下的时候(非fling时按下),所以incrementalDeltaY表示的是距离上一次滑动记录点的y方向的位移距离:

    if (incrementalDeltaY != 0) {
        atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
    }
    

    如果竖直方向产生了有效位移,则调用trackMotionScroll方法,里面又调用了offsetChildrenTopAndBottom(incrementalDeltaY)方法,这个方法在ViewGroup中定义:

    @UnsupportedAppUsage
    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的纵轴坐标都同时加上offset,mRenderNode持有了View在界面上显示的属性,所以在这里判断它是否为空可以得出界面上此时是否有正在显示的View,如果有则执行invalidateViewProperty方法:

    @UnsupportedAppUsage
    void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
        if (!isHardwareAccelerated()
                || !mRenderNode.hasDisplayList()
                || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            if (invalidateParent) {
                invalidateParentCaches();
            }
            if (forceRedraw) {
                mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
            }
            invalidate(false);
        } else {
            damageInParent();
        }
    }
    

    可以看到,调用invalidate引起当前节点以下的view树重绘,从而达到滚动内容的效果。

    注意这样一段代码:

    final int firstTop = getChildAt(0).getTop();
    final int lastBottom = getChildAt(childCount - 1).getBottom();
    
    final Rect listPadding = mListPadding;
    
    // "effective padding" In this case is the amount of padding that affects
    // how much space should not be filled by items. If we don't clip to padding
    // there is no effective padding.
    int effectivePaddingTop = 0;
    int effectivePaddingBottom = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        effectivePaddingTop = listPadding.top;
        effectivePaddingBottom = listPadding.bottom;
    }
    
     // FIXME account for grid vertical spacing too?
    final int spaceAbove = effectivePaddingTop - firstTop;
    final int end = getHeight() - effectivePaddingBottom;
    final int spaceBelow = lastBottom - end;
    ... ...
    ... ...
    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
    

    这段代码作用是什么呢?我们已经知道前面初始化添加的View只是界面上能显示的下的View,那么这里的getChildAt方法取得的就是屏幕上的显示的View(屏幕外的其实也不存在),仔细分析一下不难发现,这段代码其实就是判断滚动的距离是否超出已添加View的总高度(即是否需要显示新View),若是需要显示新的Item则调用fillGap方法:

    @Override
    void fillGap(boolean down) {
        final int count = getChildCount();
        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());
        } 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方法,从而回到obtainView、Adapter.getView的逻辑上去。

    在trackMotionScroll方法中还有一个点就是view回收复用的逻辑,拿向上滑动来说吧,代码如下:

    if (down) {
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }
    

    child.getBottom>=top这个算法你可以理解一下,其实就是往上滑动出了屏幕就保存,之所以用incrementalDeltaY而不是0可能是为了拇指的有效滑动做的限制,实际上就是还剩一点滑出屏幕的时候就要保存了。

  • Header和Footer

    那么HeaderViews和FooterViews是何时添加的呢?

    还记得在onLayout过程中构造View的时候会调用mAdapter.getView方法吧,在setAdapter的时候有这样的代码:

    if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
        mAdapter = adapter;
    }
    

    所以在setAdapter方法调用之前如果通过addHeaderView方法添加过HeaderView的话这里的mAdapter就是HeaderViewListAdapter(在KITKAT版本开始就不需要注意设置的顺序了),所以getView方法就是HeaderViewListAdapter的getView方法:

    public View getView(int position, View convertView, ViewGroup parent) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }
    
        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }
    
        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }
    

    可以看到,这里会判断position是否小于mHeaderViewInfos的容量,来决定此处是头部View还是常规item,如果不在头部View范围内了,adjPosition表示常规item中的position,如果这个值小于自定义Adapter.getCount()(我们重写的这个方法通常是返回数据集合的长度)则说明是在常规item范围,就去自定义Adapter中的getView方法去取View,如果超出了则表示进入了FooterViews的范围,adjPosition - adapterCount就是要取的FooterViews中的View的下标。

    同样,getItemViewType方法也是这样判断的:

    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }
    
        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }
    
  • 总结

    1. ListView会测量View的高度,根据position只创建ListView在屏幕中可见范围内能显示出来的View,关键方法是obtainView。
    2. 滚动的时候是先把当前屏幕内可见的View整体调整top和bottom从而达到上移或下移的效果,然后计算位移之后是否需要创建新的View,如果需要则重走obtainView方法,关键方法是trackMotionScroll。
    3. 不管是ScrapView集合还是TransientStateView集合保存了View,每次滑动或者初始化都会走Adapter的getView方法,当前position的View未初始化过的时候是为了创建新View,否则是为了和旧的比较,getView方法中传入的convertView就是之前保存的ScrapView或TransientStateView,这就是ListView为什么需要我们在getView方法中自定义ViewHolder的逻辑,如果没有这个逻辑,那么不管调用getView的目的是不是只是用于比较都需要调用LayoutInflater.inflate方法去重复创建View,从而影响性能,这就是convertView的来源和作用。
    4. header和footer都可以添加多个,在每次getView的时候都会先跳过这些集合的长度去找到普通的ItemView。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容