属性动画详解

1. 动画分类

Android 中动画分为 3 种:View 动画(视图动画)、帧动画、属性动画。

(1)帧动画:将一系列的图片按照顺序播放,每一张图片就是动画中的一帧,连续播放后就形成了动画,使用起来比较简单,缺点是当图片过多或者过大是,容易导致 OOM。

(2)View 动画:动画变化分为 4 种,平移、缩放、旋转、透明度,通过这 4 种动画其中的一种变换或者组合变换,使视图完成一种渐进式的动画效果。

(3)属性动画:是在 Android 3.0(API 11)才提供动画库。属性动画不仅可以使用自带的 API 来实现最常用的动画,而且通过自定义 View 的方式来做出定制化的动画,相比于 View 动画功能更强大。

  • View 动画只能作用在单一视图上,即只对一个 Button、TextView、或者 ViewGroup,不能作用于非 View 对象的属性,如改变视图颜色属性、自定义 View 时的路径改变的动画效果等,这些通过 View 动画难以实现,通过属性动画可以很好的完成。
  • View 动画的效果只有 4 种,很难完成更复杂的动画效果。
  • View 动画不能控件的属性,如通过 View 动画移动一个 Button,移动后点击 Button 显示的位置,并不能触发点击事件,但是点击 Button 原来的位置,可以触发点击事件,可见 View 动画不能改变控件的属性,只是显示的效果改变了而已。

从以上这 3 点可以看出,属性动画的优势,目前多数动画都是采用属性动画实现的,很少存在兼容性问题,因为在 API 11 以前的手机基本很少有人使用了。

下面就来一起学习下属性动画。

2. 属性动画

animator_content.png

属性动画中了解上图中的这些内容,基本可以完成日常的开发,其中按照常用的排序:

ViewPropertyAnimator --> ObjectAnimator --> ValueAnimator

当然这是单一动画的选择顺序,按照这个顺序使用起来会很方便,如果是使用 AnimationSet,组合动画中每个动画,一般使用 ObjectAnimator 来构建。

2.1 ValueAnimator

但是我们还是先来看 ValueAnimator,为啥呢?因为它是 ViewPropertyAnimator 和 ObjectAnimator 的底层实现,ObjectAnimator 还是继承它的。

主要原理:ValueAnimator 实际上是对 int 值、float 值、对象值来进行控制,有了初始值和结束值,以及持续时间,来得到每个时间点的值,但是得到了值,并不能关联到我们要控制的控件或者视图的属性上,这时就需要手动将时间点的值赋给要控制的对象,并刷新对象,从而实现对象的动画过渡效果。

原理可能有点不太直观,下面来看看具体的操作。

ValueAnimator.ofInt(int values)
ValueAnimator.ofFloat(float values)
ValueAnimator.ofObject(int values)

ValueAnimator 操作值有 3 种方法,这里主要演示 ofInt 和 ofObject,ofFloat 和 ofInt 类似

(1)ofInt

方式一:使用代码实现

final TextView textView = findViewById(R.id.text_view);
final ValueAnimator valueAnimator1 = ValueAnimator.ofInt(16, 48, 10);
// ofInt 动画,改变 TextView 的字体大小
valueAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int animatedValue = (int) animation.getAnimatedValue();
        textView.setTextSize(animatedValue);
    }
});
// Button 点击事件
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        valueAnimator1.setDuration(4000);
        valueAnimator1.start();
    }
});

上述代码逻辑很简单,通过一个点击按钮开启动画,动画 ValueAnimator 完成,ofInt 方法设置了 3 个值,控制 TextView 的字体大小,从 16 过渡到 48,再到 10,仅仅有这些还不够,还需要手动设置控件的字体,所以需要设置 AnimatorUpdateListener,在得到值后,更新控件的字体大小。

方式二:使用 XML 实现

在工程目录 res/animator/value_animator.xml 动画 XML 文件中设置动画的参数

<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="4000"
    android:fillAfter="false"
    android:fillBefore="true"
    android:fillEnabled="true"
    android:repeatCount="1"
    android:repeatMode="restart"
    android:valueFrom="16"
    android:valueTo="48"
    android:valueType="intType" />

然后加载动画,设置监听 AnimatorUpdateListener

// 2.XML 方式
final ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(ValueAnimatorActivity.this, R.animator.value_animator);
// 设置需要动画的控件
animator.setTarget(textView);
// 设置动画更新监听 AnimatorUpdateListener
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int animatedValue = (int) animation.getAnimatedValue();
        textView.setTextSize(animatedValue);
    }
});

最后同样的操作开启动画

animator.start();
animator_ofint.gif

(2)ofObject

首先看一下 ofObject 方法的参数,第一个参数是一个估值器,后面是对象的协变参数,为什么多了一个估值器?

public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) 

对于 ofInt 方法,没有估值器参数,实际上已经具备系统内置的估值器 IntEvaluator,内置的估值器已经实现从开始值到结束值的过渡过程,能够得到不同时刻的 int 值,同理,对于 ofFloat()方法,也内置了 FloatEvaluator。

ofInt(int... values)

对于 ofObject() 方法,系统没有提供估值器,因为系统不知道我们我们要传入的对象的类型,所以需要我们自己来实现一个估值器。

这里我们自定义一个类 MyPoint,通过 MyPoint 对象来表示控件的位置

public class MyPoint {

    private int x;
    private int y;

    public MyPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }
}

然后实现一个估值器,通过这个估值器来得到从开始值到结束值之间的不同时刻下的值,其中 fraction 由插值器给出,代表动画的进度 startValue 和 endValue 表示开始值和结束值。

public class PointEvaluator implements TypeEvaluator<MyPoint> {

    @Override
    public MyPoint evaluate(float fraction, MyPoint startValue, MyPoint endValue) {
        int x = (int) (fraction * (endValue.getX() - startValue.getX()) + startValue.getX());
        int y = (int) (fraction * (endValue.getY() - startValue.getY()) + startValue.getY());
        return new MyPoint(x, y);
    }
}

接下来就可以使用自定义的估值器通过 ofObject 来构建 ValueAnimator,这里只给出通过代码实现的方式。

// ofObject 动画
final ValueAnimator valueAnimator2 = ValueAnimator.ofObject(new PointEvaluator(),
        new MyPoint(30, 30), new MyPoint(500, 500));
// 同样需要设置更新监听,得到更新值后,手动通过 myPoint 表示的位置来设置 textView 位置
valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        MyPoint myPoint = (MyPoint) animation.getAnimatedValue();

        textView.layout(myPoint.getX(), myPoint.getY(),
                myPoint.getX() + textView.getWidth(), myPoint.getY() + textView.getHeight());
    }
});
// 设置点击事件,开启动画
findViewById(R.id.of_object_button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        valueAnimator2.setDuration(4000);
        valueAnimator2.start();
    }
});
animator_ofobject.gif

2.2 ObjectAnimator

这里为了演示使用 ObjectAnimator 的动画效果,使用了自定义的一个画矩形的 View。

public class RectView extends View {

    private Paint mPaint;
    private String color;

    public RectView(Context context) {
        super(context);
        initView();
    }

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

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

    private void initView() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.GREEN);
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
        mPaint.setColor(Color.parseColor(color));
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth() >> 1, getHeight() >> 1);
        canvas.drawRoundRect(-100.0f, -100.0f, 100.0f, 100.0f,
                10, 10, mPaint);
    }
}

然后通过 ObjectAnimator 来创建动画,使这个圆绕 X 轴旋转。


ObjectAnimator.ofFloat(rectView, "rotation", 0, 360.0f)
                        .setDuration(4000)
                        .start();
animator_object1.gif

这样就开启了动画,不需要像 ValueAnimator 一样设置更新监听,手动赋值并刷新 View。只需要给出需要更改的属性即可。ofFloat() 方法的第一个参数是自定义绘制矩形的 View 对象,rotation 属性是基类 View 的有一个属性,代表围绕屏幕旋转角度,0 和 360.0f 表示开始时的角度值和结束值。这样开启动画后就在指定时间内矩形围绕屏幕方向的轴从 0 旋转到 360。除了 "rotation" 属性, View 的属性还有以下属性

animator_property1.png

只要将这些属性参数设置到 ObjectAnimator.ofFloat() 方法中,ObjectAnimator 就会根据属性参数找到对应的属性(前提是 该对象存在这个属性),然后进行自动赋值,实现动画效果。另外,我们还可以通过 ObjectAnimator 改变自定义 的属性,下面就来展示一下自定义属性的改变效果。

View 基类中没有 "color" 这样的一个颜色属性,那么我们就在上述的自定义矩形 View 中添加一个 color 的属性,注意在 setColor 方法中,首先解析出 color 颜色值外,还要重新绘制矩形,这样颜色值才会生效,ObjectAnimator 的自动赋值,就是通过这个 setXX 方法来完成的。

ObjectAnimator.ofObject(rectView, "color", new ColorEvaluator(), "#00FF00", "#FF0000")
                        .setDuration(4000)
                        .start();
animator_object2.gif

注意: 要想自定义属性 xx 生效,需要满足下面的两个条件:

  • 操作的对象需要提供 setXX 方法,另外如果方法中没有传递开始值,还需要提供 getXX 方法,ObjectAnimator 会从 getXX 方法中获取初始值,如果不提供,程序会 crash

  • 提供了 setXX 方法,仅仅是将值传递给了对象,如果想要达到某种效果,还需要我们自己来设置,如改变颜色需要调用 invalidate() 重绘,改变尺寸布局等调用 requestLayout() 方法。

2.3 ViewPropertyAnimator

ViewPropertyAnimator 是谷歌提供的更加方便实现 View 动画的类,使用方式:View.animate() 后跟 translationX() 等方法,动画会自动执行。

textView.animate()
        .translationYBy(-100)
        .alphaBy(-0.1f)
        .scaleX(1.5f)
        .rotationBy(180)
        .setDuration(4000)
        .start();
animator_property.gif

View 的每个方法都对应了 ViewPropertyAnimator 的两个方法,其中一个是带有 -By 后缀的,例如,View.setTranslationX() 对应了 ViewPropertyAnimator.translationX() 和 ViewPropertyAnimator.translationXBy() 这两个方法。其中带有 -By() 后缀的是增量版本的方法,例如,translationX(100) 表示用动画把 View 的 translationX 值渐变为 100,而 translationXBy(100) 则表示用动画把 View 的 translationX 值渐变地增加 100。

animator_property.png

2.4 AnimationSet

有时我们在改变一个控件或者视图时,可能需要改变多个属性的动画效果,而且不同的属性改变时的先后顺序,也有一定的要求,比如先拉伸,然后改变颜色,最后再旋转,这时就需要 AnimationSet 来完成一系列的动画组合。AnimationSet 的几个主要方法。

AnimatorSet.play(Animator anim)   // 播放当前动画
AnimatorSet.after(long delay)   // 将现有动画延迟x毫秒后执行
AnimatorSet.with(Animator anim)   // 将现有动画和传入的动画同时执行
AnimatorSet.after(Animator anim)   // 将现有动画插入到传入的动画之后执行
AnimatorSet.before(Animator anim) // 将现有动画插入到传入的动画之前执行
AnimatorSet.playSequentially // 各个动画按照顺序执行,前一个执行完,后面的再开始执行

示例:对自定义的矩形 View,先拉伸,然后改变颜色,最后再旋转

RectView rectView = findViewById(R.id.circle_view);
final ObjectAnimator animator1 = ObjectAnimator.ofFloat(rectView, "scaleX", 1, 2)
        .setDuration(2000);
final ObjectAnimator animator2 = ObjectAnimator.ofObject(rectView,
        "color", new ColorEvaluator(), "#0000FF", "#FF0000")
        .setDuration(2000);
final ObjectAnimator animator3 = ObjectAnimator.ofFloat(rectView, "rotation", 0, 450.0f)
        .setDuration(3000);

findViewById(R.id.start_btn).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playSequentially(animator1, animator2, animator3);
        animatorSet.start();
    }
});
animator_set.gif

2.5 估值器

在上述过程中我们已经接触了估值器,估值器完成的工作就是给出不同进度下的值,然后 ObjectAnimator 或者 ValueAnimator 拿到值后再进行对对象的操作。下面以 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));
    }
}

其实很很简单,就是这样一个线性的数学公式 result = min + k * (max - min),其系数 k 就是提到的进度,那么进度是怎么来的呢?这个进度是由插值器给出的,所以需要得到不同进度下的值,需要先知道进度,进度由插值器给出,插值器和估值器配合工作来得到不同进度下的值。

注意:上面提到的值,也可以是对象,道理是同样的。

2.6 插值器

插值器是来获取进度的,那么如何给出进度的?所有的插值器都是实现 TimeInterpolator 接口,随着时间的发展,给出不同时刻属性变化的百分比,这个百分比就是进度,可能用进度不是很准确,总之插值器给出这个百分比之后,最后给到估值器,估值器通过这个百分比计算对应时刻的值。

public interface TimeInterpolator {
    float getInterpolation(float input);
}

常见的插值器就是 LinearInterpolator,是一个匀速插值器,实际上就是 y = x,这样一个数学公式,x 代表时间,随着时间流逝,输出 y ,代表属性变化的百分比,LinearInterpolator 完成的就是随时间均速变化的效果。

对于 AccelerateInterpolator 加速度插值器,是利用 y=x^2 这个数学公式给出属性变化的百分比。

当需要改变插值器时,通过 setInterpolator(Interpolator interpolator) 设置 Interpolator
Interpolator 其实就是速度设置器,在参数里填入不同的 Interpolator ,动画就会以不同的速度模型来执行。

AccelerateDecelerateInterpolator  // 先加速再减速

LinearInterpolator // 匀速

AccelerateInterpolator // 加速

DecelerateInterpolator // 持续减速直到 0

AnticipateInterpolator // 回拉一下再进行正常动画轨迹

OvershootInterpolator // 动画会超过目标值一些,然后再弹回来

AnticipateOvershootInterpolator // 上面这两个的结合版:开始前回拉,最后超过一些然后回弹

BounceInterpolator // 在目标值处弹跳

CycleInterpolator // 正弦 / 余弦曲线模型

PathInterpolator // 自定义动画完成度 / 时间完成度曲线。

FastOutLinearInInterpolator // 加速模型,曲线公式是用的贝塞尔曲线

FastOutSlowInInterpolator // 先加速再减速。用的是贝塞尔曲线

LinearOutSlowInInterpolator // 持续减速

2.7 动画监听

(1)基类 Animation 中有一个监听 AnimatorListener ,可以监听动画开始、结束、重复、取消时刻,从而来进行一系列操作。

ObjectAnimator、ValueAnimator、AnimatorSet 都是继承自 Animation,所以都可以设置该监听,

(2)此外,在上面讲解 ValueAnimator 时,看到它还有另外一个监听 ValueAnimator.AnimatorUpdateListener,在值变化时,通过该监听得到不同时刻的值,从而对对象设置值,改变对象属性。

(3)有时我们可能不需要监听动画个多个时刻,如仅仅需要监听结束时刻,然后执行我们想要执行的一个动作,那对于其他时刻就没必要重写,AnimatorListenerAdapter 就是来满足这个需求的,我们可以根据需要的监听时刻进行重写。

public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener,
        Animator.AnimatorPauseListener {

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationCancel(Animator animation) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationEnd(Animator animation) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationRepeat(Animator animation) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationStart(Animator animation) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationPause(Animator animation) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAnimationResume(Animator animation) {
    }
}

3. 属性动画工作原理

有了上面的基础,来看看属性动画的工作原理(以 ObjectAnimator 为例):

(1)ObjectAnimator 的 start 方法最终调用的是 ValueAnimator 的 start(boolean playBackwards) 方法,该方法中完成了 2 项工作,设置包装属性的 PropertyValuesHolder,并设置当前的进度。

private void start(boolean playBackwards) {
    ...
    addAnimationCallback(0);

    if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
        
        // 动画初始化,准备 PropertyValuesHolder 数组
        startAnimation();
        if (mSeekFraction == -1) {
            // No seek, start at play time 0. Note that the reason we are not using fraction 0
            // is because for animations with 0 duration, we want to be consistent with pre-N
            // behavior: skip to the final value immediately.
            setCurrentPlayTime(0);
        } else {
            // 设置当前进度
            setCurrentFraction(mSeekFraction);
        }
    }
}

(2)接着看 setCurrentFraction 方法设置当前动画的进度,然后根据进度执行动画,最后一行 animateValue(currentIterationFraction);

public void setCurrentFraction(float fraction) {
    initAnimation();
    fraction = clampFraction(fraction);
    mStartTimeCommitted = true; // do not allow start time to be compensated for jank
    if (isPulsingInternal()) {
        long seekTime = (long) (getScaledDuration() * fraction);
        long currentTime = AnimationUtils.currentAnimationTimeMillis();
        // Only modify the start time when the animation is running. Seek fraction will ensure
        // non-running animations skip to the correct start time.
        mStartTime = currentTime - seekTime;
    } else {
        // If the animation loop hasn't started, or during start delay, the startTime will be
        // adjusted once the delay has passed based on seek fraction.
        mSeekFraction = fraction;
    }
    mOverallFraction = fraction;
    final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
    animateValue(currentIterationFraction);
}

(3)根据当前动画的进度,可以计算出对应的属性值,计算过程由 PropertyValuesHolder,毕竟属性是包装在 PropertyValuesHolder 中。这个计算的过程就不详细分析了,里面通过调用 Keyframes 来完成,对于 int 类型和 float 类型有默认的估值器 IntEvaluator 和 FloatEvaluator,如果是 Object 类型的,会使用我们自己实现的估值器。

void animateValue(float fraction) {
    fraction = mInterpolator.getInterpolation(fraction);
    mCurrentFraction = fraction;
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction);
    }
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}

(4)最后看一下,得到的对应属性,是如何设置到对象当中的,即怎么调用 set 方法。

// 设置属性值
void setAnimatedValue(Object target) {
    if (mProperty != null) {
        mProperty.set(target, getAnimatedValue());
    }
    if (mSetter != null) {
        try {
            mTmpValueArray[0] = getAnimatedValue();
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }
}

(5)设置属性值是在 setAnimatedValue 方法中完成的,是通过反射调用对象的 setXX 方法,这样就将当前动画进度的值赋给的对象,像上面的我们自定义 View 中的 setXX 方法,还有重绘操作,这样设置颜色值后就更新了 UI。

此外,上面 2 个条件中提到,如果没有设置初始值,还需要提供 getXX 方法,该方法也是通过反射调用的,在 PropertyValuesHolder 的 setupValue 方法中执行的。

private void setupValue(Object target, Keyframe kf) {
    if (mProperty != null) {
        Object value = convertBack(mProperty.get(target));
        kf.setValue(value);
    } else {
        try {
            if (mGetter == null) {
                Class targetClass = target.getClass();
                setupGetter(targetClass);
                if (mGetter == null) {
                    // Already logged the error - just return to avoid NPE
                    return;
                }
            }
            Object value = convertBack(mGetter.invoke(target));
            kf.setValue(value);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }
}

4. 小例子

比较简单的一个小例子,实现 TextView 的 ZoomIn 的效果。

    TextView textView = findViewById(R.id.hello_world);
    AnimatorSet animatorSet = new AnimatorSet();
    ObjectAnimator animator1 = ObjectAnimator.ofFloat(textView, "scaleX", 0.45f, 1);
    animator1.setRepeatMode(ValueAnimator.RESTART);
    animator1.setRepeatCount(ObjectAnimator.INFINITE);
    animator1.setDuration(3000);
    ObjectAnimator animator2 = ObjectAnimator.ofFloat(textView, "scaleY", 0.45f, 1);
    animator2.setRepeatMode(ValueAnimator.RESTART);
    animator2.setRepeatCount(ObjectAnimator.INFINITE);
    animator2.setDuration(3000);
    ObjectAnimator animator3 = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
    animator3.setRepeatMode(ValueAnimator.RESTART);
    animator3.setRepeatCount(ObjectAnimator.INFINITE);
    animator3.setDuration(3000);
    animatorSet.playTogether(animator1, animator2, animator3);
    animatorSet.start();
animator_sample.gif

5. 参考

官方文档

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

Android 属性动画:这是一篇很详细的 属性动画 总结&攻略

代码地址

练习代码

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

推荐阅读更多精彩内容

  • 星巴克从1999年踏入中国市场以来,常年稳居咖啡界C位。但从2018年开始,星巴克却遭遇了中国市场的“滑铁卢”——...
    六点一十阅读 527评论 0 0
  • 情绪型嫉妒 可能是在同一个屋子里面一起生活的家人,可能是过去曾经一起上学的老同学,可能是办公室里面的同事,如果所有...
    三不主义阅读 368评论 0 0
  • 作为一个大龄青年,所剩不多的爱好就是把玩折腾一些数码产品。 看了看现在的房价,感觉这个爱好也快要放弃了,才发现貌似...
    zainnn阅读 102评论 0 0