属性动画(二)

在上篇文章 属性动画(一) 中已经对属性动画有了基本的介绍,本篇文章将对属性动画中稍微高级点的内容进行介绍,主要介绍下图中绿色部分标明的知识点。

catalog.png

1. Evaluators

Evaluator 是干什么用的呢?简单来说,它是用于告诉动画系统,某种类型的属性值怎么从初始值变化到结束值的。

1.1 TypeEvaluator 解析

系统中自带一些默认的的 Evaluator,比如:IntEvaluatorFloatEvaluatorArgbEvaluator 等等,我们简单分析一下其中的 IntEvaluator 的源码。

public class IntEvaluator implements TypeEvaluator<Integer> {

    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

IntEvaluator 实现了 TypeEvaluator 接口,重写其中的 evaluate(float fraction, Integer startValue, Integer endValue) 方法。该方法中有三个参数值:

  • float fraction:表示动画的完成度,用于计算此时属性的值
  • Integer startValue:属性值的初始值
  • Integer endValue:属性值的结束值

evaluate(float fraction, Integer startValue, Integer endValue) 方法内部的实现也很简单:用最终值减去初始值再乘以完成度的,加上初始值,就是动画当前的属性值。

1.2 自定义 Evaluator

仿照 IntEvaluator,我们可以自定义 Evaluator

假设我们现在有一个自定义 View,其颜色是是从纯绿色渐变为纯红色,那该怎么实现的?答案是我们可以通过自定义 Evaluator 来实现。
假设自定义View 的代码如下:

public class CustomView extends View {

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

    int color = 0xff00ff00;

    public CustomView(Context context) {
        super(context);
    }

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

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

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
        invalidate();
    }

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

        int width = getWidth();
        int height = getHeight();

        paint.setColor(color);
        canvas.drawCircle(width / 2, height / 2, width / 6, paint);
    }
}

上述自定义 View 的代码其实很简单,根据设置的大小,画一个默认是绿色的圆。

需要注意的是,在该自定义 View 中,如果要改变 color 颜色的属性,该属性需要有一个对应的 setter 的方法,并在其中调用 invalidate() 方法,保证 color 属性变化的时候,都会重新调用 onDraw(Canvas canvas) 重新绘制该 View

那接下来该实现颜色变化的自定义 Evaluator 了,具体代码如下所示:

public class HsvEvaluator implements TypeEvaluator<Integer> {

     private float[] startColor;

     private float[] endColor;

     private float[] midColor;

     public HsvEvaluator() {
         startColor = new float[3];
         endColor = new float[3];
         midColor = new float[3];
     }

     // 重写 evaluate() 方法,让颜色按照 HSV 来变化
     @Override
     public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
         Color.colorToHSV(startValue, startColor);
         Color.colorToHSV(endValue, endColor);

         midColor[0] = startColor[0] + fraction * (endColor[0] - startColor[0]);
         midColor[1] = startColor[1] + fraction * (endColor[1] - startColor[1]);
         midColor[2] = startColor[2] + fraction * (endColor[2] - startColor[2]);

         Integer integer = Color.HSVToColor(midColor);
         return integer;
     }
 }

在其中,主要是重写 evaluate(float fraction, Integer startValue, Integer endValue) 方法,通过 Color.colorToHSV(int color, float hsv[]) 方法,将 Integer 类型的颜色值转换为保存在长度为 3 的 float 数组中,其中每一位对应的是该颜色的 HSV 值。

然后,通过完成度的值 fraction,计算出当前时刻的颜色属性的 HSV 值,并保存在对应的数组中,最后通过 Color.HSVToColor(float hsv[]) 方法转换成 Integer 类型的颜色值,并返回。

那实现该动画的代码如下所示:

    CustomView view = (CustomView) findViewById(R.id.objectAnimatorView);
    ObjectAnimator animator = ObjectAnimator.ofInt(view, "color", 0xffff0000, 0xff00ff00);
    animator.setEvaluator(new HsvEvaluator()); // 使用自定义的 HsvEvaluator
    animator.setInterpolator(new LinearInterpolator());
    animator.setDuration(2000);
    animator.start();

2. Interpolators

Interpolator 直译过来叫做 插值器,定义属性动画的变化率,Interpolator 允许动画不是线性变化的,而是非线性变化的,比如加速变化、减速变化等等。

系统已经默认提供了许多实现了 Interpolator 的类:

  • BaseInterpolator
  • LinearInterpolator
  • AccelerateDecelerateInterpolator
  • AccelerateInterpolator
  • AnticipateInterpolator
  • AnticipateOvershootInterpolator
  • BounceInterpolator
  • ......

2.1 TimeInterpolator

Interpolator 都实现了接口 TimeInterpolator,它的源码还是非常简单的,如下所示:

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

其中只有一个方法,getInterpolation(float input),按照其注释,不难理解。

  • 输入的 float 参数 input 是随着动画的运行均匀变化的,范围是 01,开始时是 0,结束时是 1
  • 其返回值,则是由输入的 input 计算得到的。返回值可以比 1 大,表示动画属性值超过设定的结束值,也可以比 1 小,表示动画属性值小于设定的结束值。
  • 其实,getInterpolation(float input) 的返回值,就是之前提到的动画完成度 ---- fraction

我们简单分析下 LinearInterpolator 匀速插值器的源码,如下所示:

/**
 * An interpolator where the rate of change is constant
 */
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

可以看到,其中的 getInterpolation(float input) 方法的实现非常简单,直接将输入的值 input 作为结果返回,则实现了匀速变化的效果。

再简单分析下 AccelerateDecelerateInterpolator 的源码,如下所示:

public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory {
    public AccelerateDecelerateInterpolator() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
    }
}

通过数学做图工具,得到如下图所示的示意图:

cos.png

可以看到:

  • 0.00.3 之间时,y 轴随 x 轴变化较慢
  • 0.30.7 之间时,y 轴随 x 轴变化较快
  • 0.71.0 之间时,y 轴随 x 轴变化较又变慢

所以它是一个先加速再减速的过程

2.2 自定义 TimeInterpolator

通过对 LinearInterpolatorAccelerateDecelerateInterpolator 源码的分析,我们也可以实现一个自定义的 Interpolator

我们可以实现一个Interpolator。如下所示:

public class DecelerateAccelerateInterpolator implements BaseInterpolator {

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

使用数学做图工具,可以得到如下所示的变化示意图:

sin.png

可以看到:

  • 0.00.5 之间时,y 轴随 x 轴变化慢慢减缓
  • 0.51.0 之间时,y 轴随 x 轴变化慢慢加速
  • 在时间走到一半的时候,属性值已经到达了设定的结束值,然后在剩下的一半时间内,属性值又从设定的结束值恢复到初始值。

3.Animations in XML

属性动画允许在 xml 文件中定义动画,而不是通过 Java 代码的形式。使用 xml 定义动画,不仅使得重复利用相同的动画更加方便,而且也更容易修改动画。

为区分在 xml 中定义的视图动画,属性动画的 xml 文件放在 res/animator/ 目录下。

属性动画在 xml 中支持以下几个标签:

  • ValueAnimator -- <animator>
  • ObjectAnimator -- <objectAnimator>
  • AnimatorSet -- <set>

可以在 这儿Animation Resources 找到属性动画中的所有属性。

以下代码是一个简单的 xml 属性动画的例子:

<set android:ordering="sequentially">
    <set>
        <objectAnimator
            android:propertyName="x"
            android:duration="500"
            android:valueTo="400"
            android:valueType="intType"/>
        <objectAnimator
            android:propertyName="y"
            android:duration="500"
            android:valueTo="300"
            android:valueType="intType"/>
    </set>
    <objectAnimator
        android:propertyName="alpha"
        android:duration="500"
        android:valueTo="1f"/>
</set>

xml 中定义好属性动画之后,在 Java 代码中使用该动画的话,需要先将该动画集合加载进来,之后调用 setTarget() 为该动画设置目标对象即可,如下所示:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
    R.anim.property_animator);
set.setTarget(myObject);
set.start();

xml 中也可以通过使用 <animator> 标签定义 ValueAnimator 动画,如下所示:

<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:valueType="floatType"
    android:valueFrom="0f"
    android:valueTo="-100f" />

Java 代码中使用上面定义的 ValueAnimator 动画,也需要将该 xml 动画加载进来,如下所示:

ValueAnimator xmlAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(this,R.animator.animator);
xmlAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator updatedAnimation) {
        float animatedValue = (float)updatedAnimation.getAnimatedValue();
        textView.setTranslationX(animatedValue);
    }
});
xmlAnimator.start();

关于在 xml 中定义属性动画更多的信息,请查阅 Animation Resources

4.硬件加速

从 Android 3.0(API level 11),Android 支持硬件加速绘制 2D 渲染,意思就是说在 View 上进行的一些 canvas 操作都使用 GPU 来实现。因为使用硬件加速需要更多的资源,所以会造成内存增加的后果。

API level 大于 14 的时候,硬件加速默认是打开的。如果你的应用只会绘制标准的 ViewDrawable 对象,全局都打开硬件加速并不会造成什么不好的影响。但是硬件加速并不是支持所有的的 2D 绘制操作的,打开硬件加速可能会造成你的自定义控件和一些绘制操作出现异常。为避免造成异常,Android 可以在几个不同的层级上对硬件加速进行开关的控制。

如果开启硬件加速之后,自定义控件和自定义绘制出现了问题,可以在真机上打开硬件加速进行测试,找出问题所在。

4.1 控制硬件加速的开关

总共可以在四个层级上对硬件加速进行控制,如下:

  • Application
  • Activity
  • Window
  • View

4.1.1 Applicatin 层级

AndroidManifest.xml 文件中,可以通过在 application 中使用以下元素对硬件加速进行控制

<application android:hardwareAccelerated="true" ...>

4.1.2 Activity 层级

如果在 Application 层级对硬件加速控制的不够精细,可以对某个单独的 Activity 中的硬件加速进行开关的控制,如下所示:

<application android:hardwareAccelerated="true">
    <activity ... />
    <activity android:hardwareAccelerated="false" />
</application>

上面代码的意思是,在应用的全局都打开硬件加速,但是在这个 Activity 中关闭了硬件加速。

4.1.3 Window 层级

可以对单独的某个 Window 进行硬件加速的控制,如下代码所示:

getWindow().setFlags(
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

Window 来说,只能打开硬件加速,而不能关闭硬件加速。

4.1.4 View 层级

使用以下代码,可以在运行的时候,把该 View 的硬件加速关闭

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

View 来说,只能关闭硬件加速,而不能打开硬件加速。

4.2 检测硬件加速是否打开

如果在应用中自定义控件和自定义绘制有很多的话,检查硬件加速是否处于打开状态还是非常有必要的,有以下两种方式可以判断:

View.isHardwareAccelerated();
Canvas.isHardwareAccelerated();
  • View.isHardwareAccelerated() 如果返回 true 可以说明此 View 依附的 Window 的硬件加速是打开的
  • Canvas.isHardwareAccelerated() 如果返回 true 则说明该 canvas 对象的硬件加速是打开的。

4.3 Android 绘制模式

如果硬件加速开启,Android FrameWork 会使用新的绘制模式将应用显示在屏幕上,这种新的绘制模式中使用到了一个关键的概念 ---- display lists

为了完全理解硬件加速相关的知识,首先理解 Android 不使用硬件加速是的软件绘制模式是非常有帮助的。

4.3.1 软件绘制模式

在软件绘制模式中,将 views 绘制到屏幕上需要以下两个步骤:

  • Invalidate the hierarchy
  • Draw the hierarchy

当应用需要更加 UI 中的某一部分时,有改变的 View (脏区域)需要调用 invalidate()invalidation 信号就会遍历 View 树来计算哪些 views 需要重新绘制,然后 Android 系统会将与这些 views 相关的 views 都进行重新绘制。这种绘制模式有两个缺点:

  • 这种模式在进行每一次重新绘制的时候,都会进行大量的代码计算。比如,在界面中有一个 button 在另一个 view 之上,该 button 调用了 invalidate() 方法,那么 Android 系统也会将该 button 之下的 view 进行重新绘制。
  • 另一个问题是,软件绘制模式会有一些隐藏的 bug。比如,有一个 view 的重新绘制依赖于另一个与其相关的 view 的重新绘制,当另一个 view 重新绘制的操作去掉之后,这个 view 的绘制工作也就不会被进行,所以可能会怀疑此 view 的代码有什么问题,这样就带来了问题。所以最好的方式是,在有所改变的自定义 view 中,主动去调用 invalidate() 方法进行重新绘制。

4.3.2 硬件加速绘制模式

在硬件加速绘制模式中,还是调用 incalidate() 方法和 draw() 方法进行重新绘制的,但是实际上处理绘制的操作还是有所不同的。

在硬件加速绘制模式中,Android 系统不会立即执行绘制指令,而是会先将其记录在 display lists 中,在 display lists 中存储着 view 树的绘制代码,Android 系统只需要将调用了 invalidate() 方法的 views 的绘制代码,记录和更新到 display lists 中即可。那些没有调用 incalidate() 方法的 views,只需要将原来存储于 display lists 中的绘制代码简单地绘制出来即可。

硬件加速绘制模式包括以下三个步骤:

  • Invalidate the hierarchy
  • Record and update display lists
  • Draw the display lists

使用这种方式,就不能再使用与脏区域相关的特性而执行本 viewdraw() 方法了。为了使 Android 系统记录和更新一个 viewdisplay lists 的绘制代码,必须调用该 viewinvalidate() 方法了,如果忘记调用 incalidate() 方法,则该 view 不会发生什么变化。

使用 display lists 对属性动画的性能也有很大的好处,一些属性(比如:透明度、旋转角度)并不需要调用 invalidate() 方法即可完成变化,它完全可以自动完成计算。这种特性也会应用在一些普通 View 的操作上。比如,现在有一个 LinearLayout 包含一个 ListView 和一个 Button,此时该 LinearLayoutdisplay lists 是这样的:

  • DrawDisplayList(ListView)
  • DrawDisplayList(Button)

假如现在改变 ListView 的透明度,调用 ListViewsetAlpha(0.5f) 方法,那么此时,该 LinearLayoutdisplay lists 如下所示:

  • SaveLayerAlpha(0.5)
  • DrawDisplayList(ListView)
  • Restore
  • DrawDisplayList(Button)

复杂的 ListView 的绘制操作计算并没有被执行,相反,系统只需要更新该 LinearLayoutdisplay lists 即可。如果使用软件绘制模式,就会重新计算 ListViewButton 的绘制操作。

4.4 不支持硬件加速的绘制操作

大部分的 Canvas 的方法都支持硬件加速,但是有一些方法是从某些版本之后才开始支持硬件加速的,如下表所示:

unsupported_api1.png

unsupported_api2.png

4.5 View Layers(视图层级)

在 Android 所有的版本中,View 都有能力实现离屏缓冲,实现离屏缓冲的方式:

  • 可以通过 View 的绘制缓存
  • 通过 Canvas.saveLayer() 的方式

离屏缓冲有一些很好的用途:当进行于复杂视图的动画,或者一些组合效果时,使用离屏缓冲会有更好的性能。比如实现一个渐变的效果,通过使用 Canvas.saveLayer() 先临时将 view 缓冲到 layers (层级)上,然后再通过一个不透明因子将其绘制到屏幕上。

从 Android 3.0 (API level 11)开始,通过使用 View.setLayerType() 方法,对什么时候以及怎样使用视图层级有了更高的控制权限。在该方法中有两个参数:

  • 一个是视图的层级类型
    • LAYER_TYPE_NONEView 进行普通的渲染,并且不会使用离屏缓冲,这是默认参数
    • LAYER_TYPE_HARDWARE:如果应用启动了硬件加速,则使用硬件将 View 绘制到 hardware texture 上。如果硬件加速没有开启,则视图层级的操作和使用 LAYER_TYPE_SOFTWARE 参数时的视图操作是一致的
    • LAYER_TYPE_SOFTWARE:使用软件将该 View 先绘制到一个 Bitmap 对象上
  • 另一个是可选的 Paint 对象,该 Paint 对象可以用于描述怎样实现当前的层级。通过该 Paint 对象可以将一些 color filters (颜色过滤器)、opacity(不透明度)应用于该层级。

对于视图类型参数的选择,从以下几个因素考虑:

  • Performance(性能):使用 LAYER_TYPE_HARDWARE 参数会将 View 绘制到 hardware texture 之上,一旦该 View 绘制到 hardware texture 之上,那么它的绘制代码就不会改变,除非再次调用 invalidate() 方法。在一些动画中,比如透明度改变的动画,会使用 GPU 直接改变视图层级上的代码,这样更高效。
  • Visual effects:不论是使用 LAYER_TYPE_HARDWARE 还是使用 LAYER_TYPE_SOFTWARE 参数,都可以通过第二个参数 Paint 的对象,将一些特殊的视觉效果应用于该 View,比如通过对 Paint 对象使用 ColorMatrixColorFilter 参数,将 View 变为黑白的。
  • Compatibility(兼容性):有时候使用硬件加速,会有一些问题,这时候就要考虑使用 LAYER_TYPE_SOFTWARE 参数关闭硬件加速,通过使用软件绘制该 View 了。

4.6 视图层级和动画

如果应用开启了硬件加速,使用硬件层级绘制一些复杂的动画会更快且更平滑。如果有一些很复杂的动画,每秒中进行 60 帧的绘制会是一个问题,有时候会掉帧,这时候使用硬件加速将 View会知道 hardware texture 之上,就会有一定的改善。

View 不会进行重新绘制,除非调用了 invalidate() 方法。如果在应用中设置了一个动画,但是动画并不平滑,可以考虑使用硬件加速优化动画效果。

如果开启了硬件加速,在 View 中有一些属性的改变并不会对该 View 进行重新绘制操作,而是直接改变图层上的绘制代码:

  • alpha:改变图层的透明度
  • x, y, translationX, translationY:改变图层的位置
  • scaleX, scaleY:改变图层的大小
  • rotation, rotationX, rotationY:改变图层在 3D 空间的方向
  • pivotX, pivotY:改变图层变化的原点位置

例如下面这样的动画代码效率会更高,而且更平滑:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator.ofFloat(view, "rotationY", 180).start();

因为硬件加速会占用很高的内存,所以一般是在使用的时候开启硬件加速,不使用的时候关闭硬件加速,如下所示:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
animator.start();

4.7 一些建议

虽然使用硬件加速可以提高 View 的显示效果,提高性能,但是还是应该更加合理的使用硬件加速,不能滥用。对此给出以下几点建议:

  • 减少应用中 View 的数量。不论是使用硬件绘制还是使用软件绘制,都会占用资源,所以当然是绘制的 View 越少越好
  • 避免过渡绘制。在开发中会不可避免的遇到一些复杂的布局,这时很可能会出现多个 View 重叠的现象,而且布局文件可能也会嵌套的比较深,所以最好减少布局文件,将嵌套层次尽可能的降低。就目前的硬件条件来看,一个像素点最多不要超过 2.5 倍的绘制。简言之,就是嵌套层级尽可能少,不要对同一个像素点进行过多的绘制。
  • 在绘制方法中不要创建对象。因为像 onDraw() 这样的绘制方法,会不断的被调用,如果在其中创建对象,就会造成内存增长过快,垃圾回收器就会更频繁的调用。
  • 不要频繁修改外形。复杂的形状,比如shapes, paths, and circles,是使用 texture masks 进行绘制的。每次修改上面这些形状时,都会创建 texture masks,这样的代价还是比较大的。
  • 不要频繁的修改 Bitmap 对象。每一次修改 bitmap 中的内容,在绘制的时候都会上传至 GPU 中的 texture
  • 小心使用透明度。当使用 setAlphaAlphaAnimationObjectAnimator 来改变一个 View 的透明度时,它渲染到离屏缓冲时需要两倍的填充率。当需要改变一个很大的 View 的透明度时,考虑使用硬件加速来实现。

参考资料:

动画官方文档

硬件加速官方文档

HenCoder Android 自定义 View 1-7:属性动画 Property Animation(进阶篇) -- HenCoder

HenCoder Android 自定义 View 1-8 硬件加速 -- HenCoder

HenCoder Android 自定义 View 1-8 硬件加速 -- HenCoder

Android硬件加速原理与实现简介) -- 美团点评技术团队

关于硬件加速的那么点儿东西 -- 黑白咖

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

推荐阅读更多精彩内容