Animation动画概述和执行原理

1 Animation动画简介

Developers:https://developer.android.google.cn/reference/android/view/animation/package-summary

Android中动画非常常用,很多效果都需要动画的配合,android提供了多种动画类型,为创建多彩的android程序提供了支持。提供的动画类型包括:补间动画,帧动画,属性动画,补间动画和帧动画被称为视图动画。

对于Animation动画,android提供了两种机制来创建视图动画,
一种是tweened animation(补间动画),
一种是frame-by-frame animation(逐帧动画) 。
Tweened animation 可以实现view一系列简单的转换(位置,尺寸,旋转,透明度),
frame-by-frame 通过加载一系列drawable资源,实现动画。

视图动画只能作用于View,且动画类型是固定的。

补间动画:确定了view的开始的视图样式和结束的视图样式,动画过程中系统会补全变化中的状态,最终就实现了动画效果。

补间动画的种类:

  • translate (平移动画)
  • scale (缩放动画)
  • rotate (旋转动画)
  • alpha (透明度动画)

补间动画可以利用xml文件和动画类进行实现,对应的具体动画类:

  • translate(平移动画) 对应 TranslateAnimation
  • scale (缩放动画) 对应 ScaleAnimation
  • rotate (旋转动画) 对应 RotateAnimation类
  • alpha ( 透明度动画) 对应 AlphaAnimation 类

补间动画一般利用xml文件实现,如果利用xml文件实现动画,需要在res/anim文件夹下穿件动画文件。

2 Animation 基类

Animation作为补间动画的基类,具有许多动画公共的属性和方法:


在android.view.animation包下,可以看出是作用于view的。
直接子类有:AlphaAnimation,AnimationSet,RotateAnimation,ScaleAnimatioin,TranslateAnimation。
XML属性包括:

下面会列举Animation中公共属性在xml文件中的表示和代码类中的设置方式及效果:
每一项包括Animation中公共属性在xml文件中的表示和代码类中的设置方式及效果

  • android:detachWallpaper 对应setDetachWallpaper(boolean):是否在壁纸上运行,取值true,flase;
  • android:duration 对应setDuration(long):动画持续时间,参数单位为毫秒;
  • android:fillAfter 对应setFillAfter(boolean):动画结束时view是否保持动画最后的状态,默认值为false;
  • android:fillBefore 对应setFillBefore(boolean):动画结束时view是否还原到开始动画前的状态,和fillAfter行为是冲突的,所以只有当fillBefore为true或者fillEnabled不为true才生效。默认是true
  • android:fillEnabled 对应setFillEnabled(boolean):如果 fillEnabled 取值为true,animation将使用fillBefore的值,否则fillBefore将被忽略。都是在动画结束时还原到原来的状态。
  • android:interpolator 对应setInterpolator(Interpolator):设定插值器;
  • android:repeatCount对应setRepeatCount(int):动画重复次数,可以是具体次数,也可以是INFINITE(-1)一直循环。
  • android:repeatMode 对应setRepeatMode(int):重复类型有两个值,reverse表示倒序回放,restart表示从头播放,需要和repeateCount配合使用。
  • android:startOffset对应setStartOffset(long):调用start函数之后等待开始运行的时间,单位为毫秒;
  • android:zAdjustment 对应setZAdjustment(int)表示被设置动画的内容运行时在Z轴上的位置(top/bottom/normal),默认为normal,一般不需要设置。

Animation构造函数:一般情况用不到
Animation():duration默认0ms,default interpolator,fillBefore默认true,fillAfter默认false
Animation(Context context, AttributeSet attrs):利用attributeset和context初始化

3 动画开启的方法:start(),startNow()

这两个方法有什么区别呢?

/**
 * Convenience method to start the animation the first time
 * {@link #getTransformation(long, Transformation)} is invoked.
 */
public void start() {
    setStartTime(-1);
}

/**
 * Convenience method to start the animation at the current time in
 * milliseconds.
 */
public void startNow() {
    setStartTime(AnimationUtils.currentAnimationTimeMillis());
}

看两个函数的注释知道:
start()函数当getTransformation()第一次被调用的时候开始执行。
startNow()动画被立即执行
start和startNow内部都是调用setStartTime函数,setStartTime函数是设置动画开始执行的时间。start函数设置setStartTime(-1)会等待getTransformation第一次执行时才开始执行动画,startNow是setStartTime(AnimationUtils.currentAnimationTimeMillis()设置了具体的开始时间,动画会立刻开始执行。
所以start函数调用后不是立即执行动画,startNow是立即执行动画。

4 动画真正实现的地方在哪里

Animation是动画的基类,所以具体动画的操作一定在其子类中,通过分析可知道,最终实现动画操作在Animation类的applyTransformation()方法中,各个子类会实现这个方法,进行动画操作。

/**
 * Helper for getTransformation. Subclasses should implement this to apply
 * their transforms given an interpolation value.  Implementations of this
 * method should always replace the specified Transformation or document
 * they are doing otherwise.
 *
 * @param interpolatedTime The value of the normalized time (0.0 to 1.0)
 *        after it has been run through the interpolation function.
 * @param t The Transformation object to fill in with the current
 *        transforms.
 */
protected void applyTransformation(float interpolatedTime, Transformation t) {
}

从Animation类内部可知applyTransformation()函数会被getTransformation()函数调用。Transformation类包括matrix,scale,clip等变换信息。

getTransformation()内部调用了applyTransformation(),来看看getTransformation内部的逻辑:

getTransformation()

getTransformation函数内部判断动画是否执行完毕,如果执行完毕返回false,如果动画还没有执行完返回true.

public boolean getTransformation(long currentTime, Transformation outTransformation) {
//如果mStartTime == -1,初始化动画开始时间
    if (mStartTime == -1) {
        mStartTime = currentTime;
    }

//计算动画已经执行到的位置
    final long startOffset = getStartOffset();
    final long duration = mDuration;
    float normalizedTime;
    if (duration != 0) {
        normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                (float) duration;
    } else {
              normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
    }

//判断动画是否被取消或者时间超过1.0f,为true表示动画结束或者已经被取消
    final boolean expired = normalizedTime >= 1.0f || isCanceled();
//设置动画是否完成标识
    mMore = !expired;
//处理fillEnable
    if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
//处理其他参数
    if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            fireAnimationStart();
            mStarted = true;
            if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
        }

        if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if (mCycleFlip) {
            normalizedTime = 1.0f - normalizedTime;
        }

        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//执行动画具体操作
        applyTransformation(interpolatedTime, outTransformation);
    }

//如果动画已经结束,判断重复执行操作
    if (expired) {
        if (mRepeatCount == mRepeated || isCanceled()) {
            if (!mEnded) {
                mEnded = true;
                guard.close();
                fireAnimationEnd();
            }
        } else {
    if (mRepeatCount > 0) {
                mRepeated++;
            }

            if (mRepeatMode == REVERSE) {
                mCycleFlip = !mCycleFlip;
            }

            mStartTime = -1;
            mMore = true;

            fireAnimationRepeat();
        }
    }

//动画还没有执行完
    if (!mMore && mOneMoreTime) {
        mOneMoreTime = false;
        return true;
    }

//所以mMore表示动画是否执行完了,为true时表示还没有执行完
    return mMore;
}

getTransformation函数又是在哪里执行的呢?

5 View 如何执行动画

分析getTransformation在哪里执行我们需要先分析View如何执行动画。
一般的步骤是定义好Animation对象设置属性之后,调用startAnimation()函数。
View中startAnimation函数源码:

/**
 * Start the specified animation now.
 *
 * @param animation the animation to start now
 */
public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}

首先设置了START_ON_FIRST_FRAME表示,它的值为-1,相当于调用了Animation的start()函数,然后调用了setAnimation设置了animation方法,之后调用了invalidateParentCaches和invalidate函数。startAnimation这个函数的作用是立即开始执行动画,所以我们就知道了执行动画需要设置以上四个参数。

再看View 的setAnimation的方法:

/**
 * Sets the next animation to play for this view.
 * If you want the animation to play immediately, use
 * {@link #startAnimation(android.view.animation.Animation)} instead.
 * This method provides allows fine-grained
 * control over the start time and invalidation, but you
 * must make sure that 1) the animation has a start time set, and
 * 2) the view's parent (which controls animations on its children)
 * will be invalidated when the animation is supposed to
 * start.
 *
 * @param animation The next animation, or null.
 */
public void setAnimation(Animation animation) {
    mCurrentAnimation = animation;

    if (animation != null) {
              if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
                && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
            animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
        }
        animation.reset();
    }
}

setAnimation 把animation对象设置给了mCurrentAnimation,然后设置了animation的startTime,最后调用了animation的reset函数。
仔细阅读注释:调用了setAnimation 方法后,如果想让动画执行需要两个条件,第一个是有个开始执行的时间,另外一个是view的父类调用了invalidated方法,这样动画才会执行。
所以还得继续观察invalidateParentCaches函数,内部只是设置了表示,再看invalidate(true)方法。

/**
 * This is where the invalidate() work actually happens. A full invalidate()
 * causes the drawing cache to be invalidated, but this function can be
 * called with invalidateCache set to false to skip that invalidation step
 * for cases that do not need it (for example, a component that remains at
 * the same dimensions with the same content).
 *
 * @param invalidateCache Whether the drawing cache for this view should be
 *            invalidated as well. This is usually true for a full
 *            invalidate, but may be set to false if the View's contents or
 *            dimensions have not changed.
 * @hide
 */
public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

invalidate() 内部其实是调用了 ViewGroup 的 invalidateChild(),内部会一直向上会执行 ViewRootImpl 的 invalidateChildInParent() ,最终触发的是ViewRootImpl 的 performTraversals(),进而执行view的测量,布局,绘制工作。(具体流程会在后续分析view绘制流程时讲解)。

所以需要执行动画时,最终会触发一次view树形结构的遍历绘制工作,动画的执行应该在view的绘制过程中进行。

看View类顶部关于Animation的注释:

* You can attach an {@link Animation} object to a view using
* {@link #setAnimation(Animation)} or
* {@link #startAnimation(Animation)}. The animation can alter the scale,
* rotation, translation and alpha of a view over time. If the animation is
* attached to a view that has children, the animation will affect the entire
* subtree rooted by that node. When an animation is started, the framework will
* take care of redrawing the appropriate views until the animation completes.
* </p>

最后一句当animation 开始运行后,framework 将关注重新绘制view视图知道动画结束,所以动画跟随view的绘制一起执行。对应上面的结论,动画开始时会触发view树的重新绘制。

View绘制过程中会调用view的draw方法,draw方法内部会调用applyLegacyAnimation。

//**
 * Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
 * case of an active Animation being run on the view.
 */
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
        Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
//动画还没有初始化,就初始化动画并告诉子view,当前view添加了动画
if (!initialized) {
        a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
        a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
        if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
        onAnimationStart();
    }

    final Transformation t = parent.getChildTransformation();
//获取动画是否执行完
    boolean more = a.getTransformation(drawingTime, t, 1f);
    if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
        if (parent.mInvalidationTransformation == null) {
            parent.mInvalidationTransformation = new Transformation();
        }
        invalidationTransform = parent.mInvalidationTransformation;
        a.getTransformation(drawingTime, invalidationTransform, 1f);
    } else {
        invalidationTransform = t;
    }

//如果动画没有结束,循环调用,会触发view树的遍历绘制
    if (more) {
        if (!a.willChangeBounds()) {
            if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                    ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
            } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                             parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                parent.invalidate(mLeft, mTop, mRight, mBottom);
            }
        } else {
            if (parent.mInvalidateRegion == null) {
                parent.mInvalidateRegion = new RectF();
            }
            final RectF region = parent.mInvalidateRegion;
            a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                    invalidationTransform);

                     parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

            final int left = mLeft + (int) region.left;
            final int top = mTop + (int) region.top;
            parent.invalidate(left, top, left + (int) (region.width() + .5f),
                    top + (int) (region.height() + .5f));
        }
    }
    return more;
}

applyLegacyAnimation这个函数内部调用了getTransformation函数,最终动画得到执行,getTransformation函数会返回动画是否完成的状态,完成为false,没完成为true,如果没有完成会再次遍历view树进行绘制。

所以viewgroup下的任何一个view执行动画,那么都会导致view执行整个绘制流程,最终会调用viewGroup的dispatchDraw()然后内部又调用drawChild去绘制各个子View,子view内部调用draw方法绘制自身。

view 动画怎么绘制的呢?

既然知道了动画是在view的draw函数中绘制的,我们看一下view的draw函数。
draw三个参数的方法:
可以看到内部获取了Animation和getChildTransformation,然后对画布进行了变换,就实现了对view的动画操作。

 /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        boolean more = false;
        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        //获取动画
        final Animation a = getAnimation();
        if (a != null) {
        //有动画,通知执行
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            //获取Transformtion信息
            transformToApply = parent.getChildTransformation();
        } else {
            。。。。。。
        }
        。。。。。。。
        int restoreTo = -1;
        //执行动画之前保存画布
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        //对画布进行操作
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;

                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }

                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }

                if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }

            // Deal with alpha if it is or used to be <1
            if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
                if (alpha < 1) {
                    mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                } else {
                    mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                }
                parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                if (!drawingWithDrawingCache) {
                    final int multipliedAlpha = (int) (255 * alpha);
                    if (!onSetAlpha(multipliedAlpha)) {
                        if (drawingWithRenderNode) {
                            renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
                        } else if (layerType == LAYER_TYPE_NONE) {
                            canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
                                    multipliedAlpha);
                        }
                    } else {
                        // Alpha is handled by the child directly, clobber the layer's alpha
                        mPrivateFlags |= PFLAG_ALPHA_SET;
                    }
                }
            }
        } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
            onSetAlpha(255);
            mPrivateFlags &= ~PFLAG_ALPHA_SET;
        }
      。。。。。。。

        //恢复画布
        if (restoreTo >= 0) {
            canvas.restoreToCount(restoreTo);
        }
        if (more && hardwareAcceleratedCanvas) {
            if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
                // alpha animations should cause the child to recreate its display list
                //还有动画继续通知
                invalidate(true);
            }
        }

        mRecreateDisplayList = false;

        return more;
    }

绘制子view都会先对画布状态进行保存save(),绘制完后,又会恢复restore(),所以一个view的绘制不会影响另外一个子view的绘制,但如果该view是viewgroup,会影响到其所有的子view的绘制,所以动画发生时不是类似调用invalidate,只绘制view自身,而是由上而下,重绘ViewGroup导致了绘制子View,子view绘制,只是变换了自己所在的画布的坐标系,其实属性没有改变。
Android动画就是通过父view来不断调整子view的画布canvas坐标系来实现的,发生动画的其实是父View而不是该view。所以 补间动画其实只是调整了子view画布canvas的坐标系,其实并没有修改任何属性,所以只能在原位置才能处理触摸事件。

以上我们反向推导了动画执行的过程,下面总结一下:
当view调用了 View.startAnimation() 时动画并没有马上就执行,会触发遍历view树的绘制,
调用到 View 的 draw() 方法,如果 View 有绑定动画,那么会去调用applyLegacyAnimation(),内部调用 getTransformation() 来根据当前时间计算动画进度,紧接着调用 applyTransformation() 并传入动画进度来应用动画。getTransformation() 会返回动画是否执行完成的状态, applyLegacyAnimation() 会根据 getTransformation() 的返回值来决定是否通知 ViewRootImpl 再发起一次遍历请求,遍历 View 树绘制,重复上面的步骤,直到动画结束。

补间动画的绘制实际上是父布局不停地改变自己的Canvas坐标,而子view虽然位置没有变化,但是画布所在Canvas的坐标发生了变化视觉效果也就发生了变化,其实并没有修改任何属性,所以只能在原位置才能处理触摸事件。

Animation动画概述和执行原理
Android动画之补间动画TweenAnimation
Android动画之逐帧动画FrameAnimation
Android动画之插值器简介和系统默认插值器
Android动画之插值器Interpolator自定义
Android动画之视图动画的缺点和属性动画的引入
Android动画之ValueAnimator用法和自定义估值器
Android动画之ObjectAnimator实现补间动画和ObjectAnimator自定义属性
Android动画之ObjectAnimator中ofXX函数全解析-自定义Property,TypeConverter,TypeEvaluator
Android动画之AnimatorSet联合动画用法
Android动画之LayoutTransition布局动画
Android动画之共享元素动画
Android动画之ViewPropertyAnimator(专用于view的属性动画)
Android动画之Activity切换动画overridePendingTransition实现和Theme Xml方式实现
Android动画之ActivityOptionsCompat概述
Android动画之场景变换Transition动画的使用
Android动画之Transition和TransitionManager使用
Android动画之圆形揭露动画Circular Reveal
Android 动画之 LayoutAnimation 动画
Android动画之视图动画的缺点和属性动画的引入

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

推荐阅读更多精彩内容