Android Canvas变换与操作(四)

只要朝着一个方向努力,一切都会变得得心应手。——勃朗宁

我喜欢写博客之前用一句励志名言激励自己!

上一篇文章地址

Canvas的意思是画布,表现在屏幕上就是一块区域,我们可以再上面使用各种API绘制我们想要的东西。可以说,Canvas贯穿整个2D Graphics,android.graphics中的所有类,几乎都于Canvas有直接或间接的联系。所以了解Canvas是学习2D Graphics的基础。

如何获得一个Canvas对象?

第一种我们通过重写View类中的onDraw方法,View中的Canvas对象会被当做参数传递过来,我们操作这个Canvas,效果会直接反应在View中。
第二种就是当你想自己创建一个Canvas对象。从上面的基本要素可以明白,一个Canvas对象一定是结合了一个Bitmap对象的。所以一定要为一个Canvas对象设置一个Bitmap对象。

       //得到一个Bitmap对象,当然也可以使用别的方式得到。但是要注意,改bitmap一定要是mutable(异变的)
        Bitmap b = Bitmap.createBitmap(100,100, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        /*先new一个Canvas对象,在调用setBitmap方法,一样的效果
         * Canvas c = new Canvas();
         * c.setBitmap(b);
         */

第三种方式,是调用SurfaceHolder.lockCanvas(),返回一个Canvas对象。

我们有了画布对象后,接下来我们先看看它的平移,canvas 中有一个函数 translate()是用来实现画布坐标系平移的,画布坐标是以左上角为原点(0,0),向右是X轴正方向,向下是Y轴正方向,如下图所示

void translate(float dx, float dy)
// float dx:水平方向平移的距离,正数指向正方向(向右)平移的量,负数为向负方向(向左)平移的量
// float dy:  垂直方向平移的距离,正数指向正方向 (向下) 平移量,负数为向负方向 (向上) 平移量

我们创建一个新的类,MyCanvasView.java类

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        //translate  平移,即改变坐标系原点位置
        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        paint.setStyle(Paint.Style.FILL);

//  canvas.translate(100, 100);
        Rect rect1 = new Rect(0, 0, 400, 220);
        canvas.drawRect(rect1, paint);
}

上面这段代码,先把canvas.translate(100, 100);注释掉,看原来矩形的位置,然后打开注释,看平移后的位置,对比如下图:


这段代码中,同一个矩形,在画布平移前画一次,平移后再画一次,大家会觉得结果会怎样?

package com.as.customview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

public class MyCanvasView extends View {

    public MyCanvasView(Context context) {
        this(context, null);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, 0);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);


        //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

        //构造一个矩形
        Rect rect = new Rect(0, 0, 400, 220);

        //在平移画布前用绿色画下边框
        canvas.drawRect(rect, paintGreen);

        ////平移画布后,再用红色边框重新画下这个矩形
        canvas.translate(100, 100);
        canvas.drawRect(rect, paintRed);
    }

    private Paint generatePaint(int color, Paint.Style style, int stroke) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(stroke);
        return paint;
    }

}

这段代码中,对于同一个矩形,在平移画布前利用绿色画下矩形边框,在平移后,再用红色画下矩形边框。大家是不是会觉得这两个边框会重合?实际结果是这样的。

从到这个结果大家可能会蛋疼,我第一次看到这个结果的时候蛋都碎一地了要。淡定……
这个结果的关键问题在于,为什么绿色框并没有移动?

这是由于屏幕显示与Canvas根本不是一个概念!Canvas是一个很虚幻的概念,相当于一个透明图层,每次Canvas画图时(即调用draw系列函数),都会产生一个透明图层,然后在这个透明图层上画图,在透明图层画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的:

1、调用canvas.drawRect(rect, paintGreen0) 时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0);系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:


2、然后再第二次调用canvas.drawRect(rect, paintRed)时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:(合成视图,从上往下看的合成方式)



上图展示了,Canvas图层与屏幕的合成过程,由于Canvas画布已经平移了100像素,所以在画图时是以新原点来产生视图的,然后合成到屏幕上,这就是我们上面最终看到的结果了。我们看到画布移动之后,有一部分超出了屏幕的范围,那超出范围的图像显不显示呢,当然不显示了!也就是说,Canvas上虽然能画上,但超出了屏幕的范围,是不会显示的。当然,我们这里也没有超出显示范围,两框框而已。

下面对上面的知识做一下总结

  • 每次调用canvas.drawXXXX系列函数来绘图,都会产生一个全新的Canvas画布。
  • 如果在DrawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的!每次产生的画布最新位置都是这些操作后的位置。(关于Save()、Restore()的画布可逆问题的后面再讲)
  • 在Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的。

旋转(Rotate)

画布的旋转是默认是围绕坐标原点来旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实我们旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。其实Roate函数有两个构造函数

void rotate(float degrees)
void rotate (float degrees, float px, float py)
// 第一个构造函数直接输入旋转的度数,正数是顺时针旋转,负数指逆时针旋转,它的旋转中心点是原点(0,0)
// 第二个构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py)

下面以第一个构造函数为例,旋转一个矩形,先画出未旋转前的图形,然后再画出旋转后的图形;

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);


        //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

        //构造一个矩形
        Rect rect = new Rect(300, 10, 500, 100);

        //画出原轮廓
        canvas.drawRect(rect, paintGreen);

        //顺时针旋转画布 30度
        canvas.rotate(30);
        canvas.drawRect(rect, paintRed);
}

接下来我们看看第二个构造函数,以某个坐标点旋转

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
      //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 2);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 2);

        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

        //画横线(水平方向居中)
        canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);

        //画竖线(垂直方向剧中)
        canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);

        // 画布旋转10度
        canvas.rotate(10, displayMetrics.widthPixels / 2, displayMetrics.heightPixels / 2);
        //再画横线(垂直方向剧中)
        canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);

        //再画竖线(水平方向剧中)
        canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);
    }

    private Paint generatePaint(int color, Paint.Style style, int stroke) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(stroke);
        paint.setAntiAlias(true);
        return paint;
    }

我们在屏幕上花了四条线段,其中两条垂线,画布旋转10度后,有画了两条线。
接下来复习之前学习过的路径Path类,直接看看完成代码

package com.as.customview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

public class MyCanvasView extends View {

    public MyCanvasView(Context context) {
        this(context, null);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, 0);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

//    protected void onDraw(Canvas canvas) {
//        // TODO Auto-generated method stub
//        super.onDraw(canvas);

//        canvas.drawColor(Color.RED);
//        //保存的画布大小为全屏幕大小
//        canvas.save();
//
//        canvas.clipRect(new Rect(100, 100, 700, 700));
//        canvas.drawColor(Color.GREEN);
//        //保存画布大小为Rect(100, 100, 700, 700)
//        canvas.save();
//
//        canvas.clipRect(new Rect(200, 200, 600, 600));
//        canvas.drawColor(Color.BLUE);
//        //保存画布大小为Rect(200, 200, 600, 600)
//        canvas.save();
//
//        canvas.clipRect(new Rect(300, 300, 500, 500));
//        canvas.drawColor(Color.BLACK);
//        //保存画布大小为Rect(300, 300, 500, 500)
//        canvas.save();
//
//        canvas.clipRect(new Rect(370, 370, 430, 430));
//        canvas.drawColor(Color.WHITE);
//
//        canvas.restore();
//        canvas.restore();
//        canvas.restore();
//        canvas.drawColor(Color.YELLOW);


//        //构造一个矩形
//        Rect rect = new Rect(300, 0, 500, 100);
//
//        //画出原轮廓
//        canvas.drawRect(rect, paintGreen);

//        Rect rect2 = new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
//        for (int i = 10; i <= 90; i += 10) {
//            canvas.save();
//            canvas.rotate(i);
//            canvas.drawRect(rect2, paintGreen);
//            canvas.restore();
//        }
//    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 2);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 2);

        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

        //画横线(水平方向居中)
        canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);

        //画竖线(垂直方向剧中)
        canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);

        // 画布旋转10度
        canvas.rotate(10, displayMetrics.widthPixels / 2, displayMetrics.heightPixels / 2);
        //画横线(垂直方向剧中)
        canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);

        //画竖线(水平方向剧中)
        canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);

        //设置画笔的文本大小为 20个px
        paintRed.setTextSize(20);
        paintGreen.setTextSize(20);
        String textGreen = "第2条绿线";
        canvas.drawText(textGreen, displayMetrics.widthPixels - 120, displayMetrics.heightPixels / 2 - 10, paintGreen);

        //复习下之前学习过的路径
        Path path = new Path();
        String textRed = "第2条红线";
        int textWidth = (int) paintRed.measureText(textRed);

        //直线的起始点
        path.moveTo(displayMetrics.widthPixels / 2 + 10, textWidth / 2);

        //是直线的终点,又是下一次绘制直线路径的起始点
        path.lineTo(displayMetrics.widthPixels / 2 + 10, textWidth + textWidth / 2);
        canvas.drawTextOnPath(textRed, path, 0, 0, paintRed);
    }

    private Paint generatePaint(int color, Paint.Style style, int stroke) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(stroke);
        paint.setAntiAlias(true);
        return paint;
    }


}

缩放(scale )

public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)

其实我也没弄懂第二个构造函数是怎么使用的,我就先讲讲第一个构造函数的参数吧
float sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n*sx;所以,sx为小数为缩小,sx为整数为放大
float sy:垂直方向伸缩的比例,同样,小数为缩小,整数为放大
        //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

        //构造一个矩形
        Rect rect = new Rect(10, 10, 200, 100);

        //画出原轮廓
        canvas.drawRect(rect, paintGreen);

        canvas.scale(0.5f, 1f);
        canvas.drawRect(rect, paintRed);

扭曲(skew)

其实我觉得译成斜切更合适,在PS中的这个功能就差不多叫斜切。但这里还是直译吧,大家都是这个名字。看下它的构造函数:

void skew (float sx, float sy)
float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值,

注意,这里全是倾斜角度的tan值哦,比如我们打算在X轴方向上倾斜60度,tan60=根号3,小数对应1.732

package com.as.customview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

public class MyCanvasView extends View {

    public MyCanvasView(Context context) {
        this(context, null);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, 0);
    }

    public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);


        //构造两个画笔,一个红色,一个绿色
        Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
        Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);

        //构造一个矩形
        Rect rect = new Rect(10, 10, 200, 100);

        //画出原轮廓
        canvas.drawRect(rect, paintGreen);

        //X轴倾斜60度,Y轴不变
        canvas.skew(1.732f, 0);
        canvas.drawRect(rect, paintRed);
    }

    private Paint generatePaint(int color, Paint.Style style, int stroke) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(stroke);
        return paint;
    }

}

裁剪画布(clip系列函数)

裁剪画布是利用clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用Save、Restore函数以外,这个操作是不可逆的,一但Canvas画布被裁剪,就不能再被恢复!

boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)
boolean clipRect(Rect rect, Region.Op op)
boolean clipRect(RectF rect, Region.Op op)
boolean clipRect(int left, int top, int right, int bottom)
boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(RectF rect)
boolean clipRect(float left, float top, float right, float bottom, Region.Op op)
boolean clipRect(Rect rect)
boolean clipRegion(Region region)
boolean clipRegion(Region region, Region.Op op)

以上就是根据Rect、Path、Region来取得最新画布的函数,难度都不大,就不再一一讲述。利用clipRect()来稍微一讲。

        canvas.drawColor(Color.RED);
        canvas.clipRect(new Rect(100, 100, 200, 200));
        canvas.drawColor(Color.GREEN);

先把背景色整个涂成红色,显示在屏幕上。然后裁切画布,最后最新的画布整个涂成绿色。可见绿色部分,只有一小块,而不再是整个屏幕了。
关于两个画布与屏幕合成,我就不再画图了,跟上面的合成过程是一样的。


画布的保存与恢复(save()、restore())

前面我们讲的所有对画布的操作都是不可逆的,这会造成很多麻烦,比如,我们为了实现一些效果不得不对画布进行操作,但操作完了,画布状态也改变了,这会严重影响到后面的画图操作。如果我们能对画布的大小和状态(旋转角度、扭曲等)进行实时保存和恢复就最好了。
这小节就给大家讲讲画布的保存与恢复相关的函数——save()、restore()。

int save ()
void restore()
这两个函数没有任何的参数,很简单。
Save():每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中;
restore():每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。

为了更清晰的显示这两个函数的作用,下面举个例子:

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        canvas.drawColor(Color.RED);

        canvas.save();//保存当前画布大小即整屏

        canvas.clipRect(new Rect(100, 100, 600, 600));
        canvas.drawColor(Color.GREEN);

        canvas.restore(); //恢复整屏画布
        canvas.drawColor(Color.YELLOW);
    }

    private Paint generatePaint(int color, Paint.Style style, int stroke) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(stroke);
        return paint;
    }

他图像的合成过程为:(最终显示为全屏幕蓝色)


下面我通过一个多次利用save()、restore()来讲述有关保存Canvas画布状态的栈的概念:代码如下:

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        canvas.drawColor(Color.RED);
        //保存的画布大小为全屏幕大小
        canvas.save();

        canvas.clipRect(new Rect(100, 100, 700, 700));
        canvas.drawColor(Color.GREEN);
        //保存画布大小为Rect(100, 100, 700, 700)
        canvas.save();

        canvas.clipRect(new Rect(200, 200, 600, 600));
        canvas.drawColor(Color.BLUE);
        //保存画布大小为Rect(200, 200, 600, 600)
        canvas.save();

        canvas.clipRect(new Rect(300, 300, 500, 500));
        canvas.drawColor(Color.BLACK);
        //保存画布大小为Rect(300, 300, 500, 500)
        canvas.save();

        canvas.clipRect(new Rect(370, 370, 430, 430));
        canvas.drawColor(Color.WHITE);

    }

在这段代码中,总共调用了四次save操作。上面提到过,每调用一次save()操作就会将当前的画布状态保存到栈中,所以这四次save()所保存的状态的栈的状态如下:



注意在,第四次save()之后,我们还对画布进行了canvas.clipRect(new Rect(370, 370, 430, 430));操作,并将当前画布画成白色背景。也就是上图中最小块的白色部分,是最后的当前的画布。
如果,现在使用restore(),会怎样呢,会把栈顶的画布取出来,当做当前画布的画图,试一下:

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        canvas.drawColor(Color.RED);
        //保存的画布大小为全屏幕大小
        canvas.save();

        canvas.clipRect(new Rect(100, 100, 700, 700));
        canvas.drawColor(Color.GREEN);
        //保存画布大小为Rect(100, 100, 700, 700)
        canvas.save();

        canvas.clipRect(new Rect(200, 200, 600, 600));
        canvas.drawColor(Color.BLUE);
        //保存画布大小为Rect(200, 200, 600, 600)
        canvas.save();

        canvas.clipRect(new Rect(300, 300, 500, 500));
        canvas.drawColor(Color.BLACK);
        //保存画布大小为Rect(300, 300, 500, 500)
        canvas.save();

        canvas.clipRect(new Rect(370, 370, 430, 430));
        canvas.drawColor(Color.WHITE);

        canvas.restore();
        canvas.drawColor(Color.YELLOW);

    }

上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色



那如果我连续Restore()三次,会怎样呢?
我们先分析一下,然后再看效果:Restore()三次的话,会连续出栈三次,然后把第三次出来的Canvas状态当做当前画布,也就是Rect(100, 100, 700, 700),所以如下代码:

protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        canvas.drawColor(Color.RED);
        //保存的画布大小为全屏幕大小
        canvas.save();

        canvas.clipRect(new Rect(100, 100, 700, 700));
        canvas.drawColor(Color.GREEN);
        //保存画布大小为Rect(100, 100, 700, 700)
        canvas.save();

        canvas.clipRect(new Rect(200, 200, 600, 600));
        canvas.drawColor(Color.BLUE);
        //保存画布大小为Rect(200, 200, 600, 600)
        canvas.save();

        canvas.clipRect(new Rect(300, 300, 500, 500));
        canvas.drawColor(Color.BLACK);
        //保存画布大小为Rect(300, 300, 500, 500)
        canvas.save();

        canvas.clipRect(new Rect(370, 370, 430, 430));
        canvas.drawColor(Color.WHITE);

        canvas.restore();
        canvas.restore();
        canvas.restore();
        canvas.drawColor(Color.YELLOW);

    }

上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色


扩展阅读:《Android 2D Graphics学习(二)、Canvas篇1、Canvas基本使用》

下一篇文章地址

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