Android源碼解析-- Animator動畫

Android在3.0版本中引入了新的動畫實現(xiàn):屬性動畫。我們一般稱之為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.對象的過渡區(qū)間設(shè)置為0~100
2.設(shè)置了動畫時間100
3.設(shè)置了一個AnimatorUpdateListener監(jiān)聽器
我們通過打印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,而并不關(guān)心其中的時序和數(shù)值的對應關(guān)系宝穗。ValueAnimator是整個屬性動畫的基礎(chǔ),因此本章將重點分析ValueAnimator的內(nèi)部機制哄辣。我們先來看下ValueAnimator的繼承關(guān)系圖:

屬性動畫相關(guān)類圖

ValueAnimator繼承于Animator,在Animator中只是定義了一些常用接口和一些基礎(chǔ)api,比如:
1.AnimatorListener回調(diào)
2.start,end 函數(shù)等
此外,ValueAnimator還實現(xiàn)了接口AnimationFrameCallback蹄衷。這個接口是在AnimationHandler類中定義的回調(diào)接口,這個接口,我們一會兒會提到,它跟我們的動畫息息相關(guān)说铃。

有了以上的知識儲備,我們可以開始我們下一步,我們要使用ValueAnimator,就需要構(gòu)造它,上面的code1例子中我們使用了ValueAnimator.ofInt(0,100)的靜態(tài)工廠方式去構(gòu)造一個int屬性集合的ValueAnimator對象访惜。一般情況下,你也可以通過new ValueAnimator()的方式構(gòu)建一個ValueAnimator 對象,然后通過set[Type]Values方式注入你所需要的值集合嘹履。但是相比第一種方法,通過new方式的構(gòu)造手段代碼偏多而且不集中,也不利于維護(當然凡事沒有絕對,需要考慮你自己的業(yè)務場景)腻扇。

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函數(shù)中,ValueAnimator 將生成一個ValueAnimator對象,然后通過調(diào)用setIntValues方法將values數(shù)組轉(zhuǎn)為PropertyValuesHolder對象。而int數(shù)組類型valuesPropertyValuesHolder對象的轉(zhuǎn)換是通過PropertyValuesHolderofInt方法實現(xiàn):

//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.
 */

大致意思就是一個存放屬性和值的容器,而每次動畫的過程中都會從這個容器中取值或者設(shè)置值幼苛。PropertyValuesHolderofInt方法將返回一個IntPropertyValuesHolder類型對象。這個類型的作用就像它名字一樣限定了Holder中所存放的類型是Int類型焕刮。Ok,我們說到這里,我們可以看到一個簡單ValueAnimator對象的生成,實際上伴隨著多個類型的對象舶沿。

相關(guān)類圖

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函數(shù)中會調(diào)用內(nèi)部的start(boolean playBackwards)函數(shù)配并。參數(shù)playBackwards代表動畫是否還有回彈操作括荡。可以通過ValueAnimator.reverse()方法將其設(shè)置為true溉旋。

我們剛才說到,ValueAnimator實現(xiàn)了AnimationFrameCallback接口,ValueAnimator.start(boolean)代碼中,ValueAnimator先通過一個AnimationHandler.getInstance()方法獲取線程內(nèi)的AnimationHandler單例,然后將實現(xiàn)了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一個回調(diào)對象mFrameCallback,而所有的繪制都將通過這個對象進行隊列遍歷來實現(xiàn)。

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);//進行循環(huán)動畫
            }
        }
    };

mFrameCallbackChoreographer.FrameCallback的實現(xiàn)類,它實現(xiàn)了doFrame回調(diào)接口,在這個接口中,將通過調(diào)用doAnimationFrame函數(shù)來執(zhí)行mAnimationCallbacks隊列中的動畫,最后當mAnimationCallbacks隊列中還有對象的時候,將再次執(zhí)行getProvider().postFrameCallback(this);函數(shù)進行循環(huán)動畫操作观腊。

getProvider()返回一個AnimationFrameCallbackProvider類型的對象邑闲。AnimationFrameCallbackProvider是什么呢?AnimationFrameCallbackProvider其實就是一個跟最終的動畫操作對象Choreographer交互的一個接口對象梧油。而在AnimationHandler類中,它的實現(xiàn)類是MyFrameCallbackProvider苫耸。

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

MyFrameCallbackProvider內(nèi)部實現(xiàn)了跟Choreographer對象的操作:

private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

        final Choreographer mChoreographer = Choreographer.getInstance();

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

Choreographer對象是什么呢?如果你做過動畫,或者深入研究過動畫相關(guān),相信對這個類或者這個對象并不陌生,它是android系統(tǒng)中所有動畫和繪制的管理者儡陨。簡單概括起來,Choreographer就是一個步調(diào)管理者,它是什么步調(diào)呢褪子?就是以16ms左右為頻率做組成的一個繪制信號量淌。這部分涉及到Android的繪制系統(tǒng),我們不深入探究,我們可以寫個代碼簡單了解一下:

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來計算每次步調(diào)的時差。最后我們可以在日志中輸出:

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會在一定頻率的步調(diào)中執(zhí)行繪制函數(shù)嫌褪。而這種步調(diào)或者是用軟件模擬,或者是通過系統(tǒng)的 VSYNC 信號實現(xiàn)类少。我們來整理一下ValueAnimatorstart流程:

ValueAnimator.start調(diào)用流程圖

通過上面的流程圖我們可以看出,在AnimationHandler進行繪制的時候,實際上是調(diào)用了ValueAnimatordoAnimationFrame方法:

public final void doAnimationFrame(long frameTime) {
        AnimationHandler handler = AnimationHandler.getInstance();
        if (mLastFrameTime == 0) {
            // First frame
            handler.addOneShotCommitCallback(this);
//對于第一幀添加到commit回調(diào)
            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);//動畫處理函數(shù)

        if (finished) {
            endAnimation();
        }
    }

對于第一幀或者resume后的動畫,將通過handler對象的addOneShotCommitCallback方法將Callback對象加入到Commit隊列中去。之后將調(diào)用動畫處理函數(shù):animateBasedOnTime(long)渔扎。

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

animateBasedOnTime函數(shù)將調(diào)用animateValue函數(shù)實現(xiàn)真正意義上的屬性賦值硫狞。

void animateValue(float fraction) {
        fraction = mInterpolator.getInterpolation(fraction);
        mCurrentFraction = fraction;//歸一化后的進度參數(shù)
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].calculateValue(fraction);//計算值
        }
        if (mUpdateListeners != null) {//通知回調(diào)
            int numListeners = mUpdateListeners.size();
            for (int i = 0; i < numListeners; ++i) {
                mUpdateListeners.get(i).onAnimationUpdate(this);
            }
        }
    }

由于動畫的整個過程相當于是一個以時間為變量的函數(shù):
x = f(t)。(t代表時間)
為了方便計算,動畫的計算過程會先將時間變量歸一化,行程進度變量fraction,然后通過差值計算得到相應的差值變量賦值給fraction晃痴。
比如:你執(zhí)行動畫400ms,現(xiàn)在你執(zhí)行到了200ms,那么你歸一化以后的進度變量就為200/400 = .5f残吩。如果你采用的是線性差值器的話那么你的差值變量也同樣為.5f
mValues變量指的就是我們上面提到的PropertyValuesHolder變量倘核。PropertyValuesHoldercalculateValue函數(shù),將調(diào)用mKeyframeSetgetValue函數(shù),而這個函數(shù)的參數(shù),就是我們上面

//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函數(shù)給屬性賦值:

public void setIntValues(int... values) {
        mValueType = int.class;
        mKeyframeSet = KeyframeSet.ofInt(values);
//靜態(tài)構(gòu)造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);
    }

由于我們此時傳入的數(shù)組是[0,100],所以if語句將跳轉(zhuǎn)到我們的step2處,之后給每一個值都生成一個Keyframe對象幀放入keyframes數(shù)組集合中,再將數(shù)組集合keyframes存入對象IntKeyframeSet中漏益。Keyframe通過靜態(tài)方法ofInt來構(gòu)建一個Keyframe對象,這個對象第一個浮點參數(shù),代表你這個值在數(shù)組中的偏移蛹锰。比如你的數(shù)組是[0,1,2,3,4],那么2在此數(shù)組中的偏移為2 / (5 -1) = 50%。我們再回到PropertyValuesHoldercalculateValue方法,此方法里調(diào)用了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計算器計算返回給上層調(diào)用铜犬。我們再回朔一下ValueAnimator的繪制過程:

  1. ValueAnimator調(diào)用start方法將自己放入到AnimatorHandler的隊列中去,AnimatorHandlerpost一個FrameCallbackChoreographer的動畫消息處理隊列中去。

2.當收到一條VSYNC消息或者是繪制指令,將回調(diào)ValueAnimatordoAnimationFrame方法,而doAnimationFrame()方法中會調(diào)用animateBasedOnTime()-> animateValue()方法用于計算轻庆。

  1. animateValue()方法計算中會調(diào)用PropertyValuesHolder[] mValuescalculateValue方法用于計算當前時刻的差值:
void calculateValue(float fraction) {
        mAnimatedValue = mKeyframeSet.getValue(fraction);
    }

并將當前值保存在mAnimatedValue變量中去癣猾。

我們通過上面的流程解釋了Animator動畫過程中的差值計算,那么接下去,我們就需要把這個值注入到我們的控件屬性中去,這樣才能夠?qū)崿F(xiàn)動畫的效果。那么我們計算好了屬性值,我們需要在哪兒注入到我們的控件對象中去呢余爆?而且,屬性可能對應的是不同的類型,我們又如何區(qū)分不同的類型呢纷宇?
我們現(xiàn)在解答第一個問題:
我們看下一下這個例子:

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

這時候,我們會看見我們的控件view沿著x軸方向正方向平移500個單位。實際上,這種動畫的實現(xiàn)就是用的我們上面的屬性動畫,而屬性動畫的計算過程跟我們上述的一摸一樣蛾方。那么它又是如何將計算好的結(jié)果設(shè)置到View對象上的呢像捶?
首先,View.animate()方法返回的是一個ViewPropertyAnimator,不要被它的名字所誤導,它并不是一個Animator,它的作用其實類似一個AnimatorBuilder對象

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

ViewPropertyAnimator調(diào)用startstartAnimation方法的時候,ViewPropertyAnimator會真正的構(gòu)造我們的屬性動畫ValueAnimator

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

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

這里,ViewPropertyAnimator會給生成的ValueAnimator對象增加非常重要的接口mAnimatorEventListener转捕。我們知道,ValueAnimator在計算完每一幀以后,都會回調(diào)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);//回調(diào)接口
            }
        }
    }

也就是說,每次計算完后,ValueAnimator都會回調(diào)mAnimatorEventListener對象的onAnimationUpdate方法,而在mAnimatorEventListener對象的實現(xiàn)中,將會把計算好的值賦予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);//設(shè)置值
                    }
                }
            }
          ....
        }

這里主要調(diào)用了個叫setValue(values.mNameConstant, value);的方法,而此方法會通過傳入的名字常量執(zhí)行不同的操作:

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;
              ....
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末作岖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子五芝,更是在濱河造成了極大的恐慌痘儡,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枢步,死亡現(xiàn)場離奇詭異沉删,居然都是意外死亡渐尿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進店門矾瑰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砖茸,“玉大人,你說我怎么就攤上這事殴穴×购唬” “怎么了?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵采幌,是天一觀的道長劲够。 經(jīng)常有香客問我,道長休傍,這世上最難降的妖魔是什么征绎? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮磨取,結(jié)果婚禮上人柿,老公的妹妹穿的比我還像新娘。我一直安慰自己忙厌,他們只是感情好凫岖,可當我...
    茶點故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慰毅,像睡著了一般隘截。 火紅的嫁衣襯著肌膚如雪扎阶。 梳的紋絲不亂的頭發(fā)上汹胃,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天,我揣著相機與錄音东臀,去河邊找鬼着饥。 笑死,一個胖子當著我的面吹牛惰赋,可吹牛的內(nèi)容都是我干的宰掉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼赁濒,長吁一口氣:“原來是場噩夢啊……” “哼轨奄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拒炎,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挪拟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后击你,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玉组,經(jīng)...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡谎柄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了惯雳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朝巫。...
    茶點故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖石景,靈堂內(nèi)的尸體忽然破棺而出劈猿,到底是詐尸還是另有隱情,我是刑警寧澤潮孽,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布糙臼,位于F島的核電站,受9級特大地震影響恩商,放射性物質(zhì)發(fā)生泄漏变逃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一怠堪、第九天 我趴在偏房一處隱蔽的房頂上張望揽乱。 院中可真熱鬧,春花似錦粟矿、人聲如沸凰棉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撒犀。三九已至,卻和暖如春掏秩,著一層夾襖步出監(jiān)牢的瞬間或舞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工蒙幻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留映凳,地道東北人。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓邮破,卻偏偏與公主長得像诈豌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抒和,可洞房花燭夜當晚...
    茶點故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 1 背景 不能只分析源碼呀矫渔,分析的同時也要整理歸納基礎(chǔ)知識,剛好有人微博私信讓全面說說Android的動畫摧莽,所以今...
    未聞椛洺閱讀 2,716評論 0 10
  • 文章轉(zhuǎn)載至郭神的博客 在手機上去實現(xiàn)一些動畫效果算是件比較炫酷的事情,因此Android系統(tǒng)在一開始的時候就給我們...
    DanielHan閱讀 911評論 0 52
  • 總在山水間遇見一曲曲流觴送膳, 踽踽獨行都不覺孤單员魏。 總在花樹下唱起一支支短歌, 秋日蕭瑟也能聞夏花叠聋。 總在晚風中輕吟...
    熒惑3_3閱讀 169評論 0 1
  • 如果時間可以回到過去碌补,會不會一切就不一樣了虏束。如果可以回到過去,我想我會重新選擇厦章,至少不會像這樣默默無聞镇匀,做著自己不...
    o2o相隨可樂閱讀 2,516評論 0 0
  • 一直沒看過肖申克的救贖,一方面由于名字的教育意義袜啃,另一方面沒有片源可以觀看汗侵。偶然看了斯蒂芬金的書也隨意翻了一下這本...
    小酸梅是summer閱讀 307評論 1 0