Android自定义View实战(验证码)

最近有点时间,准备补补自定义View,我就直接去鸿洋大神的博客了。按照博客上的文章说明,自己实现四位验证码的效果。这个过程中遇到了不少问题,也从中学到了不少,把一些知识盲点给清除了。

鸿洋博客:Android 自定义View (一)
可以参考鸿洋大神的博客一步步的来学习和进阶。

最基本的知识,想必大家都知道,自定义View有3种。

第一种,继承控件,通过继承已有控件进行扩展,实现一些自带控> 件没有的功能。
第二种,组合控件,通过多种自带控件组合在一起,形成新的控件。
第三种,绘制控件,继承自View完全靠canvas绘制出想要的图形和文字。

继承控件和组合控件,想必大家都会了,绘制控件很多人都讳莫如深,有着深深的恐惧。最初,我也是有这样的恐惧,也曾花些时间学习了一下,但是学的一知半解,由于时间关系,没有继续深入,很多知识点也没有梳理清楚。现在准备花点时间,一一揭开他们的面纱。

先说一下实现自定义View的步骤:

1、自定义View的属性
2、在构造方法中获取自定义的属性
3、重写onMeasure()
4、重写onDraw()

1、最基本的操作

自定义View最基本的操作,就是定义一个类,继承自View,实现三个构造方法。如下所示。

public class MyVerifyCode extends View {

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

    public MyVerifyCode(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    private void init() {
    }
}

三个构造方法有什么区别呢?为什么要写三个构造方法?

当我们在Java代码中使用某些控件时,会直接new出来一个对象,这时候我们一般会传入Context,此时调用的就是一个参数的构造方法。因为此时我们只是简单的想要一个对象,至于对象的属性和方法还没有相应的操作,在后面的代码中才会通过调用方法给对象的属性赋值。

当我们在xml布局文件中使用控件时,会通过一些属性来赋值,此时会调用第二个构造方法,第二个参数attrs就是接受xml文件中传入的属性值。当我们在xml布局文件中使用控件的同时,附加了style样式属性,那么就会调用第三个构造方法,也就是第三个参数defStyleAttr来接受添加的样式。所以,自定义View一般都是会实现这三种构造方法。

当然了,你说你自定义的View只需要在Java代码中使用,那么你只实现第一个构造方法也是可以的。如果不使用样式,只实现前两个构造方法也可以。

<com.sendtion.customview.view.MyVerifyCode
        android:id="@+id/my_verify_code"
        android:layout_width="200dp"
        android:layout_height="100dp"/>

另外有一点需要注意,为了在初始化的时候统一调用,一般会在前两个构造方法中使用this调用,这样的话我们只需要在第三个构造方法中做初始化操作。

2、onMeasure测量

上面实现了自定义View最基本的操作,你说我就实现这么多,不做别的什么操作了行不行?当然可以啊,但是这样的代码有什么卵用呢?

有两个重要的方法,一个是onMeasure(widthMeasureSpec, heightMeasureSpec),一个是onDraw(Canvas canvas),这两个方法是我们主要操作的地方。

onMeasure方法不是必须的,它是用来测量View的宽高的,默认有系统自动测量。

当你自定义的View在布局中设置宽高为match_parent或者固定宽高时,得到的效果是正确的。此时,是不需要重写onMeasure方法的。

<com.sendtion.customview.view.MyVerifyCode
        android:id="@+id/my_verify_code"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

然而,当你的View宽高设置为wrap_content时,得到的效果仍然是march_parent。此时,需要重写onMeasure方法,自己实现测量。

<com.sendtion.customview.view.MyVerifyCode
        android:id="@+id/my_verify_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

自己实现测量时,要根据widthMeasureSpec和heightMeasureSpec两个参数来确定View的宽高。

widthMeasureSpec和heightMeasureSpec是一个32位的int值,高2位代表测量模式,低30位代表测量的大小,用于辅助View的测量。

重写之前先了解MeasureSpec的specMode测量模式,一共三种类型:

EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面重写onMeasure方法,经过上面的分析,我们只需要处理AT_MOST模式就可以了,当然EXACTLY也要给一个值才行。

所以我们的实现代码如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        } else {
            paint.setTextSize(vcTextSize);
            paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
            float textWidth = rect.width();
            Log.e("---", "onMeasure: textWidth= " + textWidth);
            //width = (int) textWidth; //这样也可以,但是没计算padding
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        }
        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        } else {
            paint.setTextSize(vcTextSize);
            paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
            float textHeight = rect.height();
            Log.e("---", "onMeasure: textHeight= " + textHeight);
            //height = (int) textHeight; //这样也可以,但是没计算padding
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
        }
        setMeasuredDimension(width, height);
    }

从以上代码可以看出,在EXACTLY模式下面,直接使用系统测量的数据,否则就自己计算。计算方式也很简单,就是字体内容的宽高加上padding数值。
View的宽度为字体的宽度rect.width()+paddingLeft+paddingRight,View的高度为字体的高度rect.height()+paddingTop+paddingBottom。

<com.sendtion.customview.view.MyVerifyCode
        android:id="@+id/my_verify_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"/>

3、onDraw绘制

对于Android的2D绘图,我们主要借助于graphics包,其中主要包含了Canvas类、Paint类、Color类和Bitmap类等。

要绘制图形就得有画笔,根据自己的需要对画笔设置相关的属性,然后通过canvas来绘制。

画笔的常见属性如下:

setAntiAlias: 设置画笔的锯齿效果。
setColor: 设置画笔颜色 。
setARGB: 设置画笔的a,r,p,g值。
setAlpha: 设置Alpha值 。
setTextSize: 设置字体尺寸。
setStyle: 设置画笔风格,空心或者实心。
setStrokeWidth: 设置空心的边框宽度。
getColor: 得到画笔的颜色 。
getAlpha: 得到画笔的Alpha值

详细的属性解析,请参考此文:Paint的效果研究

Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,在使用该类前需要设置好paint。

Canvas常用操作速查表:


Canvas画布描述

除了进行基本的绘制外,Canvas也可以进行一些基本操作。位移、缩放、旋转、倾斜以及快照和回滚。

具体操作参考:Canvas操作

下面是具体的实现,以下操作全部在onDraw方法中。

@Override
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画背景
        paint.setColor(vcBackground);
        canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
}

首先,画一个矩形的纯色背景。通过画笔的setColor()方法设置矩形的颜色,然后通过画布的drawRect()方法画出矩形。

传入的参数为:drawRect(left, top, right, bottom, paint); 左上点到右下点,也就是设置View的宽高和画笔。

paint.setColor(vcBackground);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);

然后,在矩形中绘制文字。通过画笔的setColor()方法设置文字的颜色,画笔的getTextBounds()方法获取文字的宽高。通过计算公式获得文字开始绘制的起始位置,通过画布的drawText()方法绘制出文字。

文字的宽高从getTextBounds()方法的Rect参数中获取,但是这种获取方式不太精确,获取到的文字宽度会小一点,更精确的方式是paint的measureText()方法。如下所示:

float textWidth = mBound.width();//这样宽度会不全,比系统的textView短
float textWidth = mPaint.measureText(mTitleText);//比较精确的测量文本宽度的方式

传入的参数为:drawText(text, x, y, paint); 传入要绘制的文字,开始绘制的起始坐标,画笔。

//画文字
paint.setColor(vcTextColor);
paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
int startX = getWidth()/2 - rect.width()/2;
int startY = getHeight()/2 - rect.height()/2;
canvas.drawText(verifyCode, startX, startY-offset, paint);

这样就实现了在一个矩形的红色背景上,绘制出白色的四位验证码。
然而,我们发现绘制的结果并不能令我们满意,文字并不是居中显示的,而是有点偏上的位置。这是因为文字的绘制比较特殊,它有一个基线的存在,文字的绘制以基线为基准。后面再说明文字绘制的居中显示。

先说解决方案。

Paint.FontMetricsInt fm = paint.getFontMetricsInt();
startX = (int) (getWidth() / 2 - paint.measureText(verifyCode) / 2);
startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent;
canvas.drawText(verifyCode, startX, startY, paint);

4、自定义属性

想要自定义属性,一般我们会在res/values目录下创建attrs.xml文件,通过declare-styleable来指定自定义属性的归属,name属性一般使用自定义View类名称,通过attr来定义需要的属性,name指定属性名,format指定属性值的类型。

<com.sendtion.customview.view.MyVerifyCode
        android:id="@+id/my_verify_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        app:vc_background="@color/colorAccent"
        app:vc_auto_refresh="false"
        app:vc_text_size="16sp"
        app:vc_text_color="@color/color_f"/>

format的取值类如下所示:

boolean 表示attr取值为true或者false
color 表示attr取值是颜色类型,例如#ff3344,或者是一个指向color的资源id,例如R.color.colorAccent.
dimension 表示 attr 取值是尺寸类型,例如例如取值16sp、16dp,也可以是一个指向dimen的资源id,例如R.dimen.dp_16
float 表示attr取值是整形或者浮点型
fraction 表示 attr取值是百分数类型,只能以%结尾,例如30%
integer 表示attr取值是整型
string 表示attr取值是String类型,或者一个指向String的资源id,例如R.string.testString
reference 表示attr取值只能是一个指向资源的id。
enum 表示attr取值只能是枚举类型。
flag 表示attr取值是flag类型。

需要注意的是:refrence , 表示attr取值只能是一个指向资源的id。比如:app:vc_text_color="@color/colorAccent"

自定义属性的值需要在构造方法中取出,实现代码如下:

public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyVerifyCode);
        vcAutoRefresh = typedArray.getBoolean(R.styleable.MyVerifyCode_vc_auto_refresh, false);
        vcTextSize = typedArray.getDimensionPixelSize(R.styleable.MyVerifyCode_vc_text_size, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
        vcTextColor = typedArray.getColor(R.styleable.MyVerifyCode_vc_text_color, getResources().getColor(R.color.color_f));
        vcBackground =typedArray.getColor(R.styleable.MyVerifyCode_vc_background, getResources().getColor(R.color.colorAccent));
        typedArray.recycle();

        init();
}

需要注意的是,TypedArray使用完后需要回收。

另外,自定义属性的dp和sp值需要转换为px才行,否则以实际输入值为px值,比如默认字体大小为16sp,就需要转换为px单位,具体的转换方式如代码所示:

TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()))

取大小单位时,有三个方法可以使用,这3个方法都是把dimens.xml文件中的dp或sp数值乘以屏幕scale来换算成px单位,那相乘之后可能会有小数。

三个方法的区别是:

getDimension() 返回float型px值 精确
getDimensionPixelOffset() 返回int型px值 直接把小数删除
getDimensionPixelSize() 返回int型px值 进行四舍五入

各方法使用场景:

如果你的代码中可以用float作为长度单位的话,就用getDimension()方法,因为最精确;
如果只能接收int为长度单位的的话,那就看你自己的需求来选要用getDimensionPixelOffset()或getDimensionPixelSize();
如果你在写代码的时候不记得这3个方法的区别了,无所谓了,随便用哪一个都可以,不就是相差小数点那一点点的大小而已嘛,差别很小可以忽略不计了;

5、文字居中绘制

为什么把文字居中绘制单独拿出来呢?因为文字绘制这一块有一些特殊的知识点,如果不了解,绘制的文字就不是自己想要的效果。

我们先来看一下绘制文字的方法和参数:

drawText(String text, float x, float y, Paint paint);

方法的参数很简单: text 是文字内容,x 和 y 是文字的坐标。这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。y的坐标并不是文字的底部,也不是文字的中间,而是底部靠上一点的位置。这是因为y坐标是文字基线的位置。

下面我会画出几条线,分别是矩形的中间位置十字交叉线,文字的范围线。

//画背景
paint.setColor(vcBackground);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
//画文字
paint.setColor(vcTextColor);
paint.setStrokeWidth(2);
paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
int startX = getWidth()/2 - rect.width()/2;
int startY = getHeight()/2 - rect.height()/2;
canvas.drawText(verifyCode, startX, startY, paint);
//画十字交叉线
paint.setColor(Color.GREEN);
paint.setStrokeWidth(2);
canvas.drawLine(0, getHeight()/2, getWidth(), getHeight()/2, paint);
canvas.drawLine(getWidth()/2, 0, getWidth()/2, getHeight(), paint);
//画范围线
canvas.drawLine(0, getHeight()/2-rect.height()/2, getWidth(), getHeight()/2-rect.height()/2, paint);
canvas.drawLine(0, getHeight()/2+rect.height()/2, getWidth(), getHeight()/2+rect.height()/2, paint);
canvas.drawLine(getWidth()/2-rect.width()/2, 0, getWidth()/2-rect.width()/2, getHeight(), paint);
canvas.drawLine(getWidth()/2+rect.width()/2, 0, getWidth()/2+rect.width()/2, getHeight(), paint);
//画起始点
paint.setColor(Color.BLUE);
paint.setStrokeWidth(10);
canvas.drawPoint(getWidth()/2-rect.width()/2, getHeight()/2-rect.height()/2, paint);

效果图如下:

据图所示,我们看到文字的位置不是我们想要的,文字在偏上的位置,绘制的起始点也不是我们以为的位置。起始点所在的横向直线就是我们要说的基线。从其他博客借一张图。

文字基线示意图

Baseline是基线,在android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);
leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;
top和bottom文档描述地很模糊,其实这里我们可以借鉴一下TextView对文本的绘制,TextView在绘制文本的时候总会在文本的最外层留出一些内边距,因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:

示意图

Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent,top是负值, descent和bottom是正值。

所以,原本的文字高度获取方式:

float textHeight = rect.height();

可以修改为比较精确的测量文本高度的方式:

Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int textHeight = fontMetrics.bottom - fontMetrics.top;

因为top是负值,bottom是正值,所以用bottom-top获取文字的高度。那么,我们获取绘制的起始点y坐标就可以修改一下了:

int startY = getHeight() / 2 + (fm.bottom - fm.top) / 2 - fm.bottom;

当然了,也可以使用ascent和descent,有一点小小的误差,因为ascent和descent包含文字音标之类的,所以他们的值比top和bottom要大一些,计算的结果也更准确一点。

int startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent;

最终实现效果如下:

完整的代码如下:

/**
 * 自定义验证码
 */
public class MyVerifyCode extends View {
    private String verifyCode;
    private Paint paint;
    private Rect rect;

    private boolean vcAutoRefresh;
    private int vcTextSize = 56;//单位
    private int vcTextColor = getResources().getColor(R.color.color_f);
    private int vcBackground = getResources().getColor(R.color.colorAccent);

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

    public MyVerifyCode(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyVerifyCode);
        vcAutoRefresh = typedArray.getBoolean(R.styleable.MyVerifyCode_vc_auto_refresh, false);
        vcTextSize = typedArray.getDimensionPixelSize(R.styleable.MyVerifyCode_vc_text_size, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
        vcTextColor = typedArray.getColor(R.styleable.MyVerifyCode_vc_text_color, getResources().getColor(R.color.color_f));
        vcBackground =typedArray.getColor(R.styleable.MyVerifyCode_vc_background, getResources().getColor(R.color.colorAccent));
        typedArray.recycle();

        init();
    }

    private void init() {
        //会出现三位数的情况
        verifyCode = String.valueOf((int) (Math.random() * 10000));//四位验证码
        verifyCode = "abcdefg";

        paint = new Paint();
        //paint.setTextAlign(Paint.Align.CENTER);
        paint.setTextSize(vcTextSize);
        //Paint.ANTI_ALIAS_FLAG :抗锯齿标志
        //Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
        //Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
        //Paint.UNDERLINE_TEXT_FLAG : 下划线
        //Paint.STRIKE_THRU_TEXT_FLAG : 中划线
        //Paint.FAKE_BOLD_TEXT_FLAG : 加粗
        //Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
        //Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
        //Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);

        rect = new Rect();

        if (vcAutoRefresh) {

        }

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                verifyCode = String.valueOf((int) (Math.random() * 10000));//四位验证码
                postInvalidate();
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        } else {
            paint.setTextSize(vcTextSize);
            paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
            float textWidth = rect.width();
            Log.e("---", "onMeasure: textWidth= " + textWidth);
            //width = (int) textWidth; //这样也可以,但是没计算padding
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        }
        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        } else {
            paint.setTextSize(vcTextSize);
            paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);
            float textHeight = rect.height();
            Log.e("---", "onMeasure: textHeight= " + textHeight);
            //height = (int) textHeight; //这样也可以,但是没计算padding
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //画背景
        paint.setColor(vcBackground);
        canvas.drawRect(0, 0, getWidth(), getHeight(), paint);

        //画文字
        Log.e("---", "onDraw: getWidth=" + getWidth() + ", getHeight=" + getHeight());
        paint.setColor(vcTextColor);
        paint.setStrokeWidth(2);
        paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect);

        //第一种方式,计算的基准点偏上
        int startX = getWidth()/2 - rect.width()/2;
        int startY = getHeight()/2 - rect.height()/2;
        //canvas.drawText(verifyCode, startX, startY, paint);

        //第二种方式,貌似更准确,计算的基准点更准,内容变化也会居中显示
        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        startX = (int) (getWidth() / 2 - paint.measureText(verifyCode) / 2);
        startY = getHeight() / 2 + (fm.bottom - fm.top) / 2 - fm.bottom;
        //startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent;
        canvas.drawText(verifyCode, startX, startY, paint);

        //画十字交叉线
        paint.setColor(Color.GREEN);
        paint.setStrokeWidth(2);
        canvas.drawLine(0, getHeight()/2, getWidth(), getHeight()/2, paint);
        canvas.drawLine(getWidth()/2, 0, getWidth()/2, getHeight(), paint);

        //画基准线
        canvas.drawLine(0, getHeight()/2-rect.height()/2, getWidth(), getHeight()/2-rect.height()/2, paint);
        canvas.drawLine(0, getHeight()/2+rect.height()/2, getWidth(), getHeight()/2+rect.height()/2, paint);
        canvas.drawLine(getWidth()/2-rect.width()/2, 0, getWidth()/2-rect.width()/2, getHeight(), paint);
        canvas.drawLine(getWidth()/2+rect.width()/2, 0, getWidth()/2+rect.width()/2, getHeight(), paint);

        //画基准点
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(10);
        canvas.drawPoint(startX, startY, paint);
    }
}

全部源码:github地址

** 参考文章:**

本文完结。

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

推荐阅读更多精彩内容