回炉再造,灵活的YMenuView2.0诞生

出处
炎之铠邮箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原创,转载请注明本出处!
本项目GitHub地址:https://github.com/totond/YMenuView
欢迎 Star or Fork!

前言

之前把我项目用到的类似于PathView的菜单YMenuView抽离出来,分享了实现思路,但是只是实现了在右下角的几种菜单弹出收回的效果,限制太大,适用性不强。这次重温了一下设计模式之后把YMenuView回炉再造,重构代码,打造出一个可以让用户发挥自己自由想象,实现自己想法需求的灵活YMenuView2.0,还加了3个自定义YMenu的例子,先来看看看效果:


(前面几个是之前YMenuView1.x的效果,后面是新加的)

介绍

YMenuView是包含着一个MenuButton和若干个OptionButton的RelativeLayout,具体实现原理前一篇已经有介绍了,所以这篇文章主要说明YMenuView2.0的改变(使用方法的话可以直接看Github。YMenuView2.0重构代码之后,最大的改变就是把YMenuView的大部分逻辑抽离出来,留下4个主要的抽象方法,可以让用户自定义,这4个分别是决定:MenuButton的位置、OptionButton的位置、OptionButton的显示动画和OptionButton的消失动画。

重构过程

抽象类YMenu

YMenuView2.0的主角不再是YMenuView类,现在的它只是一个小弟(子类),真正的大哥现在叫YMenu(父类)
  最近正在学习Animation的源码,得知Animation是一个抽象类,然后其他具体的动画时通过继承它,然后重写applyTransformation()方法来实现具体的动画需求,这是模板方法模式的应用(其实Activity也这样),所以YMenuView也参考这种模式,把一些次要的逻辑抽象出来,放都在抽象父类YMenu里面,开放出4个抽象方法给用户可以通过继承来自定义YMenu:

    /**
     * 设置MenuButton的位置,重写该方法进行自定义设置
     *
     * @param menuButton 传入传入MenuButton,此时它的宽高位置属性还未设置,需要在此方法设置。
     */
    public abstract void setMenuPosition(View menuButton);

    /**
     * 设置OptionButton的位置,重写该方法进行自定义设置
     *
     * @param optionButton 传入OptionButton,此时它的宽高位置属性还未设置,需要在此方法设置。
     * @param menuButton   传入MenuButton,此时它已经初始化完毕,可以利用。
     * @param index        传入的是该OptionButton的索引,用于区分不同OptionButton。
     */
    public abstract void setOptionPosition(OptionButton optionButton, View menuButton, int index);

    /**
     * 设置OptionButton的显示动画,重写该方法进行自定义设置
     *
     * @param optionButton 传入了该动画所属的OptionButton,此时它的宽高位置属性已初始化完毕,可以利用。
     * @param index        传入的是该OptionButton的索引,用于区分不同OptionButton。
     * @return             返回的是创建好的动画                    
     */
    public abstract Animation createOptionShowAnimation(OptionButton optionButton, int index);


    /**
     * 设置OptionButton的消失动画,重写该方法进行自定义设置
     *
     * @param optionButton 传入了该动画所属的OptionButton,此时它的宽高位置属性已初始化完毕,可以利用。
     * @param index        传入的是该OptionButton的索引,用于区分不同OptionButton。
     * @return             返回的是创建好的动画
     */
    public abstract Animation createOptionDisappearAnimation(OptionButton optionButton, int index);

YMenu有4个具体实现类(效果都在上面的gif图里展现过了):

抽象方法的实现

这4个方法抽象出来之后,由子类实现功能,下面介绍如何实现(以下的4个方法介绍的代码是在YMenuView里面的):

1.setMenuPosition()方法

此方法的实现决定了MenuButton的位置,MenuButton就是上面效果图那个齿轮,点击之后会有选项出现消失的动作。在上一篇YMenuView源码分析里面就已经写到,它的位置是用Relative的LayoutParams来设置的(和上一篇有点不同就是因为是从抽象父类获取属性,所以要用一些get方法,而且我还重命名了一些属性,具体可以在Github的使用介绍属性表查看):

    @Override
    public void setOptionPosition(OptionButton optionButton, View menuButton, int index){

        LayoutParams layoutParams = new LayoutParams(getYMenuButtonWidth(), getYMenuButtonHeight());
        layoutParams.setMarginEnd(getYMenuToParentXMargin());
        layoutParams.bottomMargin = getYMenuToParentYMargin();
        layoutParams.addRule(ALIGN_PARENT_RIGHT);
        layoutParams.addRule(ALIGN_PARENT_BOTTOM);

        menuButton.setLayoutParams(layoutParams);
    }

但是这样使用LayoutParams的话理解起来很不直观(其实熟悉RelativeLayout的还好),如果让用户自定义的话会有点麻烦,所以我把这个过程参考建造者模式封装成了一个类MenuPositionBuilder,通过它来实现这个位置的设置过程。

    @Override
    public void setMenuPosition(View menuButton){
        new MenuPositionBuilder(menuButton)
                //设置宽高
                .setWidthAndHeight(getYMenuButtonWidth(), getYMenuButtonHeight())
                //设置参考方向
                .setMarginOrientation(PositionBuilder.MARGIN_RIGHT,PositionBuilder.MARGIN_BOTTOM)
                //设置是否在XY方向处于中心
                .setIsXYCenter(false,false)
                //设置XY方向的参考,如果设置了MARGIN_LEFT和MARGIN_TOP,那么XMargin和YMargin就是与参照物左边界和上边界的距离
                .setXYMargin(getYMenuToParentXMargin(),getYMenuToParentYMargin())
                //最后确认时候调用
                .finish();
    }

这样的话感觉就清晰一些了,当然有人如果熟悉了RelativeLayout参数的话,可能会觉得比原来复杂了,也可以用上面的写法,这两者是等价的。至于MenuPositionBuilder的实现过程,运用的建造者模式,比较简单,由于篇幅原因这里就不写了,有兴趣的话可以直接到项目Github地址上查看源码。

2.setOptionPosition()方法

此方法的实现决定了那些选项OptionButton的位置,这里是YMenuView的实现:

    @Override
    public void setOptionPosition(OptionButton optionButton, View menuButton, int index){
        //设置动画模式和时长
        optionButton.setSD_Animation(getOptionSD_AnimationMode());
        optionButton.setDuration(getOptionSD_AnimationDuration());

        //计算OptionButton的位置
        int position = index % getOptionColumns();

        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                getYOptionButtonWidth()
                , getYOptionButtonHeight());

        layoutParams.rightMargin = getYOptionToParentXMargin()
                + getYOptionXMargin() * position
                + getYOptionButtonWidth() * position;

        layoutParams.bottomMargin = getYOptionToParentYMargin()
                + (getYOptionButtonHeight() + getYOptionYMargin())
                * (index / getOptionColumns());
        layoutParams.addRule(ALIGN_PARENT_BOTTOM);
        layoutParams.addRule(ALIGN_PARENT_RIGHT);

        optionButton.setLayoutParams(layoutParams);
    }

和MenuButton一样,这个方法也有一个专属的OptionPositionBuilder,下面使用它来设置:

    @Override
    public void setOptionPosition(OptionButton optionButton, View menuButton, int index){
        //设置动画模式和时长
        optionButton.setSD_Animation(getOptionSD_AnimationMode());
        optionButton.setDuration(getOptionSD_AnimationDuration());

        //计算OptionButton的位置
        int position = index % getOptionColumns();

        new OptionPositionBuilder(optionButton,menuButton)
                //设置宽高
                .setWidthAndHeight(getYOptionButtonWidth(), getYOptionButtonHeight())
                //设置在XY方向是否以MenuButton作为参照物
                .isAlignMenuButton(false,false)
                //设置参考方向
                .setMarginOrientation(PositionBuilder.MARGIN_RIGHT,PositionBuilder.MARGIN_BOTTOM)
                //设置XY方向的距离,如果设置了MARGIN_LEFT和MARGIN_TOP,那么XMargin和YMargin就是与参照物左边界和上边界的距离
                .setXYMargin(
                        getYOptionToParentXMargin()
                                + getYOptionXMargin() * position
                                + getYOptionButtonWidth() * position,
                        getYOptionToParentYMargin()
                                + (getYOptionButtonHeight() + getYOptionYMargin())
                                * (index / getOptionColumns()))
                //最后确认时候调用
                .finish();
    }

这里YMenuView的OptionButton位置具体的实现逻辑上一篇介绍YMenuView1.x的时候已经说过了,这里就不详细说了。

3.createOptionShowAnimation()方法和createOptionDisappearAnimation()方法

这两个方法用来决定OptionButton的出现和消失的动画,原本在YMenuView1.x版本是在OptionButton内部实现,但是为了我们可以通过继承YMenu来实现一个自定义的效果,所以就通过一个回调设计把这两个方法的具体实现放到YMenu里抽象出来了。
  所以,这个方法就是给出一个索引为index的OptionButton的参数,给设置动画作为参考值,然后把设置好的动画返回。

    @Override
    public Animation createOptionShowAnimation(OptionButton optionButton, int index){
        AnimationSet animationSet = new AnimationSet(true);
        AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
        alphaAnimation.setDuration(getOptionSD_AnimationDuration());
        TranslateAnimation translateAnimation = new TranslateAnimation(0,0,0,0);
        switch (optionButton.getmSD_Animation()){
            //从MenuButton的左边移入
            case FROM_BUTTON_LEFT:
                translateAnimation= new TranslateAnimation(getYMenuButton().getX() - optionButton.getRight(),0
                        ,0,0);
                translateAnimation.setDuration(getOptionSD_AnimationDuration());
                break;
            case FROM_RIGHT:
                //从右边缘移入
                translateAnimation= new TranslateAnimation((getWidth() - optionButton.getX()),0,0,0);
                translateAnimation.setDuration(getOptionSD_AnimationDuration());
//                showAnimation.setInterpolator(new OvershootInterpolator(1.3f));
                break;
            case FROM_BUTTON_TOP:
                //从MenuButton的上边缘移入
                translateAnimation= new TranslateAnimation(0,0,
                        getYMenuButton().getY() - optionButton.getBottom(),0);
                translateAnimation.setDuration(getOptionSD_AnimationDuration());
                break;
            case FROM_BOTTOM:
                //从下边缘移入
                translateAnimation = new TranslateAnimation(0,0,getHeight() - optionButton.getY(),0);
                translateAnimation.setDuration(getOptionSD_AnimationDuration());
        }

        animationSet.addAnimation(translateAnimation);
        animationSet.addAnimation(alphaAnimation);
        return animationSet;
    }

这里的逻辑其实和之前的没有变化,也是要注意Animation里面的坐标原点是View的左上角就行了,这里只列出ShowAnimation的实现,至于DisappearAnimation的话就是和这个相反而已,篇幅原因就不列出来了。

4.抽象方法的执行顺序

这4个抽象方法是有执行顺序的,所以后面的方法能用到前面设置好的对象参数:



  这些顺序的具体细节就是:

  • 先初始化MenuButton,让MenuButton经历Measure和Layout过程之后,再初始化OptionButton,这样OptionButton就能以MenuButton的位置信息做为参考。
  • 跟着每一个OptionButton经历Measure和Layout过程之后,它的ShowAnimation和DisappearAnimation又能以它的宽高信息进行初始化。
  • 这样根据不同时期放出了不同抽象方法,让用户可以通过实现这4个方法来实现自定义的YMenu。

Circle8YMenu的实现


  这是一个OptionButton围绕着MenuButton的布局,Option最大数量为8个,MenuButton的位置位于ViewGroup正中间,如果想改变的话可以继承Circle8YMenu,单单重写setMenuPosition()方法就可以了。

    //8个Option位置的x、y乘积因子
    private float[] xyTimes = {0,0.707f,1,0.707f,0,-0.707f,-1,-0.707f};

    //设置MenuButton的位置,这里设置成位于ViewGroup中心
    @Override
    public void setMenuPosition(View menuButton) {
        new MenuPositionBuilder(menuButton)
                .setWidthAndHeight(getYMenuButtonWidth(),getYMenuButtonHeight())
                .setMarginOrientation(PositionBuilder.MARGIN_RIGHT,PositionBuilder.MARGIN_BOTTOM)
                .setIsXYCenter(true,true)
                .setXYMargin(getYMenuToParentXMargin(),getYMenuToParentYMargin())
                .finish();
    }

    //设置OptionButton的位置,这里是设置成圆形围绕着MenuButton
    @Override
    public void setOptionPosition(OptionButton optionButton, View menuButton, int index) {
        if (index >= 8){
            try {
                throw new Exception("Circle8YMenuView的OptionPosition最大数量为8,超过将会发生错误");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        int centerX = menuButton.getLeft() + menuButton.getWidth()/2;
        int centerY = menuButton.getTop() + menuButton.getHeight()/2;
        int halfOptionWidth = getYOptionButtonWidth()/2;
        int halfOptionHeight = getYOptionButtonHeight()/2;
        //利用乘积因子来决定不同位置
        float x = xyTimes[index % 8];
        float y = xyTimes[(index + 6) % 8];

        OptionPositionBuilder OptionPositionBuilder = new OptionPositionBuilder(optionButton,menuButton);
        OptionPositionBuilder
                .isAlignMenuButton(false,false)
                .setWidthAndHeight(getYOptionButtonWidth(), getYOptionButtonHeight())
                .setMarginOrientation(PositionBuilder.MARGIN_LEFT,PositionBuilder.MARGIN_TOP)
                //计算OptionButton的位置
                .setXYMargin(
                        (int)(centerX + x * getYOptionXMargin() - halfOptionWidth)
                        ,(int)(centerY + y * getYOptionXMargin() - halfOptionHeight)
                )
                .finish();
    }

这里设置了MenuButton和OptionButton的位置。MenuButton的位置没什么好说的,直接居中,而关于OptionButton的位置这里用到了乘积因子,我觉得这是一种比较方便的算法,在这里解释一下:把MenuButton的中心看作参考点,以optionXMargin为半径,OptionButton中心到参考点(这些在XML定义的属性到了这里怎么用就看自己了,这里就用optionXMargin),然后x,y乘以optionXMargin就是OptionButton到参考点的x,y方向距离。

以这个0号OptionButton为例,它的x=0,y=-1,然后是以ViewGroup的左和上边缘为参考,所以它就处于MenuButton的正上方相隔optionXMargin的位置。

然后就是动画的实现:

    //设置OptionButton的显示动画
    @Override
    public Animation createOptionShowAnimation(OptionButton optionButton, int index) {
        AnimationSet animationSet = new AnimationSet(true);
        TranslateAnimation translateAnimation= new TranslateAnimation(
                getYMenuButton().getX() - optionButton.getX()
                ,0
                ,getYMenuButton().getY() - optionButton.getY()
                ,0);
        translateAnimation.setDuration(getOptionSD_AnimationDuration());
        AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
        alphaAnimation.setDuration(getOptionSD_AnimationDuration());
        animationSet.addAnimation(alphaAnimation);
        animationSet.addAnimation(translateAnimation);
        //为不同的Option设置延时
        if (index % 2 == 1) {
            animationSet.setStartOffset(getOptionSD_AnimationDuration()/2);
        }
        return animationSet;
    }

    //设置OptionButton的消失动画
    @Override
    public Animation createOptionDisappearAnimation(OptionButton optionButton, int index) {
        AnimationSet animationSet = new AnimationSet(true);
        TranslateAnimation translateAnimation= new TranslateAnimation(
                0
                ,getYMenuButton().getX() - optionButton.getX()
                ,0
                ,getYMenuButton().getY() - optionButton.getY()
        );
        translateAnimation.setDuration(getOptionSD_AnimationDuration());
        AlphaAnimation alphaAnimation = new AlphaAnimation(1,0);
        alphaAnimation.setDuration(getOptionSD_AnimationDuration());
        animationSet.addAnimation(translateAnimation);
        animationSet.addAnimation(alphaAnimation);
        //为不同的Option设置延时
        if (index % 2 == 0) {
            animationSet.setStartOffset(getOptionSD_AnimationDuration()/2);
        }
        return animationSet;
    }

动画都是选项从MenuButton里面冒出来和滚回去,亮点就是根据OptionButton的index设置了不同的延时,让整个效果看起来炫酷一点。

重写了这4个方法之后,就能够创造出这个Circle8YMenu了,如果想在这基础上改东西,直接重写这4个方法中要改变的方法就行了,有点方便。

TreeYMenu的实现


  这是一个OptionButton分布成分叉树的布局,Option最大数量为9个,MenuButton的位置位于ViewGroup中下方。

    //9个Option位置的x、y乘积因子
    private static final float[] xTimes = {-1,1,0,-2,-2,2,2,-1,1};
    private static final float[] yTimes = {-1,-1,-2,0,-2,0,-2,-3,-3};

    //设置MenuButton的位置,这是设置在屏幕中下方
    @Override
    public void setMenuPosition(View menuButton) {
        new MenuPositionBuilder(menuButton)
                //设置宽高
                .setWidthAndHeight(getYMenuButtonWidth(), getYMenuButtonHeight())
                //设置参考方向
                .setMarginOrientation(PositionBuilder.MARGIN_RIGHT,PositionBuilder.MARGIN_BOTTOM)
                //设置是否在XY方向处于中心
                .setIsXYCenter(true,false)
                //设置XY方向距离
                .setXYMargin(getYMenuToParentXMargin(),getYMenuToParentYMargin())
                .finish();
    }

    //设置OptionButton的位置,这里是把9个Option设置为树状布局
    @Override
    public void setOptionPosition(OptionButton optionButton, View menuButton, int index) {
        if (index > 8){
            try {
                throw new Exception("TreeYMenuView的OptionPosition最大数量为9,超过将会发生错误");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        int centerX = menuButton.getLeft() + menuButton.getWidth()/2;
        int centerY = menuButton.getTop() + menuButton.getHeight()/2;
        int halfOptionWidth = getYOptionButtonWidth()/2;
        int halfOptionHeight = getYOptionButtonHeight()/2;

        //利用乘积因子来决定不同位置
        float x = xTimes[index];
        float y = yTimes[index];
        OptionPositionBuilder OptionPositionBuilder = new OptionPositionBuilder(optionButton,menuButton);
        OptionPositionBuilder
                .isAlignMenuButton(false,false)
                .setWidthAndHeight(getYOptionButtonWidth(), getYOptionButtonHeight())
                .setMarginOrientation(PositionBuilder.MARGIN_LEFT,PositionBuilder.MARGIN_TOP)
                .setXYMargin(
                        (int)(centerX + x * getYOptionXMargin() - halfOptionWidth)
                        ,(int)(centerY + y * getYOptionYMargin() - halfOptionHeight)
                )
                .finish();
    }

这里OptionButton的设置和Circle8YMenu的差不多,就是乘积因子改变了,摆成了一个树状的布局。动画的展开也是像树一样分叉:

    //设置OptionButton的显示动画,这里是为前三个先从MenuButton冒出,后面的分别从这三个冒出
    @Override
    public Animation createOptionShowAnimation(OptionButton optionButton, int index) {
        float fromX,fromY;
        AnimationSet animationSet = new AnimationSet(true);
        if (index < 3){
            fromX = getYMenuButton().getX() - optionButton.getX();
            fromY = getYMenuButton().getY() - optionButton.getY();
        }else {
            int oldIndex = (index - 3) / 2;
            fromX = getOptionButtonList().get(oldIndex).getX() - optionButton.getX();
            fromY = getOptionButtonList().get(oldIndex).getY() - optionButton.getY();
            //设置冒出动画延时
            animationSet.setStartOffset(getOptionSD_AnimationDuration());
        }

        TranslateAnimation translateAnimation= new TranslateAnimation(
                fromX
                ,0
                ,fromY
                ,0);
        translateAnimation.setDuration(getOptionSD_AnimationDuration());

        AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
        alphaAnimation.setDuration(getOptionSD_AnimationDuration());

        animationSet.addAnimation(alphaAnimation);
        animationSet.addAnimation(translateAnimation);
        animationSet.setInterpolator(new LinearInterpolator());
        return animationSet;
    }

    //设置OptionButton的消失动画,这里设置的是直接从当前位置移动到MenuButton位置消失
    @Override
    public Animation createOptionDisappearAnimation(OptionButton optionButton, int index) {

        AnimationSet animationSet = new AnimationSet(true);
        TranslateAnimation translateAnimation= new TranslateAnimation(
                0
                ,getYMenuButton().getX() - optionButton.getX()
                ,0
                ,getYMenuButton().getY() - optionButton.getY()
        );
        translateAnimation.setDuration(getOptionSD_AnimationDuration());
        AlphaAnimation alphaAnimation = new AlphaAnimation(1,0);
        alphaAnimation.setDuration(getOptionSD_AnimationDuration());

        animationSet.addAnimation(translateAnimation);
        animationSet.addAnimation(alphaAnimation);
        //设置动画延时
        animationSet.setStartOffset(60*(getOptionPositionCount() - index));
        return animationSet;
    }

这里OptionButton前三个是从MenuButton的位置冒出,其他的6个则是分别从这三个的位置冒出,利用延时实现了先后效果。而消失动画则是给每个OptionButton都加不同的延时,实现逐个回收的效果。

SquareYMenu的实现


  这是一个OptionButton和MenuButton组成正方形的布局,Option最大数量为8个,MenuButton为位置依靠右下。这里就不贴出源码了,直接给出乘积数组,因为其他代码思路和前面的差不多,想看具体代码的可以看项目Github地址。

    //8个Option位置的x、y乘积因子
    private static final int[] xTimes = {-1,-1,0,-2,-2,-1,-2,0};
    private static final int[] yTimes = {-1,0,-1,-2,-1,-2,0,-2};

Ban的补充

YMenuView1.x就有有Ban功能,能把一些位置设为不放置OptionButton,那么YMenuView2.0也支持这个,例如:

        //对Circle8YMenu
        mYMenu.setBanArray(0,2,4,6);
        //对TreeYMenu
        mYMenu.setBanArray(2,8,7);
        //对SquareYMenu
        mYMenu.setBanArray(3,4,7,5,6);

  但是动画延时还是会算上不被填充的位置,这暂时无法避免,所以想要更好的体验效果的话就继承YMenu重写方法吧。

总结

这次重构过程主要使用了模板方法模式把一些次要的逻辑封装起来,主要的类结构从一个YMenuView扩展为一个父类YMenu和4个子类。在这个过程中也学到了不少东西,感觉自己在这方面还是有很多不足,后面会陆续更新改进的。但是最后也把自己的想法实现了。在此记录并分享,如有意见和建议,敬请提出。如果喜欢YMenuView的话,也可以上Github点个Star _

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

推荐阅读更多精彩内容