探究RecyclerView的设计和实现

如果你已经对RecyclerView熟练运用,那你是否想过RecyclerView是如何通过设置如下几行代码,就能实现应变千变万化的UI效果呢

  /*==============探究RecyclerView的设计和实现 ==================*/
    RecyclerView recyclerView = new RecyclerView(this);
    //设置布局方式为线性布局
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setHasFixedSize(true);
    //设置适配器
    recyclerView.setAdapter(new MyRcAapter());

某个控件或框架大家都说它设计的如何牛逼,而你也一直在用,那我们何尝不试着深入其源码的实现,看源码的过程,也在学习他人优秀的代码。

下面我们就一起来一探究竟吧!

首先我们从调用setAdapter方法开始入手,看看里面的具体实现是什么:

  public void setAdapter(Adapter adapter) {
    // bail out if layout is frozen
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    requestLayout();
}
 private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
        boolean removeAndRecycleViews) {
    //先移除原来的mAdapter,注销观察者,和从RecyclerView Detached。
    if (mAdapter != null) {
        mAdapter.unregisterAdapterDataObserver(mObserver);
        mAdapter.onDetachedFromRecyclerView(this);
    }
    //判断是否清除原有的ViewHolder
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        removeAndRecycleViews();
    }
    mAdapterHelper.reset();
    final Adapter oldAdapter = mAdapter;
    mAdapter = adapter;
    if (adapter != null) {
       //注册观察者
        adapter.registerAdapterDataObserver(mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
        mLayout.onAdapterChanged(oldAdapter, mAdapter);
    }
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
    //刷新试图
    setDataSetChangedAfterLayout();
}

这里主要做了几件事事情
1)判断是否有Adapter,如果有则先移除原来的,注销观察者,和从RecyclerView Detached。
2)判断是否清除原有的ViewHolder
3)注册观察者,刷新视图

具体的实现是RecyclerView内部的RecyclerViewDataObserver

private class RecyclerViewDataObserver extends AdapterDataObserver {
    RecyclerViewDataObserver() {
    }
    @Override
    public void onChanged() {
        assertNotInLayoutOrScroll(null);
        mState.mStructureChanged = true;

        setDataSetChangedAfterLayout();
        if (!mAdapterHelper.hasPendingUpdates()) {
            requestLayout();//重新绘制布局
        }
    }
  //....
}

这里的逻辑是,当数据发生变化的时候调用Adapter的notifyDataSetChanged方法之后最终会调用RecyclerViewDataObserver的onChanged函数,然后在onChanged又回调用requestLayout函数进行重新布局。

public abstract static class Adapter<VH extends ViewHolder> {
    //....
    public final void notifyDataSetChanged() {
        mObservable.notifyChanged();
    }
    //...
}  


static class AdapterDataObservable extends Observable<AdapterDataObserver> {
    //...
    public boolean hasObservers() {
        return !mObservers.isEmpty();
    }

    public void notifyChanged() {
        // since onChanged() is implemented by the app, it could do anything, including
        // removing itself from {@link mObservers} - and that could cause problems if
        // an iterator is used on the ArrayList {@link mObservers}.
        // to avoid such problems, just march thru the list in the reverse order.
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }
     //...
}

关于如何布局,RecyclerView中把这个职责则交给了LayoutManager,回到我们调用 setLayoutManager函数,其内部会调用requestLayout函数进行绘制布局,然后就会调用RecyclerView 的onLayout函数,再调用dispatchLayout函数,

public void setLayoutManager(LayoutManager layout) {
    if (layout == mLayout) {
        return;
    }
    //...
    mChildHelper.removeAllViewsUnfiltered();
    mLayout = layout;
    if (layout != null) {
       //...
        mLayout.setRecyclerView(this);
        if (mIsAttached) {
            mLayout.dispatchAttachedToWindow(this);
        }
    }
    mRecycler.updateViewCacheSize();
    requestLayout();
}


protected void onLayout(boolean changed, int l, int t, int r, int b) {
    this.eatRequestLayout();
    //调用分发方法
    this.dispatchLayout();
    this.resumeRequestLayout(false);
    this.mFirstLayoutComplete = true;
}

在dispatchLayout中会调用Adapter 中的getCount函数获取到元素的个数,通过调用LayoutManager的onLayoutChilden函数,对所有子元素进行布局。
这里有三个函数dispatchLayoutStep1 、dispatchLayoutStep2 、dispatchLayoutStep3都会执行,做了简单的逻辑处理,避免重复执行某个方法,其中dispatchLayoutStep2是真正起布局作用的
(如果大家看过ListView的源码,就会知道ListView是在添加到窗口时调用其父类absListView的onAttachedAToWindow函数,然后获取元素的个数,再执行onLayoutChilden函数,并在ListView实现这个函数)

 void dispatchLayout() {
   //...
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {//没有执行过布局
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        //  执行过布局,但是宽高发生改变
        
        mLayout.setExactMeasureSpecsFrom(this);
        //真正的视图的布局 
        dispatchLayoutStep2();
    } else {// 执行过布局,宽高没有改变
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

  private void dispatchLayoutStep2() {
   //...
    //获取Item数量
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    //执行布局,调用LayoutManager的onLayoutChilden函数
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;
    //...
}

那么在LayoutManager的onLayoutChildren方法中具体做了什么,则是根据布局模式来布局ItemView,例如从上到下布局,还是从下到上布局,在每一种布局方式中都会调用fill函数,在fill函数中又回循环的layoutChunk函数进行布局,每次布局完之后判断,计算当前屏幕剩余的空间和是否需还有Item View。

  public void onLayoutChildren(Recycler recycler, State state) {
        //...
        if (this.mAnchorInfo.mLayoutFromEnd) {//从下往上布局
        this.updateLayoutStateToFillStart(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForStart;
        this.fill(recycler, this.mLayoutState, state, false);
        startOffset1 = this.mLayoutState.mOffset;
        if (this.mLayoutState.mAvailable > 0) {
            extraForEnd += this.mLayoutState.mAvailable;
        }

        this.updateLayoutStateToFillEnd(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForEnd;
        this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
        this.fill(recycler, this.mLayoutState, state, false);//填充ItemView
        endOffset = this.mLayoutState.mOffset;
    } else {//从上到下部剧
        this.updateLayoutStateToFillEnd(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForEnd;
        this.fill(recycler, this.mLayoutState, state, false);
        endOffset = this.mLayoutState.mOffset;
        if (this.mLayoutState.mAvailable > 0) {
            extraForStart += this.mLayoutState.mAvailable;
        }
        this.updateLayoutStateToFillStart(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForStart;
        this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
        this.fill(recycler, this.mLayoutState, state, false);//填充ItemView
        startOffset1 = this.mLayoutState.mOffset;
    }
    //...
  }

  int fill(Recycler recycler, LayoutState layoutState, State state, boolean stopOnFocusable) {
    //存储当前可用空间
    int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != -2147483648) {
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        this.recycleByLayoutState(recycler, layoutState);
    }
    //计算RecyclerView的可用布局宽或高
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
    //迭代布局Item View
    while (remainingSpace > 0 && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        //布局ItemView
        this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (layoutChunkResult.mFinished) {
            break;
        }
        //计算布局偏移量
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        if (!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            //计算剩余的可用空间
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        //...
    }

    return start - layoutState.mAvailable;
}

接下来我们看一下layoutChunk函数

1)首先是从layoutstate中获取到ItemView的布局参数、尺寸信息
2)然后并且根据布局方式计算出Item View的上下左右坐标
3)最后调用layoutDecoratedWithMargins函数实现布局,调用Item View的layout函数将Item View布局到具体的位置。
这么一处理,LayoutManager就把RecyclerView布局的职责分离了出来,这也使得RecyclerView更灵活。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LinearLayoutManager.LayoutState layoutState, LayoutChunkResult result) {
    // 1,获取Item View
    View view = layoutState.next(recycler);
    if (view == null) {
        if (DEBUG && layoutState.mScrapList == null) {
            throw new RuntimeException("received null view when unexpected");
        }
        // if we are laying out views in scrap, this may return null which means there
        // is
        // no more items to layout.
        result.mFinished = true;
        return;
    }
    //2 获取 Item View的布局参数
    LayoutParams params = (LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection == android.support.v7.widget.LinearLayoutManager.LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection == android.support.v7.widget.LinearLayoutManager.LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    //3 测量Item View的布局参数
    measureChildWithMargins(view, 0, 0);
    //4 计算该ItemView 需要的宽度或高度
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    //ItemView的上下左右的坐标
    int left, top, right, bottom;
    //5 竖直方向 计算上下左右的坐标
    if (mOrientation == VERTICAL) {
        if (isLayoutRTL()) {
            right = getWidth() - getPaddingRight();
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            left = getPaddingLeft();
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        if (layoutState.mLayoutDirection == android.support.v7.widget.LinearLayoutManager.LayoutState.LAYOUT_START) {
            bottom = layoutState.mOffset;
            top = layoutState.mOffset - result.mConsumed;
        } else {
            top = layoutState.mOffset;
            bottom = layoutState.mOffset + result.mConsumed;
        }
    } else {////水平方向 计算上下左右的坐标
        top = getPaddingTop();
        bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

        if (layoutState.mLayoutDirection == android.support.v7.widget.LinearLayoutManager.LayoutState.LAYOUT_START) {
            right = layoutState.mOffset;
            left = layoutState.mOffset - result.mConsumed;
        } else {
            left = layoutState.mOffset;
            right = layoutState.mOffset + result.mConsumed;
        }
    }
    //6 布局Item View
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    if (DEBUG) {
        Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + (right - params.rightMargin) + ", b:"
                + (bottom - params.bottomMargin));
    }
    // Consume the available space if the view is not removed OR changed
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}

 public void layoutDecoratedWithMargins(View child, int left, int top, int right,
            int bottom) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Rect insets = lp.mDecorInsets;
        child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                right - insets.right - lp.rightMargin,
                bottom - insets.bottom - lp.bottomMargin);
    }

看到这里,我们已经知道如何布局,还有一个特别重要的点没讲,就是如何获取创建布局和添加数据以及它的缓存机制

那我们需要通过LayoutState对象next这个重要的函数入手

   View next(RecyclerView.Recycler recycler) {
        //...
        //调用Recycler的getViewForPosition
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }

从上面可以看到,实际上调用Recycler的getViewForPosition函数,再通过tryGetViewHolderForPositionByDeadline函数返回ViewHolde获取其中的Item View

public final class Recycler {
   final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
   ArrayList<ViewHolder> mChangedScrap = null;
   final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  //...
   public View getViewForPosition(int position) {
       //调用Recycler的getViewForPosition
       return getViewForPosition(position, false);
   }
   View getViewForPosition(int position, boolean dryRun) {
        //返回ViewHolder的ItemView
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        //...
        boolean fromScrapOrHiddenOrCache = false;
        ViewHolder holder = null;
        // 0)如果有缓存, 从mChangedScrap中获取ViewHolder缓存
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) 从 mAttachedScrap 中获取ViewHolder 缓存
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            //...
            
        }
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            //...
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)从其他ViewHolder缓存中检测是否有缓存
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                if (holder != null) {
                    // update position
                    holder.mPosition = offsetPosition;
                    fromScrapOrHiddenOrCache = true;
                }
            }
            //...
            // 3)没有ViewHolder,则需要创建ViewHolder,这里就会调用createViewHolder函数
            if (holder == null) {
                long start = getNanoTime();
                if (deadlineNs != FOREVER_NS && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                    // abort - we have a deadline we can't meet
                    return null;
                }
                holder = mAdapter.createViewHolder(this, type); 
                //...
            }
        }
        //...
        boolean bound = false;
        if (mState.isPreLayout() && holder.isBound()) {
            // do not update unless we absolutely have to.
            holder.mPreLayoutPosition = position;
        } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
            if (DEBUG && holder.isRemoved()) {
                throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder + exceptionLabel());
            }
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            // 4)绑定数据,这里会调用Adapter的onBindViewHolder
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }
        /*===============设置Item View的LayoutParams=================*/
        final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        final RecyclerView.LayoutParams rvLayoutParams;
        if (lp == null) {
            rvLayoutParams = (RecyclerView.LayoutParams) generateDefaultLayoutParams();
            holder.itemView.setLayoutParams(rvLayoutParams);
        } else if (!checkLayoutParams(lp)) {
            rvLayoutParams = (RecyclerView.LayoutParams) generateLayoutParams(lp);
            holder.itemView.setLayoutParams(rvLayoutParams);
        } else {
            rvLayoutParams = (RecyclerView.LayoutParams) lp;
        }
        rvLayoutParams.mViewHolder = holder;
        rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
        //返回ViewHolder
        return holder;
    }

}

在RecyclerView的内部类Recycler 中有mAttachedScrap 、mChangedScrap、mCachedViews几个ViewHolder列表对象,它们用于缓冲ViewHolder。

深入tryGetViewHolderForPositionByDeadline函数
1)首先从几个ViewHolder缓存对象获取对应位置的ViewHolder
2)如果没有缓存则调用RecyclerView.Adapter.createViewHolder函数创建ViewHolder

/**
 *createViewHolder函数实际是调用了onCreateViewHolder函数创建了ViewHolder
 * 这就是为什么在继承RecyclerView.Adapter是需要复写  onCreateViewHolder函数,
 *并返回ViewHolder的原因
 */
public final VH createViewHolder(ViewGroup parent, int viewType) {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        final VH holder = onCreateViewHolder(parent, viewType);
        holder.mItemViewType = viewType;
        TraceCompat.endSection();
        return holder;
    }
//创建ViewHolder,子类需复写
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);

3)在调用完RecyclerView.Adapter的onCreateViewHolder后,则执行tryBindViewHolderByDeadline,调用Adapter的onBindViewHolder

 //bindViewHolder进行数据绑定,执行完onBindViewHolder函数之后数据就绑定到Item View上
 public final void bindViewHolder(VH holder, int position) {
        //...
        onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
        //...
  }
 public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
        onBindViewHolder(holder, position);
   }
 //绑定数据,子类需复写,
 public abstract void onBindViewHolder(VH holder, int position);

看到这里,我相信大家对RecyclerView整体的设计有了一定的了解,
这个时候你再使用RecyclerView,继承Adapter并实现onCreateViewHolder 、onBindViewHolder、getItemCount这个三个方法,就知道为什么了,而不再是简单的使用

总结

最后还是要把这篇博客总结一下

RecyclerView 通过Adapter 和观察者模式进行数据绑定,在Adapter中封装了ViewHolder的创建与绑定逻辑,使用起来更加方便,而其缓存单元不同于ListView,而是用ViewHolder代替了View,代替的之前的繁琐的步骤。并且把布局的工作交给了LayoutManager,在LayoutManager的onLayoutChilden中对ItemView 进行布局等一系列操作,这样一来也大大的增加了布局的灵活性。把布局责任独立出来也更符合设计模式中的单一职责原则,减少代码的耦合,使得RecyclerView的布局更具扩张性。


风后面是风,天空上面是天空,而你的生活可以与众不同

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容