《Android开发艺术探索》笔记6:动画的深入分析

1,View动画

1.1,View的分类

View动画分为平移动画,对应的xml标签<translate>,Java类为TranslateAnimation;缩放动画,对应的xml标签<scale>,Java类为ScaleAnimation;旋转动画,对应的xml标签<rotate>,Java类为RotateAnimation;透明度动画,对应的xml标签<alpha>,Java类为AlphaAnimation。以下是xml中的定义:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@anim/interpolator_resource"
  android:shareInterpolator="true|false" >

  <alpha 
      android:fromAlpha="float" ><!-- 透明度起始值 -->
      android:toAlpha="float"/><!-- 透明度结束值 -->
  <scale 
      android:fromXScale="float" ><!-- 水平方向缩放起始值 -->
      android:toXScale="float"<!-- 水平方向缩放结束值 -->
      android:fromYScale="float"<!-- 垂直方向缩放起始值 -->
      android:toYScale="float"<!-- 垂直方向缩放结束值 -->
      android:pivotX="float"<!-- 缩放轴点x坐标 -->
      android:pivotY="float"/><!-- 缩放轴点y坐标 -->
  <translate 
      android:fromXDelta="float" ><!-- x的起始位置 -->
      android:fromYDelta="float"<!-- y的起始位置 -->
      android:toXDelta="float"<!-- x的结束位置 -->
      android:toYDelta="float"/><!-- y的结束位置 -->
  <rotate 
      android:fromDegrees="float" ><!-- 起始角度 -->
      android:toDegrees="float"<!-- 结束角度 -->
      android:pivotX="float"<!-- 旋转轴点x坐标 -->
      android:pivotY="float"/><!-- 旋转轴点y坐标 -->
  <set>
  ...
  </set>

</set>

此外,还有
android:duration:动画的持续时长,ms。
android:fillAfter:动画结束以后View是否停留在结束位置。
其中<set>标签表示动画集合,对应AnimationSet,它主要有2个属性如下:
android:interpolator
表示动画集合采用的插值器,插值器会影响动画的速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。默认值“@android:anim/accelerate_decelerate_interpolator”,即加速减速插值器。
android:shareInterpolator
表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值。

1.2,View的使用

1,引用XML

Button button = findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this,R.anim.<anim_filename>);
button.startAnimation(animation);

2,Java代码啊创建

AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(2000);
button.startAnimation(alphaAnimation);

3,View动画过程监听

 public static interface AnimationListener {
      void onAnimationStart(Animation animation);
      void onAnimationEnd(Animation animation);
      void onAnimationRepeat(Animation animation);
}
1.3,自定义View动画

自定义View继承自Animation类,重写它的initialize和applyTranslation方法,在initialize方法中做初始化工作,在applyTranslation方法中进行矩阵变换。实用的例子可以参考ApiDemos下的动画,例如Rotate3dAnimation。

1.4,帧动画

帧动画是顺序播放的一组图片,类似于电影播放。下面是帧动画的使用方式:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/image1"
        android:duration="500" />
    <item
        android:drawable="@drawable/image2"
        android:duration="500" />
    <item
        android:drawable="@drawable/image3"
        android:duration="500" />
</animation-list>
Button button = (Button) findViewById(R.id.button);
button.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable drawable = (AnimationDrawable) button.getBackground();
drawable.start();

以上就是帧动画的使用方式,非常简单,但是图片使用过多,会造成OOM。

2,View动画的特殊使用场景

2.1,LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时就会具有这个动画效果,例如ListView的item的动画,通常使用LayoutAnimation制造。以下是LayoutAnimation的使用步骤:
1,定义LayoutAnimation:

<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/anim_item"
    android:animationOrder="reverse"
    android:delay="0.5" />

android:delay
表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。也就是说,第一个子元素延迟150ms播放动画,第二个子元素延迟300ms播放动画,以此类推。
android:animationOrde
表示子元素动画的顺序,有三种:normal、reverse和random,其中normal表示顺序显示,即排在前面的子元素先开始动画;reverse表示逆向播放,即排在后面的子元素先开始动画;random则是随机播放入场动画。
android:animation
为子元素指定具体的动画。
2,为子元素指定具体的入场动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true" >

    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />

    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />

</set>

3,为ViewGroup指定android:layoutAnimation属性,android:layoutAnimation="@anim/anim_layout"。对于ListView来说,这样item就具有了入场动画了。

<ListView
     android:id="@+id/list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layoutAnimation="@anim/anim_layout"
     android:background="#fff4f7f9"
     android:cacheColorHint="#00000000"
     android:divider="#dddbdb"
     android:dividerHeight="1.0px"
     android:listSelector="@android:color/transparent" />

除了在XML中定义,还可以通过LayoutAnimationController实现:

ListView listView = (ListView) findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
2.2,Activity切换动画

Activity切换动画通过overridePendingTransition(int enterAnim, int exitAnim)方法实现:
Fragment切换动画可以通过FragmentTransaction中的setCustomAnimations()方法来实现。


2,属性动画

属性动画可以对任意对象的属性进行动画而不仅仅是View,动画的默认时间间隔为300ms,默认帧率10ms/帧。可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,因此,属性动画几乎无所不能,只要对象有这个属性,它都是实现动画效果。常用的属性动画类有:ValueAnimator、ObjectAnimator和AnimatorSet。

2.1,使用属性动画

下面是是对ObjectAnimator、ValueAnimator和AnimatorSet的简单玩法:
(1)改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一个距离:它的高度。动画使用默认的时间间隔,插值器。

ObjectAnimator.ofFloat(myObject, "translationY", -myObject.getHeight());

(2)改变一个对象的背景色,下面动画可以让View在3s内实现从0xFFFF8080到0xFF8080FF的渐变,并且无限循环+反转。

Button button = (Button) findViewById(R.id.button);
ValueAnimator colorAnim = ObjectAnimator.ofInt(button, "backgroundColor",
        0xFFFF8080, 0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();

(3)动画集合,5秒内对View的旋转、平移、缩放和透明度都进行改变。

Button button = (Button) findViewById(R.id.button);
AnimatorSet set = new AnimatorSet();
set.playTogether(
       ObjectAnimator.ofFloat(button, "rotationX", 0, 360),
       ObjectAnimator.ofFloat(button, "rotationY", 0, 180),
       ObjectAnimator.ofFloat(button, "rotation", 0, -90),
       ObjectAnimator.ofFloat(button, "translationX", 0, 90),
       ObjectAnimator.ofFloat(button, "translationY", 0, 90),
       ObjectAnimator.ofFloat(button, "scaleX", 1, 1.5f),
       ObjectAnimator.ofFloat(button, "scaleY", 1, 0.5f),
      ObjectAnimator.ofFloat(button, "alpha", 1, 0.25f, 1)
);
set.setDuration(5000).start();

属性动画除了可以用Java代码描述,还可以在XML中定义,属性动画需要定义在res/animator/目录下,它的语法结构如下:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially|together"><!--together:子动画同时播放。sequentially:自动化按照先后顺序依次播放-->

    <objectAnimator 
        android:duration="int"<!--动画时长-->
        android:propertyName="string"<!--属性名称-->
        android:repeatCount="int"<!--重复次数-->
        android:repeatMode="restart|reverse"<!--重复模式-->
        android:startOffset="int"<!--延迟时间-->
        android:valueFrom="float|int|color"<!--属性起始值-->
        android:valueTo="float|int|color"<!--属性结束值-->
        android:valueType="colorType|intType|floatType|pathType" /><!--属性类型-->

    <animator
        android:duration="int"
        android:repeatCount="int"
        android:repeatMode="restart|reverse"
        android:startOffset="int"
        android:valueFrom="float|int|color"
        android:valueTo="float|int|color"
        android:valueType="colorType|intType|floatType|pathType" />
      
       <set>
            ...
      </set>
</set>

上面XML的标签<set>表示属性动画集合AnimatorSet,<objectAnimator >表示的是ObjectAnimator,<animator>表示的是ValueAnimator。在Java代码引用XML:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context, 
        R.animator.<animator_filename>);
set.setTarget(button);
set.start();
2.2,插值器和估值器

TimeInterpolator插值器的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有

图1、系统内置的插值器

以上图来自Google Develop.
常用的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等。

TypeEvaluator估值器的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有

图2、系统内置的估值器

以上图来自Google Develop.

下面的图来自Google官方,表示插值器的工作原理


图3、线性插值器的工作原理

上图表示的是线性插值器的流程,即匀速动画流程,表示在0-40ms内,将对象的x属性从0到40增加,我们截取t=20ms的这一帧来分析,因为属性动画的默认刷新率是10ms,那么时间流逝的百分比为(20/40)0.5,说明时间过去了一半,那么x具体会变化多少呢?我们先看一下LinearInterpolator的源码:

public class LinearInterpolator implements Interpolator, 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方法,就是返回的x的值,即0.5,而且是输入值和输出值是一样的,此时需要看看估值器的源码,才能知道x的具体数值了:

public class IntEvaluator implements TypeEvaluator<Integer> {

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

根据估值器的算法,参数中fraction代表的是估值小数,startValue起始值,endValue终点值,对应的值分别是0.5,0,40,带入公式得0+0.5*(40 - 0)=20。就是图中t=20ms,x=20的由来。
注意:属性动画要求对象的属性有set方法和get方法(可选)。插值器和估值器除了以上系统提供的以为,还可以自定义。自定义插值器,需要派生一个累实现Interpolator或者TimeInterpolator,自定义估值器需要派生一个类实现TypeEvaluator接口。

2.3,监听属性动画

属性动画和View动画一样,系统也提供了接口用于监听属性动画的过程和状态,主要会用到2个接口,AnimatorListener 和AnimatorUpdateListener

public static interface AnimatorListener {
        void onAnimationStart(Animator animation);
        void onAnimationEnd(Animator animation);
        void onAnimationCancel(Animator animation);
        void onAnimationRepeat(Animator animation);
}

AnimatorListener 接口实现属性动画的开始、结束、取消和重复的监听,然而并不是所有的方法都是我们感兴趣的,系统给我们提供了一个简化的监听AnimatorUpdateListener,如下:

public static interface AnimatorUpdateListener {
      void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener 会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate方法就会被调用一次。
此外,我们还可以通过AnimatorListenerAdapter 类来选择监听某一个或者多个过程。AnimatorListenerAdapter 的代码如下:

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

    @Override
    public void onAnimationCancel(Animator animation) {
    }

    @Override
    public void onAnimationEnd(Animator animation) {
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
    }

    @Override
    public void onAnimationStart(Animator animation) {
    }

    @Override
    public void onAnimationPause(Animator animation) {
    }

    @Override
    public void onAnimationResume(Animator animation) {
    }
}

可以看到AnimatorListenerAdapter 实现了AnimatorListener 和AnimatorPauseListener 接口的抽象类,可以取到属性动画的各种状态下的回调方法,如果我们只是想监听动画结束状态的话,可以如下:

anim.addListener(new AnimatorListenerAdapter() {  
  
    @Override  
    public void onAnimationEnd(Animator animation) {  
        // TODO Auto-generated method stub  
        super.onAnimationEnd(animation);  
    }  
  
});
2.4,对任意属性做动画

属性动画的原理:属性动画要求动画作用的对象提供该属性的set和get方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想要动画生效,那么必须同时满足如下2个条件:
(1)object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。
(2)object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI上的改变之类的(如果这条不满足,动画无效但不会Crash)。

针对上面所说的问题,如果一个对象并不同时满足以上2个条件的话,Google官方给我们的建议是:
(1)给对象加上set和get方法,如果你有权限的话
(2)用一个类去包装原始对象,间接的提供set和get方法

private void performAnimate() {
     ViewWrapper wrapper = new ViewWrapper(mButton);
     ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
}

@Override
public void onClick(View v) {
     if (v == mButton) {
         performAnimate();
     }
}

private static class ViewWrapper {
     private View mTarget;

     public ViewWrapper(View target) {
          this.mTarget = target;
     }

     public int getWidth() {
         return mTarget.getLayoutParams().width;
     }

    public void setWidth(int width) {
         mTarget.getLayoutParams().width = width;
         mTarget.requestLayout();
    }

}

(3)采用ValueAnimator,监听动画过程,自己实现属性的改变

@Override
public void onWindowFocusChanged(boolean hasFocus) {
      super.onWindowFocusChanged(hasFocus);
      if (hasFocus) {
          Button button = (Button)findViewById(R.id.button1);
          performAnimate(button, button.getWidth(), 500);
      }
}

private void performAnimate(final View target, final int start, final int end) {
      ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
      valueAnimator.addUpdateListener(new AnimatorUpdateListener() {

          // 持有一个IntEvaluator对象,方便下面估值的时候使用
          private IntEvaluator mEvaluator = new IntEvaluator();

          @Override
          public void onAnimationUpdate(ValueAnimator animator) {
              // 获得当前动画的进度值,整型,1-100之间
              int currentValue = (Integer) animator.getAnimatedValue();
              Log.d(TAG, "current value: " + currentValue);

              // 获得当前进度占整个动画过程的比例,浮点型,0-1之间
              float fraction = animator.getAnimatedFraction();
              // 直接调用整型估值器通过比例计算出宽度,然后再设给Button
              target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
              target.requestLayout();
          }
      });

      valueAnimator.setDuration(5000).start();
}

3,属性动画的工作原理

属性动画的原理:属性动画要求动画作用的对象提供该属性的set和get方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。
下面从ObjectAnimator的start方法为入口,查看一下ObjectAnimator的源码:

@Override
public void start() {
    // See if any of the current active/pending animators need to be canceled
    AnimationHandler handler = sAnimationHandler.get();
    if (handler != null) {
        int numAnims = handler.mAnimations.size();
        for (int i = numAnims - 1; i >= 0; i--) {
            if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
                ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                    anim.cancel();
                }
            }
        }
        numAnims = handler.mPendingAnimations.size();
        for (int i = numAnims - 1; i >= 0; i--) {
            if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
                ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                    anim.cancel();
                }
            }
        }
        numAnims = handler.mDelayedAnims.size();
        for (int i = numAnims - 1; i >= 0; i--) {
            if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
                ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                    anim.cancel();
                }
            }
        }
    }
    if (DBG) {
        Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
        for (int i = 0; i < mValues.length; ++i) {
            PropertyValuesHolder pvh = mValues[i];
            Log.d(LOG_TAG, "   Values[" + i + "]: " +
                pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
                pvh.mKeyframes.getValue(1));
        }
    }
    super.start();
}

上述代码的作用很简单,注意anim.cancel()和3个循环,意思就是判断如果当前动画、等待动画(Pending)和延迟动画(Delay)中有和当前动画相同的动画,那么就把相同的动画取消掉,然后打印一堆的Log,再调用父类ValueAnimator的start方法,所以接下来需要参谋参谋ValueAnimator的start方法:

private void start(boolean playBackwards) {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }
    mPlayingBackwards = playBackwards;
    mCurrentIteration = 0;
    mPlayingState = STOPPED;
    mStarted = true;
    mStartedDelay = false;
    mPaused = false;
    updateScaledDuration(); // in case the scale factor has changed since creation time
    AnimationHandler animationHandler = getOrCreateAnimationHandler();
    animationHandler.mPendingAnimations.add(this);
    if (mStartDelay == 0) {
        // This sets the initial value of the animation, prior to actually starting it running
        setCurrentPlayTime(0);
        mPlayingState = STOPPED;
        mRunning = true;
        notifyStartListeners();
    }
    animationHandler.start();
}

上述代码可以看出属性动画需要运行在有Looper的线程中,最终会调用 animationHandler.start(),animationHandler是一个Runnable,通过各种追踪代码,一直会调用到JNI层,最后还是会回调回来,一直到ValueAnimator的doAnimationFrame方法:

final boolean doAnimationFrame(long frameTime) {
    if (mPlayingState == STOPPED) {
        mPlayingState = RUNNING;
        if (mSeekTime < 0) {
            mStartTime = frameTime;
        } else {
            mStartTime = frameTime - mSeekTime;
            // Now that we're playing, reset the seek time
            mSeekTime = -1;
        }
    }
    if (mPaused) {
        if (mPauseTime < 0) {
            mPauseTime = frameTime;
        }
        return false;
    } else if (mResumed) {
        mResumed = false;
        if (mPauseTime > 0) {
            // Offset by the duration that the animation was paused
            mStartTime += (frameTime - mPauseTime);
        }
    }
    // The frame time might be before the start time during the first frame of
    // an animation.  The "current time" must always be on or after the start
    // time to avoid animating frames at negative time intervals.  In practice, this
    // is very rare and only happens when seeking backwards.
    final long currentTime = Math.max(frameTime, mStartTime);
    return animationFrame(currentTime);
}

上述代码调用了animationFrame方法,不过animationFrame方法又调用了animateValue方法,接下来就看看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);
        }
    }
}

上述代码的calculateValue方法就是计算每帧动画所对应的属性值,着重看一下到底在哪里调用了属性的get和set方法。在初始化的时候,如果属性的初始值没有提供,则get方法将会被调用,在PropertyValuesHolder的setupValue中可以看出,get方法其实是通过反射来调用的:

private void setupValue(Object target, Keyframe kf) {
    if (mProperty != null) {
        Object value = convertBack(mProperty.get(target));
        kf.setValue(value);
    }
    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());
    }
}

当动画的下一帧到来的时候,PropertyValuesHolder中的setAnimatedValue方法会将新的属性值设置给对象,调用其set方法。从下面的源码可以看出,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());
        }
    }
}

ps:各位,《Android开发艺术探索》真的是android进阶的一本好书,虽然很多概念在我这里已经很清晰并不算什么难点,但是该书还是帮我重新系统性回顾了一遍android相关知识,我的笔记做的笔记详细,但是还是建议购买一本正版《Android开发艺术探索》方便学习(ps:绝非打广告啊)。

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

推荐阅读更多精彩内容