BRAVH源码模拟

BRAVH是一个recyclerView的adapter,能够快速适配多种类型adapter,可定制,用的人挺多,下面我们就来分析分析他的源码,模拟来写一个我们的adapter。

我们将recyclerView的adapter与自定义viewholder联系在一起,使用了BaseQuickAdapter<T, K extends BaseViewHolder>来作为recycler.adapter

先看BRAVH的BaseViewHolder类

BaseViewHolder extends RecyclerView.ViewHolder

继承自ViewHolder,里面setXXX方法全是由itemview里面的view调用方法实现
存放了一个SparseArray<View> views成员变量用来初始化或者第一次遍历存放引用,添加快捷操作,省去下一次findview的时间

接下来看BaseQuickAdapter类

BaseQuickAdapter<T, K extends BaseViewHolder> extends RecyclerView.Adapter<K>

将K泛型传入给RecyclerView.Adapter作为viewholder
T泛型分析:
传入的数据集List<T>的类型,用来绑定数据

自定义Adapter,Viewholder

我们也来模拟一个adapter类型,使用自定义继承viewHolder的MyViewHolder作为Viewholder

public class BackQuickAdapter<I,VH extends MyViewHolder> extends RecyclerView.Adapter<VH> {

    private List<String> datas;

    public BackQuickAdapter(List<String> datas) {
        this.datas = datas;
    }

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list,parent,false);
        //实例化VH对象
        return createViewHolder(view);
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        View contentView = holder.itemView;
        ((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(datas.get(position));
        //do something about contentView
    }

    @Override
    public int getItemCount() {
        return datas == null ? 0 : datas.size();
    }

}

这样我们就有了一个adapter,里面的Viewholder由外部传进来,我们可以继承该Viewholder自己做快捷操作,让外部继承实现MyViewholder逻辑,同BaseViewHolder,他也只是对itemview引用设置子View参数,这部分基本忽略。我们看onCreateViewHolder->VH createViewHolder(View view)这个方法,这是一个泛型类的实例化,这个直接上代码见git。

给itemView添加加载动画

onViewAttachedToWindow(VH holder)方法:
每次Viewholder添加到window的时候contentView开始动画可以制作item加载效果
想要控制只让itemView进行一次动画,BRAVH里面设置了一个标志位,我们也写一个AnimOnce来做这个标志位,每次加载的时候得到viewholder的位置,并与上一次的加载过的位置比较,如果小,代表当前位置的contentview是新的,需要开启动画,否则如果AnimOnce只要一次,不开启动画,因为动画已经之前被加载过,我们可以这样实现:

/**
 * 是否只需要一次item加载动画
 */
private boolean AnimOnce = true;
/**
 * 最新加载过动画的item的位置,用于比较下一次item的位置判断是否要加载
 */
private int vaildPos = 0;
@Override
public void onViewAttachedToWindow(VH holder) {
    int currentPos = holder.getAdapterPosition();
    if(AnimOnce)
        if(currentPos > vaildPos){
            animateView(holder.itemView);
            vaildPos = currentPos;
        }
    else{
            animateView(holder.itemView);
        }
    super.onViewAttachedToWindow(holder);
}
private void animateView(View root){
    root.setAlpha(0.3f);
    root.animate().alpha(1).setDuration(2500).start();
}
public void setAnimOnce(boolean animOnce) {
    AnimOnce = animOnce;
}

这样,就能添加我们的itemview动画,如果需要外部定制,我们可以修改我们的animateView(View root)方法,给里面添加一个animation动画,并且提供外部接口,比如:

private Animation animation = null;
public void setItemAnimation(Animation animation) {
    this.animation = animation;
}

private void animateView(View root){
    if(null != animation)
    {
        root.startAnimation(animation);
    }
}

关于动画,可以使用animator或者animation都可以的,只要定制animateView方法

添加头部尾部空数据的布局

我们看BRAVH如何实现,

  1. 添加header,footer,loading的view
  2. GetItemCount需要返回的数据量为headercount + datas.size + footercount + loadingcount
  3. GetItemType如果position < headercount,返回头布局类型,得到普通布局的normal_position = position - headercount如果normal_position < datas.size [List数据集],返回普通列表类型,否则代表有尾布局footer

下面我们也来模拟一个头部尾部布局~
写一个方法来添加头部尾部视图

private int headerLayout;
private int footerLayout;
private boolean hasHeader = false;
private boolean hasFooter = false;
//added first before call setAdapter
public void addHeaderView(@LayoutRes int headerLayout){
    this.headerLayout = headerLayout;
    hasHeader = true;
}
//added first before call setAdapter
public void addFooterView(@LayoutRes int footerLayout){
    this.footerLayout = footerLayout;
    hasFooter = true;
}

GetItemType返回对应Type

@Override
public int getItemCount() {
    return datas == null ? 0 : datas.size() + (hasFooter ? 1 : 0) + (hasHeader ? 1 : 0);
}
private final static int ITEM_TYPE_HEADER = -1;
private final static int ITEM_TYPE_NORMAL = 0;
private final static int ITEM_TYPE_FOOTER = 1;
@Override
public int getItemViewType(int position) {
    int datasize = (datas == null ? 0 : datas.size());
    int headerCount = hasHeader ? 1 : 0;
    if(position < headerCount)
        return ITEM_TYPE_HEADER;
    int realIndex = position - headerCount;
    if(realIndex < datasize){
        return ITEM_TYPE_NORMAL;
    }
    else{
        return ITEM_TYPE_FOOTER;
    }
}

根据对于Type构造Viewholder

@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = null;
    switch (viewType)
    {
        case ITEM_TYPE_HEADER:
            view = LayoutInflater.from(parent.getContext()).inflate(headerLayout, parent, false);
            break;
        case ITEM_TYPE_FOOTER:
            view = LayoutInflater.from(parent.getContext()).inflate(footerLayout, parent, false);
            break;
        default:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false);
    }
    //实例化VH对象
    return createViewHolder(view);
}

根据position位置来绑定viewholder数据

@Override
public void onBindViewHolder(VH holder, int position) {
    if(position < (hasHeader ? 1 : 0))
    {
        // this is header view
        return ;
    }
    int realDataPos = adjustPositionByType(position);
    if(realDataPos < datas.size())
    {
        View contentView = holder.itemView;
        ((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(datas.get(realDataPos));
    }
    else{
        //this is footer View
    }
}

这样我们的头部尾部布局就添加好了,添加loading布局与空布局也是一个道理,只是多加载了一种类型而已

加载更多的实现

这是在OnBindViewHolder->position判断位置
如果position已经在最后的位置,那么触发加载更多
下面我们可以写一个加载更多的方法:

private void autoLoadMore(int position) {
    if(position == getItemCount() -1)
    {
        Log.i("position","current end is "+String.valueOf(position));
        //trigger loading more
    }
}
@Override
public int getItemCount() {
    return datas == null ? 0 : datas.size() + (hasFooter ? 1 : 0) + (hasHeader ? 1 : 0);
}

实现拖拽,滑动删除

BRAVH是怎么实现呢?官方这样使用:

ItemDragAndSwipeCallback itemDragAndSwipeCallback = new ItemDragAndSwipeCallback(mAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemDragAndSwipeCallback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
// 开启拖拽
mAdapter.enableDragItem(itemTouchHelper, R.id.textView, true);
mAdapter.setOnItemDragListener(onItemDragListener);
// 开启滑动删除
mAdapter.enableSwipeItem();
mAdapter.setOnItemSwipeListener(onItemSwipeListener);

我们可以看到使用的类是ItemDragAndSwipeCallback ,这里面需要传入adapter需要BaseItemDraggableAdapter类型,我们进入ItemDragAndSwipeCallback 发现其实只是BaseItemDraggableAdapter回调用,其实本身并没有用到任何BaseItemDraggableAdapter属性。所以可以做出一个提取的过程,把传入类型BaseItemDraggableAdapter修改成interface,只要RecyclerAdapter实现这个接口就可以了,这样不必非要传入BaseItemDraggableAdapter类型。


这部分也可以直接用原生ItemTouchHelper,复写onMove实现item交换,onSwiped实现Item删除
我们开始写交换逻辑:

  1. 首先剔除header视图的位置获得在datas数据集中的位置realpos
  2. 交换数据集中的realpos位置数据
  3. Notifydatachanged
    或许我们可以这样实现
@Override
public void onItemDragMoving(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
    //change item position
    int from = getViewHolderPosition(source);
    int to = getViewHolderPosition(target);
    if (inRange(from) && inRange(to)) {
        if (from < to) {
            for (int i = from; i < to; i++) {
                Collections.swap(datas, i, i + 1);
            }
        } else {
            for (int i = from; i > to; i--) {
                Collections.swap(datas, i, i - 1);
            }
        }
        notifyItemMoved(source.getAdapterPosition(), target.getAdapterPosition());
    }
}
public int getViewHolderPosition(RecyclerView.ViewHolder viewHolder) {
    return viewHolder.getAdapterPosition() - (hasHeader ? 1 : 0);
}
private boolean inRange(int position) {
    return position >= 0 && position < datas.size();
}

下面我们实现删除逻辑:

  1. 首先剔除header视图的位置获得在datas数据集中的位置realpos
  2. 删除数据集中的realpos位置数据
  3. NotifyItemRemoved
@Override
public void onItemSwiped(RecyclerView.ViewHolder viewHolder) {
    //notify item removed
    int position = getViewHolderPosition(viewHolder);
    if(position >= 0 && position < datas.size()) {
        datas.remove(position);
        notifyItemRemoved(getPositionInSets(position));
    }
}
public int getPositionInSets(int position) {
    return position + (hasHeader ? 1 : 0);
}

自定义使用不同的Item类型
现在默认item类型有header,footer,loading,empty,default。如果想要自定义类型,那么我们可以修改自定义的adapter,在getItemType返回default类型的时候,使用抽象方法让子类实现,修改adapter为抽象类,这将影响:

  1. getItemtype 使用抽象方法getDefItemViewType(int realDataPos)
  2. oncreateViewholder 返回的View需要根据自定义itemtype实现自定义view,所以暴露抽象方法onCreateDefViewHolder(ViewGroup parent, int viewType)
  3. onBindViewHolder 绑定数据时需要根据自定义的ViewHolder来自定视图数据的绑定,所以暴露抽象方法onBindDefViewHolder(VH holder, int realDataPos)

这样我们便能够自定义itemType


getItemtype 使用抽象方法getDefItemViewType(int realDataPos)

public int getItemViewType(int position) {
    int datasize = (datas == null ? 0 : datas.size());
    int headerCount = hasHeader ? 1 : 0;
    if(position < headerCount)
        return ITEM_TYPE_HEADER;
    int realIndex = position - headerCount;
    if(realIndex < datasize){
        return getDefItemViewType(realIndex);
    }
    else{
        return ITEM_TYPE_FOOTER;
    }
}
//multi item must be override
protected int getDefItemViewType(int realDataPos) {
    //datas position item Type
    return ITEM_TYPE_NORMAL;
}

暴露抽象方法onCreateDefViewHolder(ViewGroup parent, int viewType)

public VH onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = null;
    switch (viewType)
    {
        case ITEM_TYPE_HEADER:
            view = LayoutInflater.from(parent.getContext()).inflate(headerLayout, parent, false);
            break;
        case ITEM_TYPE_FOOTER:
            view = LayoutInflater.from(parent.getContext()).inflate(footerLayout, parent, false);
            break;
        default:
            view = onCreateDefViewHolder(parent,viewType);
    }
    //实例化VH对象
    return createViewHolder(view);
}

//be override
abstract View onCreateDefViewHolder(ViewGroup parent, int viewType);

回顾的时候发现还是返回VH类型的Viewholder容易定制,这样自定义实现就不需要泛型实例化方法
abstract VH onCreateDefViewHolder(ViewGroup parent, int viewType);


暴露抽象方法onBindDefViewHolder(VH holder, int realDataPos)

public void onBindViewHolder(VH holder, int position) {
    autoLoadMore(position);

    if(position < (hasHeader ? 1 : 0))
    {
        // this is header view
        return ;
    }
    int realDataPos = adjustPositionByType(position);
    if(realDataPos < datas.size())
    {
        onBindDefViewHolder(holder,realDataPos);
    }
    else{
        //this is footer View
    }
}
//be override
abstract void onBindDefViewHolder(VH holder, int realDataPos);

我们的ItemType是由传入的数据类型决定的,可以定义一个接口,让传入的数据类型实现该接口并且实现getItemType

public interface ItemType {
    int getItemType();
}
public class MultiAdapter<T extends ItemType,VH extends MyViewHolder> extends BackQuickAdapter<T,VH>{

    public MultiAdapter(List<T> datas) {
        super(datas);
    }

    @Override
    View onCreateDefViewHolder(ViewGroup parent, int viewType) {
        return LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false);
    }
    @Override
void onBindDefViewHolder(VH holder, int realDataPos) {
    View contentView = holder.itemView;
    String args = "自定义数据类型:"+String.valueOf(datas.get(realDataPos).getItemType());
    ((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(args);
}
    @Override
    protected int getDefItemViewType(int realDataPos) {
        return datas.get(realDataPos).getItemType();
    }
}

这样就可以在外部自定义数据类型了

添加分组

首先我们看BRAVH怎么介绍

  1. 实体数据集类型要继承SectionEntity

Stop,我们到这边似乎不必要去看他怎么实现了,我猜原理应该和上面添加自定义类型是一样的,上面更加广泛,所以这里我们只有2种类型而已,一种分组头类型。一种分组内容类型。
我们开始编写代码试试:

  1. 写一个数据集类型实现ItemType接口
  2. 数据集返回2种类型,第一种是分组头类型假定(GroupHeader),第二种是一种分组内容类型(GroupContent)
  3. 编写我们的Adapter传入参数类型为1的类型

我们先写一个数据集类型实现ItemType接口

public class GroupEntity implements ItemType{
    public final static int GROUPHEADER = 0X11;
    public final static int GROUPCONTENT = 0X12;

    private boolean isHeader = false;
    private String values;

    public GroupEntity(boolean isHeader, String values) {
        this.isHeader = isHeader;
        this.values = values;
    }
    @Override
    public int getItemType() {
        return isHeader ? GROUPHEADER : GROUPCONTENT;
    }
}

这样添加分组就是2种自定义数据类型而已,我们可以写一个实体类,返回2种类型,使用MultiAdapter适配,实现方法:

  • onCreateDefViewHolder:根据viewType来inflate不同的layout的子view
  • onBindDefViewHolder:根据Viewholder的VH holder, int position来绑定itemview与数据

分组的伸缩栏

既然要实现分组,我的思路是这样的

  1. 是否需要提供不同的itemtype,然后根据不同itemtype提供不同级别的子View,这样视图倒是没有问题了。
  2. 下面需要思考的是数据,如果想要添加到列表的数据集中,之前我们定义的是T类型,那么不同级别也是要是T类型的才能加入到列表的数据集中
  3. 然后插入notifyitemInserted,删除notifyitemremoved这样就没问题了.

可是设计主实体类或许有些麻烦了,要求里面有个子数据集类型是实体类类型的:
我们先定义接口

public interface Expandable<E> extends ItemType{
    List<E> subItems();
    boolean isExpandable();
    void setExpand(boolean expand);
    boolean isExpand();
}

这个接口要求子数据级,是否可以扩展

然后定义数据类型

public class ExpandEntity implements Expandable<ExpandEntity>{

    public final static int SUB1 = 0x111;
    public final static int SUB2 = 0x112;
//    public final static int SUB3 = 0x113;

    private int itemType;
    private List<ExpandEntity> subDatas;
    private String values;

    public ExpandEntity(int itemType, List<ExpandEntity> subDatas,String values) {
        this.itemType = itemType;
        this.subDatas = subDatas;
        this.values = values;
    }

    public String getValues() {
        return values;
    }

    @Override
    public int getItemType() {
        return itemType;
    }

    @Override
    public List<ExpandEntity> subItems() {
        return subDatas;
    }

    @Override
    public boolean isExpandable() {
        return subDatas != null && subDatas.size() > 0;
    }

    boolean isExpand = false;
    @Override
    public void setExpand(boolean expand) {
        isExpand = expand;
    }

    @Override
    public boolean isExpand() {
        return isExpand;
    }
}

这样就可以实现该接口,外部类自由继承,然后自由添加values属性

下面我们的接口实体类好了,需要制作adapter,可是adapter怎么写呢?我们需要传入的数据类型为Expandable类型,而且数据集合类型要是Expandable的实现类,是否可以这样写?


class ExpandAdapter<I extends Expandable<I>,VH extends MyViewHolder> extends BackQuickAdapter<I,VH>


这样保证数据集市I类型,I又是Expandable类型,这样可以遍历I的子数据集实现多级的展开与隐藏

Adapter具体实现该怎样呢?
我们可以在onBindDefViewHolder方法里面添加itemview的点击事件,然后为itemview添加tag,tag里面是绑定的数据,再实现点击事件的时候取出tag里面数据,判断单项是否可以展开,如果可以展开,得到子数据集放入主数据集中,然后notifyItemRangeInserted就能实现数据的多级展开

void onBindDefViewHolder(VH holder, int realDataPos) {
    ExpandEntity entity = (ExpandEntity) datas.get(realDataPos);
    ((TextView)holder.itemView.findViewById(R.id.recycler_list_txt)).setText(entity.getValues());
    holder.itemView.setTag(datas.get(realDataPos));
    holder.itemView.setOnClickListener(new ClickDelegate(realDataPos));
}

private class ClickDelegate implements View.OnClickListener{
    //当前位置之后插入+headercount
    private int position = 0;

    public ClickDelegate(int position) {
        this.position = position;
    }
    @Override
    public void onClick(View v) {
        Expandable<I> raw = (Expandable<I>) v.getTag();
        if(raw.isExpandable() && !raw.isExpand()){
            //toggle this subItem
            //expand or compose
            datas.addAll(position+1,raw.subItems());
            //from start +2 to datas.size count number
            notifyItemRangeInserted(position + 1 + 1, raw.subItems().size());
            raw.setExpand(true);
            Log.i("itemclick","expand");
        }else if(raw.isExpand()){
            //这里需要折叠,去除datas中间的数据,多级遍历删除所有当前项的子项
            Log.i("itemclick","compose");
        }
    }
}

注意这里移除操作,去除datas中间的数据,实现折叠效果,然后notifyItemRangeMoved
这部分的逻辑也是对主数据集datas操作,这里没有具体实现,有心的小伙伴可以看BRAVH的expand与collapse方法,人家的折叠可是多级折叠的,将所有子集都移除然后notifyItemRangeMoved。这里我就不写这部分逻辑代码了。


我是分割线
这里我们写Adapter需要涉及到一个泛型的实例化,因为需要将View加入Viewholder,并把Viewholder实例化,而Viewholder又是VH类型的,所以下面方法我直接贴上代码

/**
     * 抽象类反射实例化
     * @param view
     * @return
     */
    protected VH createViewHolder(View view) {
        Class temp = getClass();
        Class z = null;
        while (z == null && null != temp) {
            z = getInstancedGenericKClass(temp);
            temp = temp.getSuperclass();
        }
        VH k;
        // 泛型擦除会导致z为null
        if (z == null) {
            k = (VH) new MyViewHolder(view);
        } else {
            k = createGenericKInstance(z, view);
        }
        return k != null ? k : (VH) new MyViewHolder(view);
    }
    /**
     * try to create Generic K instance
     *
     * @param z
     * @param view
     * @return
     */
    @SuppressWarnings("unchecked")
    private VH createGenericKInstance(Class z, View view) {
        try {
            Constructor constructor;
            // inner and unstatic class
            if (z.isMemberClass() && !Modifier.isStatic(z.getModifiers())) {
                constructor = z.getDeclaredConstructor(getClass(), View.class);
                constructor.setAccessible(true);
                return (VH) constructor.newInstance(this, view);
            } else {
                constructor = z.getDeclaredConstructor(View.class);
                constructor.setAccessible(true);
                return (VH) constructor.newInstance(view);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * get generic parameter K
     *
     * @param z
     * @return
     */
    private static Class getInstancedGenericKClass(Class z) {
        Type type = z.getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            Type[] types = ((ParameterizedType) type).getActualTypeArguments();
            for (Type temp : types) {
                if (temp instanceof Class) {
                    Class tempClass = (Class) temp;
                    if (MyViewHolder.class.isAssignableFrom(tempClass)) {
                        return tempClass;
                    }
                }
            }
        }
        return null;
    }

这样泛型的实例化就完成了~


又到总结时间啦。我们模拟BRAVH能够学到什么?

我们在写Adapter的时候。将数据集类型以泛型的形式传入。
在Adapter中抽象出onBindViewHolder,onCreateViewHolder,除了处理预置的类型,比如头布局,尾布局,空布局,loading布局。其他的都需要自定义ItemType数据类型来实现定制view。
扩展与折叠也是属于自定义数据类型中的一种,但是要求传入的数据集类型T中还有T类型的子集合,用来得到子集,这样可以保持与Adapter
的数据类型一直,用于展开删除其实就是对Adapter中的数据集datas插入与删除然后通知刷新而已
拖拽与滑动删除默认ItemTouhHelper,在onmoved与onswipe中交换数据集中的位置或者删除某个位置来通知刷新

Source源代码

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

推荐阅读更多精彩内容