一起撸个朋友圈吧(step5) - 控件篇(评论popup下+交互事件结构)

项目地址:https://github.com/razerdp/FriendCircle
一起撸个朋友圈吧这是本文所处文集,所有更新都会在这个文集里面哦,欢迎关注

上篇链接:http://www.jianshu.com/p/15a9fe8f917f
下篇链接:http://www.jianshu.com/p/58894dfb3f09

本篇图文较多,流量党请慎重


再开始之前,羽翼君想说一件事情,目前我还是一个学生,没什么能力买一个牛逼的服务器,仅仅是学生价租的阿里云,但是,因为为了方便,我把服务器的IP都写在了我们的项目里面,结果他喵的昨晚被攻击了!

这个服务器根本没有什么利用价值,即使是用来做肉鸡,根本没法塞牙缝好么。虽然花的钱不多,也仅仅是为了这个项目和我的毕业设计方便而租的服务器。

当然,这也是我的错,我不应该直接把服务器地址贴上的,这也是我的锅,所以在push的时候我把地址换成了我的吐槽。。。

如果您需要测试数据,您可以简信我或者加我的QQ来拿到地址,非常抱歉我这么做。(我很害怕到毕业设计答辩那天来个攻击啊)
【END】


在上篇,我们初步完成了评论popup的展示,这一次我们需要补全剩下的交互代码。

首先上预览图吧(为了方便,以后统一在电脑模拟器上录制):

preview

<h1 id="step1">Step 1:困境</h1>


在实现之前,不妨看看我们现在遇到的问题:

如下图:

结构图

从图中我们可以看到,我们现在整一个朋友圈的实现方案如下:

  • Activity作为一个controller,它现在仅仅负责的是拉取数据,并没有其他的工作。
  • Adapter,在我们将viewholder抽象出来后,adapter看起来仅仅就是将类型跟对应的viewholder匹配起来,并渲染出来。
  • 而ViewHolder,则是负责将数据展示,同时一切的数据/操作都是在ViewHolder实现的。

那么问题来了,我们的工程进行到这里,我们其实没有做任何的点击/请求(朋友圈列表拉取除外)而如今,我们需要增加交互等方法,按照图中的结构,我们可以有如下的方法(目前我所想到的):

  • 因为viewholder持有activity的context,我们可以通过activity提供公用方法,然后使用(if context instance of xxx){ (Activity)context.xxxx}来调用activity的方法
  • EventBus事件通知
  • 中间类,使用中间类来处理activity与viewholder之间的交互。

显然,方法一过于笨重不便于扩展,方法二虽然挺方便的,但是在onEventMainThread方法里我们需要很多的判断,所以我们使用方法三。

那么这个中间类是干什么的呢?直观的说,就是如下图这样的结构:

中间层

可能看图还是有点不太明白,那我们通过一个例子来解释一下吧:

假如故事发生在一个初创公司,这个公司目前的分层如下:

  • BOSS(对应Activity
  • 技术总监CTO(对应Adapter
  • 具体各个技术小组的leader(对应各个ViewHolder

在开始阶段因为急需要做出产品给投资人看效果,所以并没有招到很多人,因此一直都是Boss下发需求给CTO,然后CTO评估后再下发给技术小组的leader,然后leader完成需求。
类比于我们撸朋友圈目前进度:先完成界面展示,而不管任何交互)*

在前期,这样做问题不大,OK,这个初创公司顺利的拿下A轮投资,接下来B轮就需要打造产品特点和细节研磨,这时候就会发现,如果还是按照之前的做法(boss->cto->leader->产品生产),效率大大的降低,同时因为leader忙着忙那,同时应对着boss变来变去的需求,在自己负责的区域应对着一波又一波的需求忙得焦头烂额。(viewholder与activity耦合度过高)

于是,他们决定请人。经过简历筛选,笔试,面试后,他们找到了合适的人选,于是接下来的分工就变成了这样:

  • boss下发需求(此时其实应该是boss跟产品评估,但为了篇幅,先略过产品)(Activity通知adapter更新)
  • CTO开评估会议并细分/下发任务(Adapter将数据分发到各个viewholder并渲染)
  • leader收到任务,开内部会议进行分工,而leader则是负责项目结构,项目基础框架的优化等(viewholder绑定数据并展示)
    • 各个小组成员收到任务,开始投入生产(码代码)(controll处理各种交互,请求等事件)

然后当各个小组成员完成任务,交由给leader,leader进行review后交由测试,测试通过后可以通知可以准备发版。在各个高层使用过初步满意后正式发版。(在我们的代码里,省略那么多步骤,直接通知activity进行更新)

说了那么多,其实就是一句话:controller承担了最繁琐的步骤,做好后通知activity去更新数据。

<h1 id="step2">Step 2:controller的实现</h1>


在一大篇无聊的叙述后,我们就谈谈如何实现controller。谈起controller,就不得不想到MVC,进而想到MVP。我们这里并非实现MVP,但总的来说,有点形似而神不似吧。

首先既然要做解耦,那就必须涉及到抽象,而抽象,就我经验来说,接口化应该是最好的。

所以我们先抽象出一个BaseController(注:此接口不遵循单一职责原则):

/**
 * Created by 大灯泡 on 2016/3/9.
 * 控制器接口化
 */
public interface BaseDynamicController {
    // 点赞
    void addPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);
    // 取消点赞
    void cancelPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);

}

我们需要的操作都将会在接口里限定。
关于@RequestType.DynamicRequestType,一般而言,在需要传入一定范围内的值时,我们应该使用注解限定,这样可以降低误操作。另外RequestType用于区分同一个类下多个请求。

所以这里限定传入的值必须是DynamicRequestType所支持的值:

public class RequestType {

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ADD_PRAISE,CANCEL_PRAISE})
    public @interface DynamicRequestType{}
    // 点赞
    public static final int ADD_PRAISE=0x10;
    public static final int CANCEL_PRAISE=0x11;
}

目前我们只做了点赞和取消点赞,所以暂时只需要这两个类型。

接口写完后,我们接下来需要实现这个接口,定义一个类DynamicController并实现请求回调接口以及刚刚我们定义的controller接口:

/**
 * Created by 大灯泡 on 2016/3/8.
 * 事件控制器
 * 本控制器用于BaseItemDelegate的事件处理
 * 事件处理完成通过callback回调给activity,避免BaseItem与activity耦合度过高
 */
public class DynamicController implements BaseResponseListener, BaseDynamicController {
    private static final String TAG = "DynamicController";
    private CallBack mCallBack;
    private Activity mContext;
    //=============================================================request
    private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
    private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;

    public DynamicController(Activity context, @NonNull CallBack callBack) {
        mContext = context;
        mCallBack = callBack;
    }

    //=============================================================request callback
    @Override
    public void onStart(BaseResponse response) {

    }

    @Override
    public void onStop(BaseResponse response) {

    }

    @Override
    public void onFailure(BaseResponse response) {

    }

    @Override
    public void onSuccess(BaseResponse response) {
    
    }

    //=============================================================controller methods
    @Override
    public void addPraise(long userid, long dynamicid, MomentsInfo info,
                          @RequestType.DynamicRequestType int requesttype) {

    }

    @Override
    public void cancelPraise(long userid, long dynamicid, MomentsInfo info,
                             @RequestType.DynamicRequestType int requesttype) {

    }
    //=============================================================destroy
    public void destroyController() {
        
    }
    public interface CallBack {
        void onResultCallBack(BaseResponse response);
    }
}

在处理完成后,我们需要通知activity进行数据更新,所以我们需要定一个CallBack,让activity实现这个接口。同时为了紧张的内存,我们还需要定义一个destroy方法,及时的进行对象置空。

接下来改造一下我们的BaseItemDelegate,因为我们当初设计的时候是采取接口的形式,所以我们实质上是改造BaseItemView

public interface BaseItemView<T> {
...
    void setController(BaseDynamicController controller);
    BaseDynamicController getController();
}

我们在BaseItemView添加controller的setter/getter,然后在BaseItemDelegate进行赋值,最后就到我们的Adapter进行设置:
CircleBaseAdapter.java:

    //因为我们当初设计的时候采用的builder模式,所以我们仅仅需要到builder添加一个参数就好了,这里就不贴代码了。
    public CircleBaseAdapter(Activity context, Builder<T> mBuilder) {
      ...
        mDynamicController=mBuilder.mDynamicController;
    }
...
      @Override
    public View getView(int position, View convertView, ViewGroup parent) {
       ...
        view.setActivityContext(context);
        view.onFindView(convertView);
        view.onBindData(position, convertView, getItem(position), dynamicType);
        if (view.getController()==null)view.setController(mDynamicController);

        return convertView;
    }

因为重复的代码在之前的简书都有记录,所以这里就略过了,如果您看的云里雾里,可以在这篇文章看到所有的解析。

在viewholder和controller完成后,我们最后需要在activity将controller给new出来,然后添加到builder里面,使adapter,activity共同持有一个对象。

public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicController.CallBack {
    private FriendCircleRequest mCircleRequest;

    private DynamicController mDynamicController;

    // 方案二,预留
 /*   @Override
    protected void onEventMainThread(Events events) {
        if (events == null || events.getEvent() == null) return;
        if (events.getEvent() instanceof Events.CallToRefresh) {
            if (((Events.CallToRefresh) events.getEvent()).needRefresh) mCircleRequest.execute();
        }
    }*/

    @Override
    protected void onCreate(Bundle savedInstanceState) {
     ...
        bindListView(R.id.listview, header,
                FriendCircleAdapterUtil.getAdapter(this, mMomentsInfos, mDynamicController));
        initReq();
        //mListView.manualRefresh();
    }
...

    @Override
    public void onResultCallBack(BaseResponse response) {
        
    }
...
    }

其中FriendCircleAdapterUtil的代码略过,详情可以看GitHub。

到这里为止,我们的结构大致完成,接下来就是将剩下的代码补全。

<h1 id="step3">Step 3:代码补全</h1>


首先我们思考一下,如何更新我们的界面是最好的。目前来说我想到有以下方法:

  • 当点赞/取消点赞请求成功后,我们调用activity的列表请求将整个朋友圈数据都重新拉一遍。
  • 当点赞/取消点赞请求成功后,服务器返回当前动态的点赞列表信息,我们获取当前动态的实体类,解析服务器返回信息后进行更新操作。
  • 当点赞/取消点赞请求成功后,本地进行插入/删除。

上面几个方案中
第一个方案很明显是不适合的,因为每次点赞都需要将整个列表拉一次,解析耗时不说,就流量消耗也是很可观的。遂放弃。

第二个方案目前采用,但也有不完善的地方,这个下文再说。

第三个方案看似不错,但如果遇到下面这种情况就不适应了:你点赞的同时你好友也点赞,但因为没有数据返回,所以你好友的点赞并没有刷出来。

综上所述,目前采取第二个方案。(ps:第二个方案目前我的实现并不好,但暂时没有想到更好的方案,如果您有好的建议,在下衷心希望可以留下您的评论

首先回到我们的controller中,在CallBack里我们传递的参数是BaseResponse,但BaseResponse当初我们设计的时候,其接受的数据如下:

public class BaseResponse {
    //请求码
    private int status;
    //错误码
    private int errorCode;
    //请求类型,用于单activity多个请求的区分
    private int requestType;
    //请求回来的JSON字符串
    private String jsonStr;
    //错误信息
    private String errorMsg;
    //待用,可以存放解析后的JSON Array
    private ArrayList<Object> datas=new ArrayList<>();
    //存放解析后的数据
    private Object data;
    //是否展示dialog
    private boolean showDialog;

    private int start;
    private boolean hasMore;
...
}

可以看到,我们存放数据的地方仅仅只有一个Object,但我们需要拿到一个动态实体和点赞后服务器返回的数据解析。当然,我们可以选择在BaseResponse再添加一个对象来存放,但如果这样做,就会导致以后这个类也许会越来越臃肿。而这并不是我所希望看到的。

所以,我们需要开辟一个新的用于controller的实体:

/**
 * Created by 大灯泡 on 2016/3/10.
 * 控制器实体类
 */
public class DynamicControllerEntity<T> {
    private MomentsInfo mMomentsInfo;
    private T data;

    public MomentsInfo getMomentsInfo() {
        return mMomentsInfo;
    }
    public void setMomentsInfo(MomentsInfo momentsInfo) {
        mMomentsInfo = momentsInfo;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

做好这个之后,我们补全controller的内容:

/**
 * Created by 大灯泡 on 2016/3/8.
 * 事件控制器
 * 本控制器用于BaseItemDelegate的事件处理
 * 事件处理完成通过callback回调给activity,避免BaseItem与activity耦合度过高
 */
public class DynamicController implements BaseResponseListener, BaseDynamicController {
    private static final String TAG = "DynamicController";
    private CallBack mCallBack;
    private Activity mContext;
    //=============================================================request
    private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
    private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;

    public DynamicController(Activity context, @NonNull CallBack callBack) {
        mContext = context;
        mCallBack = callBack;
    }

    //=============================================================request callback
...

    @Override
    public void onSuccess(BaseResponse response) {
        if (response.getStatus() == 200) {
            if (mCallBack != null) mCallBack.onResultCallBack(response);
        }
        else {
            ToastUtils.ToastMessage(mContext, response.getErrorMsg());
        }
    }

    //=============================================================controller methods
    @Override
    public void addPraise(long userid, long dynamicid, MomentsInfo info,
                          @RequestType.DynamicRequestType int requesttype) {
        if (mDynamicAddPraiseRequest == null) {
            mDynamicAddPraiseRequest = new DynamicAddPraiseRequest(info);
            mDynamicAddPraiseRequest.setOnResponseListener(this);
            mDynamicAddPraiseRequest.setRequestType(requesttype);
        }
        mDynamicAddPraiseRequest.setInfo(info);
        mDynamicAddPraiseRequest.userid = userid;
        mDynamicAddPraiseRequest.dynamicid = dynamicid;
        mDynamicAddPraiseRequest.execute();
    }

    @Override
    public void cancelPraise(long userid, long dynamicid, MomentsInfo info,
                             @RequestType.DynamicRequestType int requesttype) {
        if (mDynamicCancelPraiseRequest == null) {
            mDynamicCancelPraiseRequest = new DynamicCancelPraiseRequest(info);
            mDynamicCancelPraiseRequest.setOnResponseListener(this);
            mDynamicCancelPraiseRequest.setRequestType(requesttype);
        }
        mDynamicCancelPraiseRequest.setInfo(info);
        mDynamicCancelPraiseRequest.userid = userid;
        mDynamicCancelPraiseRequest.dynamicid = dynamicid;
        mDynamicCancelPraiseRequest.execute();
    }
    //=============================================================destroy
    public void destroyController() {
        mDynamicAddPraiseRequest = null;
        mCallBack = null;
    }
    public interface CallBack {
        void onResultCallBack(BaseResponse response);
    }
}

当我们请求成功后,才调用的activity回调方法,所以我们在activity处理的时候必定是成功后的事件。

接下来在我们的请求里做对应的操作:

public class DynamicAddPraiseRequest extends BaseHttpRequestClient {

    public long userid;
    public long dynamicid;
    private MomentsInfo mInfo;

    public DynamicAddPraiseRequest(MomentsInfo info) {
        mInfo = info;
    }

    public MomentsInfo getInfo() {
        return mInfo;
    }

    public void setInfo(MomentsInfo info) {
        mInfo = info;
    }

    @Override
    public String setUrl() {
        return new RequestUrlUtils.Builder().setHost(FriendCircleApp.getRootUrl())
                                            .setPath("/dynamic/addpraise/")
                                            .addParam("userid", userid)
                                            .addParam("dynamicid", dynamicid)
                                            .build();
    }

    @Override
    public void parseResponse(BaseResponse response, JSONObject json, int start, boolean hasMore) throws JSONException {
        if (response.getStatus()==200){
            DynamicControllerEntity<List<UserInfo>> entity=new DynamicControllerEntity();
            entity.setMomentsInfo(mInfo);
            List<UserInfo> praiseList= JSONUtil.toList(json.optString("data"),new TypeToken<ArrayList<UserInfo>>(){}
                    .getType
                    ());
            entity.setData(praiseList);
            response.setData(entity);
        }

    }
}

其中RequestUrlUtils是我为了方便写url而写的一个工具类,这里就不展示了。

最后,我们补全activity的回调以及BaseItemDelegate的点击事件处理:

activity:

 @Override
    public void onResultCallBack(BaseResponse response) {
        // 通知更新
        switch (response.getRequestType()) {
            case RequestType.ADD_PRAISE:
                DynamicControllerEntity<List<UserInfo>> entity
                        = (DynamicControllerEntity<List<UserInfo>>) response.getData();
                MomentsInfo info = entity.getMomentsInfo();
                info.dynamicInfo.praiseState=CommonValue.HAS_PRAISE;
                if (info != null) {
                    if (info.praiseList != null) {
                        info.praiseList.clear();
                        info.praiseList.addAll(entity.getData());
                    }else {
                        info.praiseList=entity.getData();
                    }
                }
                mAdapter.notifyDataSetChanged();
                break;
            case RequestType.CANCEL_PRAISE:
                DynamicControllerEntity<List<UserInfo>> cancelEntity
                        = (DynamicControllerEntity<List<UserInfo>>) response.getData();
                MomentsInfo mInfo = cancelEntity.getMomentsInfo();
                mInfo.dynamicInfo.praiseState=CommonValue.NOT_PRAISE;
                if (mInfo != null) {
                    if (mInfo.praiseList != null) {
                        mInfo.praiseList.clear();
                        mInfo.praiseList.addAll(cancelEntity.getData());
                    }else {
                        mInfo.praiseList=cancelEntity.getData();
                    }
                }
                mAdapter.notifyDataSetChanged();
                break;
        }
    }

BaseItemDelegate:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            // 评论按钮
            case R.id.comment_button:
                if (mInfo == null) return;
                mCommentPopup.setDynamicInfo(mInfo.dynamicInfo);
                mCommentPopup.setOnCommentPopupClickListener(new CommentPopup.OnCommentPopupClickListener() {
                    @Override
                    public void onLikeClick(View v, DynamicInfo info) {
                        if (mDynamicController != null) {
                            switch (info.praiseState) {
                                case CommonValue.NOT_PRAISE:
                                    mDynamicController.addPraise(LocalHostInfo.INSTANCE.getHostId(), info.dynamicId,
                                            mInfo, RequestType.ADD_PRAISE);
                                    break;
                                case CommonValue.HAS_PRAISE:
                                    mDynamicController.cancelPraise(LocalHostInfo.INSTANCE.getHostId(), info.dynamicId,
                                            mInfo, RequestType.CANCEL_PRAISE);
                                    break;
                                default:
                                    break;
                            }
                        }
                    }

                    @Override
                    public void onCommentClick(View v, DynamicInfo info) {

                    }
                });
                mCommentPopup.showPopupWindow(commentImage);
                break;
            default:
                break;
        }
    }

至此,我们的controller中间层实现完成,当然,我觉得这样实现并不太好,原因如下:

  • 在CallBack中,我们把MomentsInfo给暴露了,如果出现误操作,导致的就是朋友圈内容显示的问题
  • 中间层依赖Request,原因在于MomentsInfo的传值方法,通过request传,这并不太好,因为request理论上应该仅仅负责请求和解析,不应该作为信使。

以上两点在以后我希望等我的水平提高后可以解决甚至重构。如果您有好的建议,希望能在评论区留下脚印或者GitHub提交PR。

<h1 id="step4">Step 4:Popup的补充</h1>


popup在上一篇文章中已经是初步实现了,本篇仅仅针对一些内容进行补充:

我们的评论popup有两个功能:

  • 点赞
  • 评论

也就是说有两个按钮,但我们不应该把事件都放到popup类里面完成,这会导致耦合度问题(事件处理必定需要viewholder里面的数据,如果在popup里面完成,意味着需要跟viewholder相互依赖),因此,我们采取接口,将点击动作抛出去给viewholder自己处理。

首先我们定义一个接口:

    public interface OnCommentPopupClickListener {
        void onLikeClick(View v, DynamicInfo info);

        void onCommentClick(View v, DynamicInfo info);
    }

因为点赞和评论都涉及到数据库对动态id的CRUD操作,所以我们直接传入DynamicInfo(DynamicInfo和接口的setter/getter略)。

然后我们在popup里面实现onClickListener:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.item_like:
                if (mOnCommentPopupClickListener != null) {
                    mOnCommentPopupClickListener.onLikeClick(v, mDynamicInfo);
                    mLikeView.clearAnimation();
                    mLikeView.startAnimation(mScaleAnimation);
                }
                break;
            case R.id.item_comment:
                if (mOnCommentPopupClickListener != null) {
                    mOnCommentPopupClickListener.onCommentClick(v, mDynamicInfo);
                    dismiss();
                }
                break;
        }
    }

最后外部viewholder实现接口(见Step 3最后)

事件处理解决后,第二个问题,我们可以看到朋友圈点赞的心心是有一个动画效果的,简单的描述就是:心心放大,然后缩小。

要实现这个效果可以说很简单:给两个Animation,在第一个结束的onAnimationEnd调用第二个Animation不就行了么?

是的,这样是非常简单,也十分明了。但,这样做就需要两个Animation对象,对于内存十分看紧的我,决定使用一个对象完成。

那么,要使用一个Animation完成放大后缩小的效果,就不得不提到插值器这个东东了。

插值器简单的说,就是改变动画的不同时间的值,从而改变动画的变化率。

这里推荐一个网站,这个网站可以将公式可视化为插值器曲线:
http://inloop.github.io/interpolator/

那么要实现先放大后缩小,我们的插值器曲线必定是先上升后下降,这时候很容易想到一个初中学过的东西:三角函数

sin函数在一个周期内有两个峰值,±1,而我们取半个周期就可以得到一条先升后降的曲线了。

如果可视化

效果如下图:

interpolator

不好意思,因为太好玩了,所以多玩了一会。。。。

在代码上,我们只需要继承LinearInterpolator然后重写getInterpolation就可以了。

    static class SpringInterPolator extends LinearInterpolator {

        public SpringInterPolator() {
        }

        @Override
        public float getInterpolation(float input) {
            return (float) Math.sin(input*Math.PI);
        }
    }

在动画setInterpolator的时候使用SpringInterPolator即可。

剩下的代码也就不贴了。

下一篇我们完成评论区的事件交互。

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

推荐阅读更多精彩内容