RecyclerView的使用,一篇文章就够了。

RecyclerView是support v7中提供的一个控件,可以说是listView和GridView的增强版,提供了一种插拔式的体验。提供了三个设置方法来供我们快速做出一些我们想要的效果效果。

  • LayoutManager:控制的是RecyclerView的显示方式。
  • ItemDecoration:绘制Item之间的间隔,并在特定时间绘制一些我们想要的东西。
  • ItemAnimator:控制Item增删的动画。

今天我们来学习一下RecyclerView的基本使用方法。我们想要使用它,首先我们要在app/build.gradle文件中添加依赖。

    implementation 'com.android.support:recyclerview-v7:27.1.1'
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rcl_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

添加好布局之后,和我们的listView一样,要创建Adapter和ViewHolder。和listView不同Recycler提供了内部抽象类为我们来继承,RecyclerView.Adapter<ViewHolder> 和 RecyclerView.ViewHolder。我们先来看一个例子。

    class MyAdapter  extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
        private List<User> userList;

        public MyAdapter(List<User> userList) {
            this.userList = userList;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view  = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main,parent,false);
            ViewHolder viewHolder = new ViewHolder(view);
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            holder.tvName.setText("Name:"+userList.get(position).getName());         holder.tvAge.setText("Age:"+userList.get(position).getAge());
        }
        @Override
        public int getItemCount() {
            return userList.size();
        }
        class ViewHolder extends RecyclerView.ViewHolder{
            private TextView tvName ;
            private TextView tvAge;

            public ViewHolder(View itemView) {
                super(itemView);
                tvName = itemView.findViewById(R.id.tv_name);
                tvAge = itemView.findViewById(R.id.tv_age);
            }
        }
    }

我们可以看到 我们的MyAdapter继承自RecyclerView.Adapter,重写了三个方法。

  • onCreateViewHolder():用来创建ViewHolder实例,我们在这个方法中将item布局加载进来,并创建一个ViewHolder实例,将我们的item布局传入到ViewHolder的构造方法中,并将ViewHolder返回。
  • onBindViewHolder():通过ViewHolder拿到的item布局中的View进行赋值,写过listView的你对这一步一定不会陌生。
  • getItemCount():;返回数据集的个数,也就是我们RecyclerView的item的个数。

ViewHolder的实现,我们继承RecyclerView.ViewHolder,重写了构造方法。

  • 在构造方法中我们拿到Item的View。进行findViewById查找到我们索要用的控件。方便我们进行使用。

接下来我们在Activity中找到我们到RecyclerView进行使用。

//创建一个存储User数据的集合
List<User> userList = new ArrayList<>();
//在布局中找到我们的RecyclerView
rclMain = findViewById(R.id.rcl_main);
//增加一些User假数据,放在我们的集合当中
for (int i=0 ;i<=30 ;i++){
     userList.add(new User("张"+i,20+i));
}
//为我们的RecyclerView设置Adapter
rclMain.setAdapter(new MyAdapter(userList));
//设置我们的LayoutManager为线性布局竖直方向的
rclMain.setLayoutManager(new LinearLayoutManager(this));
//设置我们的Item之间的间隔为DividerItemDecoration为竖直方向。如需修改其默认样式,
//可以修改 <!-- Application theme. -->中名字为android:listDivider的属性。
rclMain.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

以上的效果和listView完全相同,并且Item之间的间隔是一个白色的一条线。大家可以看一下1-1的图。


1-1

LayoutManager

RecyclerView 中设置LayoutManager方法的参数是RecyclerView.LayoutManager吧,这是一个抽象类,好在系统提供了3个实现类:

  • LinearLayoutManager 现行管理器,支持横向、纵向。(默认)
  • GridLayoutManager 网格布局管理器。
  • StaggeredGridLayoutManager 瀑布就式布局管理器。
    如果我们想做一个横向listView的效果也非常简单,稍微修改一下Item布局,将我们设置的LayoutManager设置成横向的LinearLayoutManager就可以了
 rclMain.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
1-2

GridLayoutManager 实现纵向的网格布局。

//设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
 rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.VERTICAL,false));
1-3

GridLayoutManager 实现横向的网格布局。

//设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
 rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false));
1-4

StaggeredGridLayoutManager实现瀑布流,已经看过上面的实现网格布局和线性list布局,我相信实现网格布局对你来说已经非常之简单了。如何随机设置item的高度保证item每个都不一样高。这里就不贴出来了。相信一定难不倒你的。

//设置数量和方向,我们设置的是3列纵向布局。
 rclMain.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));
1-5

只要修改一行代码就能实现三种不同的布局方式,Recycler的强大你是不是已经体会到了。反正我早已经被他震撼到了。

ItemDecoration

我们可以使用addItemDecoration(Recycler.ItemDecoration)添加item之间要绘制的东西。RecyclerView.ItemDecoration是抽象类,Android只提供了DividerItemDecoration一个实现类用来给使用LinearLayoutManager的RecyclerView绘制Item之间的间隙。我们可以修改APPTheme 中的android:listDivider来修改我们想要的间隙样式

values/styles.xml
    <!-- Base application theme. -->
    <style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    <style name="AppTheme" parent="AppBaseTheme">
        <item name="android:listDivider">@drawable/divider_bg</item>
    </style>

divider_bg.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line" >
    <stroke android:color="@android:color/white"/>
    <size android:height="4dp" />
</shape>

而Android提供的这个不能满足我们所有的需求,比如我们想给我们的网格布局添加item之间的间隔就不能使用DividerItemDecoration,我们只能自己来实现了。继承RecyclerView.ItemDecoration。首先我们来了解一下RecyclerView.ItemDecoration中的几个方法的含义

  • getItemOffests:可以通过outRect.set(l,t,r,b)设置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
  • onDraw:可以通过一系列c.drawXXX()方法在绘制itemView之前绘制我们需要的内容。可以理解为在Item下层 像BackGround
  • onDrawOver:与onDraw类似,只不过是在绘制itemView之后绘制,具体表现形式,就是绘制的内容在itemview上层。

我们只设置item间隔的话 是不需要进行onDraw和onDrawOver操作的,除非你想在你要设置的这个间隔中绘制一些想要的东西。而想要设置Item之间的间隔,只要通过计算我们就可以通过getItemOffests设置item的padding就能够实现我们的效果。首先我们来看一下我们具体的实现思路。

1、首先我们要初步确定我们没个item条目的padding计算规则。为了让我们的网格布局平分宽高所以我们每个item的padding值是不一样的,我们来看下面的一张演示图。
2-1无边距

2-2有边距

当orientation为vertical时,我们需要在getItemOffsets方法中计算每个Item的PaddingLeft,以及PaddingRight,保证每个Item的paddingLeft+paddingRight相等,这样才能达到均分的目的。
这里我们可以用数字的带入来计算我们每个item的padding规律,从而推导出我们要的公式。这里我也是查看的大神的博客,如果想知道具体规则可在本章底部找到答案。

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        RecyclerView.LayoutManager manager = parent.getLayoutManager();
        int childPosition = parent.getChildAdapterPosition(view);
        int itemCount = parent.getAdapter().getItemCount();
        if (manager != null) {
            if (manager instanceof GridLayoutManager) {
                // manager为GridLayoutManager时
                setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
            }
        }
    }

/**
     * 设置GridLayoutManager 类型的 offest
     *
     * @param orientation   方向
     * @param spanCount     个数
     * @param outRect       padding
     * @param childPosition 在 list 中的 postion
     * @param itemCount     list size
     */
    private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
        float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 总共的padding值
        float eachSpace = totalSpace / spanCount; // 分配给每个item的padding值
        int column = childPosition % spanCount; // 列数
        int row = childPosition / spanCount;// 行数
        float left;
        float right;
        float top;
        float bottom;
        if (orientation == GridLayoutManager.VERTICAL) {
            top = 0; // 默认 top为0
            bottom = mSpace; // 默认bottom为间距值
            if (mEdgeSpace == 0) {
                left = column * eachSpace / (spanCount - 1);
                right = eachSpace - left;
                // 无边距的话  只有最后一行bottom为0
                if (itemCount / spanCount == row) {
                    bottom = 0;
                }
            } else {
                if (childPosition < spanCount) {
                    // 有边距的话 第一行top为边距值
                    top = mEdgeSpace;
                } else if (itemCount / spanCount == row) {
                    // 有边距的话 最后一行bottom为边距值
                    bottom = mEdgeSpace;
                }
                left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                right = eachSpace - left;
            }
        } else {
            // orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小异, 将top,bottom替换为left,right即可
            left = 0;
            right = mSpace;
            if (mEdgeSpace == 0) {
                top = column * eachSpace / (spanCount - 1);
                bottom = eachSpace - top;
                if (itemCount / spanCount == row) {
                    right = 0;
                }
            } else {
                if (childPosition < spanCount) {
                    left = mEdgeSpace;
                } else if (itemCount / spanCount == row) {
                    right = mEdgeSpace;
                }
                top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                bottom = eachSpace - top;
            }
        }
        outRect.set((int) left, (int) top, (int) right, (int) bottom);
    }

接下来是onDraw和onDrawOver的用法。这里我制作简单的介绍,如果你想要做出一些不一样的内容。文章末尾我会给出几个比较好的博客,供大家欣赏。
首先我们要知道onDraw和onDrawOver的执行时间,一下是RecyclerView绘制流程的源码

/**
     * RecyclerView的draw方法
     * @param c
     */
    @Override
    public void draw(Canvas c) {
        // 调用父类也就是View的draw方法
        super.draw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            // 执行ItemDecorations的onDrawOver方法
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
    }

    /**
     *  View的draw方法
     * @param canvas
     */
    @CallSuper
    public void draw(Canvas canvas) {
        ....
        // View会继续调用onDraw
        if (!dirtyOpaque) onDraw(canvas);
        ....
    }

    /**
     * RecyclerView的onDraw方法
     * @param c
     */
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            // 执行ItemDecorations的onDraw方法
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

View先会调用draw方法,在draw中又会调用onDraw方法。 而在RecyclerView的draw方法中会先通过super.draw() 调用父类也就是View的draw方法,进而继续调用RecyclerView的OnDraw方法,ItemDecorations的onDraw方法就在此时会被调用,RecyclerView执行完super.draw()之后,ItemDecorations的onDrawOver方法也被调用,这也就解释了为什么说onDraw会绘制在itemview之前,表现形式是在最底层(抽象的说法,最底层应该是background),onDrawOver是在itemview绘制之后,表现形式在最上层。知道了onDraw和onDrawOver的具体执行时间与含义。我们这里再来看Android提供给我们的DividerItemDecoration实现我们listView的Divider效果是如何做到的

   @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

   private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int top;
        final int bottom;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top,
                    parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }

  @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

原理很简单,总结起来就是通过getItemOffsets方法来设置我们item的paddingBottom或paddingRight,然后在我们onDraw方法中遍历我们的item并在我们需要的区域内绘制我们想要的绘制的内容。

setItemAnimator

设置我们的Item动画效果。Android提供了一个默认的实现。DefaultItemAnimator,添加以下代码我们就可以了。

rclMain.setItemAnimator(new DefaultItemAnimator());

添加了这行代码我们再运行我们的代码发现,完全木有效果啊,什么鬼。
这里我忘记和大家说了。如果我们想要看到效果,就在我们的数据集合中添加删除数据。并执行notifyItemInserted(position)或notifyItemRemoved(position) ,就可以看到效果啦。当然这一种效果怎么可能满足的了我们产品的需求呢,幸好我们有大神。大神们已经开源了很多有关的代码。这就要大家自己去挖掘了。

点击事件

因为RecyclerView没有给我们提供onItemClickListener的方法。所以这个只能我们自己来实现,具体实现比较简单,和我们ListView,Item中设置点击按钮的实现方式是一样的。我们只需要自己在RecyclerView中写一个onclickListener监听器,并提供出去,在BindViewHolder的时候设置ItemView的点击事件,调用我们监听器的方法回调给我们的使用者就可以了。

class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder>
{

//...
    public interface OnItemClickLitener
    {
        void onItemClick(View view, int position);
        void onItemLongClick(View view , int position);
    }

    private OnItemClickLitener mOnItemClickLitener;

    public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)
    {
        this.mOnItemClickLitener = mOnItemClickLitener;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position)
    {
        holder.tv.setText(mDatas.get(position));

        // 如果设置了回调,则设置点击事件
        if (mOnItemClickLitener != null)
        {
            holder.itemView.setOnClickListener(new OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    int pos = holder.getLayoutPosition();
                    mOnItemClickLitener.onItemClick(holder.itemView, pos);
                }
            });

            holder.itemView.setOnLongClickListener(new OnLongClickListener()
            {
                @Override
                public boolean onLongClick(View v)
                {
                    int pos = holder.getLayoutPosition();
                    mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
                    return false;
                }
            });
        }
    }
//...
}

知道了如何设置Item的点击事件,我相信设置item中单独View的点击事件也一定难不住你了。

不同类型的Item

我们可以根据RecyclerView.Adapter提供的getItemViewType();方法根据Position位置返回我们不同的Type类型。
在onCreateViewHolder中根据不同的type创建不同的布局和ViewHolder并返回。在onBindViewHolder中根据ViewHodler instanceof方法获取他数据哪个ViewHolder来进行赋值操作,例子如下:

lass HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int TYPE_HEADER = 1;
    private static final int TYPE_ITEM = 2;

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {
            //...
            return new HeaderViewHolder(v);
        } else {
            //...
            return new MyViewHolder(v);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof HeaderViewHolder) {
            //...
        } else if (holder instanceof MyViewHolder) {
            //...
        }
    }

    @Override
    public int getItemCount() {
        return mDatas.size() + 1; //增加头部ItemView
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_HEADER;
        } else {
            return TYPE_ITEM;
        } 
    }

    class HeaderViewHolder extends RecyclerView.ViewHolder {
        //...
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        //...
    }
}

参考文章:https://www.jianshu.com/p/bb09d3ddc64f
https://blog.csdn.net/lmj623565791/article/details/45059587
https://www.jianshu.com/p/e742df6f59e2
https://www.jianshu.com/p/6a093bcc6b83
https://www.jianshu.com/p/3221b5c8fc38

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