Android源码解析-- Animator动画

Android在3.0版本中引入了新的动画实现:属性动画。我们一般称之为Animator。这种动画通过变更控件属性达到动画效果。其中,属性动画最重要的一点,就是控制了动画的时序,我们不妨来看下属性动画的简单用法:

//code1
ValueAnimator animator = ValueAnimator.ofInt(0,100)//line1
                .setDuration(100);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator valueAnimator) {
                        Log.d("animator-demo","onAnimationUpdate "+valueAnimator.getAnimatedValue());
                    }
                });
        animator.start();

code1非常简单,就是定义了这样的ValueAnimator对象:
1.对象的过渡区间设置为0~100
2.设置了动画时间100
3.设置了一个AnimatorUpdateListener监听器
我们通过打印animator-demo日志可以得到:

11-08 01:21:00.594 29311 29311 D animator-demo: onAnimationUpdate 0
11-08 01:21:00.606 29311 29311 D animator-demo: onAnimationUpdate 0
11-08 01:21:00.626 29311 29311 D animator-demo: onAnimationUpdate 7
11-08 01:21:00.644 29311 29311 D animator-demo: onAnimationUpdate 30
11-08 01:21:00.664 29311 29311 D animator-demo: onAnimationUpdate 59
11-08 01:21:00.681 29311 29311 D animator-demo: onAnimationUpdate 84
11-08 01:21:00.701 29311 29311 D animator-demo: onAnimationUpdate 98
11-08 01:21:00.718 29311 29311 D animator-demo:
onAnimationUpdate 100

可以看出,通过ValueAnimator我们很平滑的从0过渡到了100,而并不关心其中的时序和数值的对应关系。ValueAnimator是整个属性动画的基础,因此本章将重点分析ValueAnimator的内部机制。我们先来看下ValueAnimator的继承关系图:

属性动画相关类图

ValueAnimator继承于Animator,在Animator中只是定义了一些常用接口和一些基础api,比如:
1.AnimatorListener回调
2.start,end 函数等
此外,ValueAnimator还实现了接口AnimationFrameCallback。这个接口是在AnimationHandler类中定义的回调接口,这个接口,我们一会儿会提到,它跟我们的动画息息相关。

有了以上的知识储备,我们可以开始我们下一步,我们要使用ValueAnimator,就需要构造它,上面的code1例子中我们使用了ValueAnimator.ofInt(0,100)的静态工厂方式去构造一个int属性集合的ValueAnimator对象。一般情况下,你也可以通过new ValueAnimator()的方式构建一个ValueAnimator 对象,然后通过set[Type]Values方式注入你所需要的值集合。但是相比第一种方法,通过new方式的构造手段代码偏多而且不集中,也不利于维护(当然凡事没有绝对,需要考虑你自己的业务场景)。

public static ValueAnimator ofInt(int... values) {
        ValueAnimator anim = new ValueAnimator();
        anim.setIntValues(values);
        return anim;
    }

public void setIntValues(int... values) {
        if (values == null || values.length == 0) {
            return;
        }
        if (mValues == null || mValues.length == 0) {
            setValues(PropertyValuesHolder.ofInt("", values));
        } else {
            PropertyValuesHolder valuesHolder = mValues[0];
            valuesHolder.setIntValues(values);
        }
        // New property/values/target should cause re-initialization prior to starting
        mInitialized = false;
    }

ofInt函数中,ValueAnimator 将生成一个ValueAnimator对象,然后通过调用setIntValues方法将values数组转为PropertyValuesHolder对象。而int数组类型valuesPropertyValuesHolder对象的转换是通过PropertyValuesHolderofInt方法实现:

//code PropertyValuesHolder.java
public static PropertyValuesHolder ofInt(String propertyName, int... values) {
        return new IntPropertyValuesHolder(propertyName, values);
    }

PropertyValuesHolder是什么呢?我们可以通过PropertyValuesHolder的注释看出一些端倪:

/**
 * This class holds information about a property and the values that that property
 * should take on during an animation. PropertyValuesHolder objects can be used to create
 * animations with ValueAnimator or ObjectAnimator that operate on several different properties
 * in parallel.
 */

大致意思就是一个存放属性和值的容器,而每次动画的过程中都会从这个容器中取值或者设置值。PropertyValuesHolderofInt方法将返回一个IntPropertyValuesHolder类型对象。这个类型的作用就像它名字一样限定了Holder中所存放的类型是Int类型。Ok,我们说到这里,我们可以看到一个简单ValueAnimator对象的生成,实际上伴随着多个类型的对象。

相关类图

ValueAnimatorsetDuration方法纯粹就是记录一个mDuration时间,没有特别的操作。

@Override
    public ValueAnimator setDuration(long duration) {
        if (duration < 0) {
            throw new IllegalArgumentException("Animators cannot have negative duration: " +
                    duration);
        }
        mDuration = duration;
        return this;
    }

我们重点看下start()方法:

//code ValueAnimator.java
@Override
  public void start() {
        start(false);
 }

private void start(boolean playBackwards/*是否有返回操作*/) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        ....
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        mLastFrameTime = 0;
        AnimationHandler animationHandler = AnimationHandler.getInstance();
        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));
//加入到animationHandler管理

        if (mStartDelay == 0 || mSeekFraction >= 0) {
            startAnimation();
            if (mSeekFraction == -1) {
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }

ValueAnimatorstart函数中会调用内部的start(boolean playBackwards)函数。参数playBackwards代表动画是否还有回弹操作。可以通过ValueAnimator.reverse()方法将其设置为true

我们刚才说到,ValueAnimator实现了AnimationFrameCallback接口,ValueAnimator.start(boolean)代码中,ValueAnimator先通过一个AnimationHandler.getInstance()方法获取线程内的AnimationHandler单例,然后将实现了AnimationFrameCallbackValueAnimator对象(也就是自己)通过addAnimationFrameCallback方法加入到AnimationHandler对象中去。

//AnimationHandler.java
//sAnimatorHandler是一个ThreadLocal变量,用于存储AnimationHandler的线程单例
public static AnimationHandler getInstance() {
        if (sAnimatorHandler.get() == null) {
            sAnimatorHandler.set(new AnimationHandler());
        }
        return sAnimatorHandler.get();
    }

public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        if (mAnimationCallbacks.size() == 0) {
            getProvider().postFrameCallback(mFrameCallback);
        }
        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);//加入队列
        }

        if (delay > 0) {
            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
        }
    }

AnimationHandler类的addAnimationFrameCallback方法中, AnimationHandler将会往getProvider()对象中post一个回调对象mFrameCallback,而所有的绘制都将通过这个对象进行队列遍历来实现。

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);//进行循环动画
            }
        }
    };

mFrameCallbackChoreographer.FrameCallback的实现类,它实现了doFrame回调接口,在这个接口中,将通过调用doAnimationFrame函数来执行mAnimationCallbacks队列中的动画,最后当mAnimationCallbacks队列中还有对象的时候,将再次执行getProvider().postFrameCallback(this);函数进行循环动画操作。

getProvider()返回一个AnimationFrameCallbackProvider类型的对象。AnimationFrameCallbackProvider是什么呢?AnimationFrameCallbackProvider其实就是一个跟最终的动画操作对象Choreographer交互的一个接口对象。而在AnimationHandler类中,它的实现类是MyFrameCallbackProvider

//code AnimationHandler.java
private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }

MyFrameCallbackProvider内部实现了跟Choreographer对象的操作:

private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

        final Choreographer mChoreographer = Choreographer.getInstance();

        @Override
        public void postFrameCallback(Choreographer.FrameCallback callback) {
            mChoreographer.postFrameCallback(callback);
        }
       ....
    }

Choreographer对象是什么呢?如果你做过动画,或者深入研究过动画相关,相信对这个类或者这个对象并不陌生,它是android系统中所有动画和绘制的管理者。简单概括起来,Choreographer就是一个步调管理者,它是什么步调呢?就是以16ms左右为频率做组成的一个绘制信号。这部分涉及到Android的绘制系统,我们不深入探究,我们可以写个代码简单了解一下:

long time  = SystemClock.uptimeMillis();
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long l) {
                Log.d("Choreographer","doFrame use time= "+(SystemClock.uptimeMillis() - time)+"ms");
                time = SystemClock.uptimeMillis();
                Choreographer.getInstance().postFrameCallback(this);
            }
        });

我们往Choreographer对象中post一个FrameCallback匿名对象,通过变量time来计算每次步调的时差。最后我们可以在日志中输出:

11-08 05:31:25.290  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.308  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.327  9080  9080 D Choreographer: doFrame use time= 19ms
11-08 05:31:25.345  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.363  9080  9080 D Choreographer: doFrame use time= 19ms
11-08 05:31:25.383  9080  9080 D Choreographer: doFrame use time= 19ms
11-08 05:31:25.400  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.421  9080  9080 D Choreographer: doFrame use time= 21ms
11-08 05:31:25.439  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.457  9080  9080 D Choreographer: doFrame use time= 17ms
11-08 05:31:25.474  9080  9080 D Choreographer: doFrame use time= 18ms
11-08 05:31:25.494  9080  9080 D Choreographer: doFrame use time= 19ms
11-08 05:31:25.513  9080  9080 D Choreographer: doFrame use time= 19ms

可以看出,Choreographer会在一定频率的步调中执行绘制函数。而这种步调或者是用软件模拟,或者是通过系统的 VSYNC 信号实现。我们来整理一下ValueAnimatorstart流程:

ValueAnimator.start调用流程图

通过上面的流程图我们可以看出,在AnimationHandler进行绘制的时候,实际上是调用了ValueAnimatordoAnimationFrame方法:

public final void doAnimationFrame(long frameTime) {
        AnimationHandler handler = AnimationHandler.getInstance();
        if (mLastFrameTime == 0) {
            // First frame
            handler.addOneShotCommitCallback(this);
//对于第一帧添加到commit回调
            if (mStartDelay > 0) {
                startAnimation();
            }
            if (mSeekFraction < 0) {
                mStartTime = frameTime;
            } else {
                long seekTime = (long) (getScaledDuration() * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false;         }
        mLastFrameTime = frameTime;
        if (mPaused) {
            mPauseTime = frameTime;
            handler.removeCallback(this);
            return;
        } else if (mResumed) {
            mResumed = false;
            if (mPauseTime > 0) {
                mStartTime += (frameTime - mPauseTime);
                mStartTimeCommitted = false; // allow start time to be compensated for jank
            }
            handler.addOneShotCommitCallback(this);
        }
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);//动画处理函数

        if (finished) {
            endAnimation();
        }
    }

对于第一帧或者resume后的动画,将通过handler对象的addOneShotCommitCallback方法将Callback对象加入到Commit队列中去。之后将调用动画处理函数:animateBasedOnTime(long)

 boolean animateBasedOnTime(long currentTime) {
        boolean done = false;
        if (mRunning) {
            ....
            animateValue(currentIterationFraction);
        }
        return done;
    }

animateBasedOnTime函数将调用animateValue函数实现真正意义上的属性赋值。

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);
            }
        }
    }

由于动画的整个过程相当于是一个以时间为变量的函数:
x = f(t)。(t代表时间)
为了方便计算,动画的计算过程会先将时间变量归一化,行程进度变量fraction,然后通过差值计算得到相应的差值变量赋值给fraction
比如:你执行动画400ms,现在你执行到了200ms,那么你归一化以后的进度变量就为200/400 = .5f。如果你采用的是线性差值器的话那么你的差值变量也同样为.5f
mValues变量指的就是我们上面提到的PropertyValuesHolder变量。PropertyValuesHoldercalculateValue函数,将调用mKeyframeSetgetValue函数,而这个函数的参数,就是我们上面

//PropertyValuesHolder.java
void calculateValue(float fraction) {
        mAnimatedValue = mKeyframeSet.getValue(fraction);
    }

KeyframeSet是什么呢?我们通过阅读这个成员的注释可以看出一些门道:

/**
     * The set of keyframes (time/value pairs) that define this animation.
     */
    KeyframeSet mKeyframeSet = null;

简要说明,就是存储了一些value值,什么样的value值呢?用于计算时间和对应值vaue的value集合。我们不妨看下mKeyframeSet是在哪儿被赋值的。由于我们是通过"ValueAnimator.ofInt()"方式来生成一个ValueAnimator对象,因此,ValueAnimator将会通过setIntValues函数给属性赋值:

public void setIntValues(int... values) {
        mValueType = int.class;
        mKeyframeSet = KeyframeSet.ofInt(values);
//静态构造KeyframeSet
    }

public static KeyframeSet ofInt(int... values//传入的是[0,100]) {
        int numKeyframes = values.length;
        IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
        if (numKeyframes == 1) {
            keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
            keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
        } else {    //step2
            keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
            for (int i = 1; i < numKeyframes; ++i) {
                keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
            }
        }
        return new IntKeyframeSet(keyframes);
    }

由于我们此时传入的数组是[0,100],所以if语句将跳转到我们的step2处,之后给每一个值都生成一个Keyframe对象帧放入keyframes数组集合中,再将数组集合keyframes存入对象IntKeyframeSet中。Keyframe通过静态方法ofInt来构建一个Keyframe对象,这个对象第一个浮点参数,代表你这个值在数组中的偏移。比如你的数组是[0,1,2,3,4],那么2在此数组中的偏移为2 / (5 -1) = 50%。我们再回到PropertyValuesHoldercalculateValue方法,此方法里调用了KeyFrameSetgetValue方法:

//KeyFrameSet.getValue
Keyframe prevKeyframe = mFirstKeyframe;
        for (int i = 1; i < mNumKeyframes; ++i) {
            Keyframe nextKeyframe = mKeyframes.get(i);
            if (fraction < nextKeyframe.getFraction()) {
                final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
                final float prevFraction = prevKeyframe.getFraction();
                float intervalFraction = (fraction - prevFraction) /
                    (nextKeyframe.getFraction() - prevFraction);
                // Apply interpolator on the proportional duration.
                if (interpolator != null) {
                    intervalFraction = interpolator.getInterpolation(intervalFraction);
                }
                return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
                        nextKeyframe.getValue());
            }
            prevKeyframe = nextKeyframe;
        }

KeyFrameSetgetValue方法,会通过传入的差值变量,匹配到 最接近且不超过 的一个时间帧,并通过mEvaluator计算器计算返回给上层调用。我们再回朔一下ValueAnimator的绘制过程:

  1. ValueAnimator调用start方法将自己放入到AnimatorHandler的队列中去,AnimatorHandlerpost一个FrameCallbackChoreographer的动画消息处理队列中去。

2.当收到一条VSYNC消息或者是绘制指令,将回调ValueAnimatordoAnimationFrame方法,而doAnimationFrame()方法中会调用animateBasedOnTime()-> animateValue()方法用于计算。

  1. animateValue()方法计算中会调用PropertyValuesHolder[] mValuescalculateValue方法用于计算当前时刻的差值:
void calculateValue(float fraction) {
        mAnimatedValue = mKeyframeSet.getValue(fraction);
    }

并将当前值保存在mAnimatedValue变量中去。

我们通过上面的流程解释了Animator动画过程中的差值计算,那么接下去,我们就需要把这个值注入到我们的控件属性中去,这样才能够实现动画的效果。那么我们计算好了属性值,我们需要在哪儿注入到我们的控件对象中去呢?而且,属性可能对应的是不同的类型,我们又如何区分不同的类型呢?
我们现在解答第一个问题:
我们看下一下这个例子:

View view = ...;
view.animate().translationX(500).start();

这时候,我们会看见我们的控件view沿着x轴方向正方向平移500个单位。实际上,这种动画的实现就是用的我们上面的属性动画,而属性动画的计算过程跟我们上述的一摸一样。那么它又是如何将计算好的结果设置到View对象上的呢?
首先,View.animate()方法返回的是一个ViewPropertyAnimator,不要被它的名字所误导,它并不是一个Animator,它的作用其实类似一个AnimatorBuilder对象

public ViewPropertyAnimator animate() {
        if (mAnimator == null) {
            mAnimator = new ViewPropertyAnimator(this);
        }
        return mAnimator;
    }

ViewPropertyAnimator调用startstartAnimation方法的时候,ViewPropertyAnimator会真正的构造我们的属性动画ValueAnimator

//code ViewPropertyAnimator.java
public void start() {
        ...
        startAnimation();
    }

private void startAnimation() {
        ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
        ....
        animator.addUpdateListener(mAnimatorEventListener);//增加回调
        animator.addListener(mAnimatorEventListener);//增加回调
        ...
        animator.start();
    }

这里,ViewPropertyAnimator会给生成的ValueAnimator对象增加非常重要的接口mAnimatorEventListener。我们知道,ValueAnimator在计算完每一帧以后,都会回调AnimatorUpdateListener接口的onAnimationUpdate方法:

//code ValueAnimator.java
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);//回调接口
            }
        }
    }

也就是说,每次计算完后,ValueAnimator都会回调mAnimatorEventListener对象的onAnimationUpdate方法,而在mAnimatorEventListener对象的实现中,将会把计算好的值赋予View对象:

//code AnimatorEventListener.java
@Override
        public void onAnimationUpdate(ValueAnimator animation) {
            PropertyBundle propertyBundle = mAnimatorMap.get(animation);
            if (propertyBundle == null) {
                // Shouldn't happen, but just to play it safe
                return;
            }

            boolean hardwareAccelerated = mView.isHardwareAccelerated();
            boolean alphaHandled = false;
            if (!hardwareAccelerated) {
                mView.invalidateParentCaches();
            }
            float fraction = animation.getAnimatedFraction();
            int propertyMask = propertyBundle.mPropertyMask;
            if ((propertyMask & TRANSFORM_MASK) != 0) {
                mView.invalidateViewProperty(hardwareAccelerated, false);
            }
            ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder;
            if (valueList != null) {
                int count = valueList.size();
                for (int i = 0; i < count; ++i) {
                    NameValuesHolder values = valueList.get(i);
                    float value = values.mFromValue + fraction * values.mDeltaValue;
                    if (values.mNameConstant == ALPHA) {
                        alphaHandled = mView.setAlphaNoInvalidation(value);
                    } else {
                        setValue(values.mNameConstant, value);//设置值
                    }
                }
            }
          ....
        }

这里主要调用了个叫setValue(values.mNameConstant, value);的方法,而此方法会通过传入的名字常量执行不同的操作:

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

推荐阅读更多精彩内容

  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,690评论 0 10
  • 文章转载至郭神的博客 在手机上去实现一些动画效果算是件比较炫酷的事情,因此Android系统在一开始的时候就给我们...
    DanielHan阅读 908评论 0 52
  • 总在山水间遇见一曲曲流觞, 踽踽独行都不觉孤单。 总在花树下唱起一支支短歌, 秋日萧瑟也能闻夏花。 总在晚风中轻吟...
    荧惑3_3阅读 165评论 0 1
  • 如果时间可以回到过去,会不会一切就不一样了。如果可以回到过去,我想我会重新选择,至少不会像这样默默无闻,做着自己不...
    o2o相随可乐阅读 2,502评论 0 0
  • 一直没看过肖申克的救赎,一方面由于名字的教育意义,另一方面没有片源可以观看。偶然看了斯蒂芬金的书也随意翻了一下这本...
    小酸梅是summer阅读 304评论 1 0