Android Transition Animation 过渡动画解析

本文主要译自 lgvalle 的 Material-Animations 的 README.MD,文中的源码见 Material-Animations

Android 4.4.2 中引入了 Transition 过渡动画,不过那时的 API 的功能比较简单,只能对整个 Activity 或 Fragment 做动画,Google 在 Android 5.0 的 Material Design 中引入更完整的 Transition 框架。Android 的过渡动画可以分为四个部分:

  1. Activity/Fragment 切换时的内容过渡动画(content transition)

  2. Activity/Fragment 切换时的共享元素过渡动画

  3. 同一个页面中的场景过渡动画

  4. 共享元素过渡动画 + 揭露效果(circular reveal)

在介绍过渡动画之前,先要明白 Transition 是什么?

Transition 是指不同 UI 状态转换时的动画。其中有两个关键概念:场景(scenes)和转换(transitions)。场景定义了一个确定的 UI 状态,而转换定义了两个场景切换时的动画。

当两个场景切换时,Transition 主要有下面两个行为:

(1)确定开始场景和结束场景中每个 view 的状态。

(2)根据状态差异创建 Animator,用以场景切换时 view 的动画。

下面再来看四种不同的过渡动画,本文只以 Activity 作为例子分析,Fragment 也是类似的。

1. Activity/Fragment 切换时的内容过渡动画

页面切换时的内容过渡动画可以细分为 4 个动画:

transition_A_to_B.png
transition_B_to_A.png

Android 5.0(API 21)中默认提供了三种转换:

  • 分解(Explode) - 从场景中心移入或移出视图
transition_explode.gif
  • 滑动(Slide)- 从场景边缘移入或移出视图
transition_slide.gif
  • 淡入淡出(Fade)- 通过调整透明度在场景中增添或移除视图
transition_fade.gif

1.1 启用内容过渡动画

在 Android 5.0 以上,要启用内容过渡动画要完成下面几步:

1. 打开窗口内容转换开关

在风格定义中设置:

<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
  <!-- enable window content transitions -->
  <item name="android:windowContentTransitions">true</item>
</style>

或者在代码中设置:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}

2. 指定进入、退出转换

(1)可以在风格定义中指定转换:

<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
  <!-- specify enter and exit transitions -->
  <item name="android:windowEnterTransition">@transition/activity_fade</item>
  <item name="android:windowExitTransition">@transition/activity_slide</item>
</style>

转换文件存放在res/transition目录下。

res/transition/activity_fade.xml

<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/"
    android:duration="1000"/>

res/transition/activity_slide.xml

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/"
    android:duration="1000"/>

(2) 也可以在代码中指定转换:

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_transition);
    setupWindowAnimations();
}

private void setupWindowAnimations() {
    // inflate from xml
    Slide slide = TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
    // or create directly
    Slide slide = new Slide();
    slide.setDuration(1000);

    getWindow().setExitTransition(slide);
}

TransitionActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_transition);
    setupWindowAnimations();
}

private void setupWindowAnimations() {
    // inflate from xml
    Fade fade = TransitionInflater.from(this).inflateTransition(R.transition.activity_fade);
    // or create directly
    Fade fade = new Fade();
    fade.setDuration(1000);

    getWindow().setEnterTransition(fade);
}

注:如果没有指定returnTransitionreenterTransition,返回 MainActivity 时会分别执行反转的进入和退出转换

上面代码设置的内容过渡动画效果如下表中的左边的图所示:

没有指定返回转换 指定了返回转换
Enter: Fade In
Exit: Slide out
Enter: Fade In
Exit: Slide out
Return: Slide out
transition_fade.gif
transition_fade2.gif

1.2 内容过渡动画的原理

内容过渡动画的创建之前,必须要确定视图的状态信息,而这需要修改所有过渡视图(transitioning view)的可见性。当 Activity A 启动 Activity B 时,下面的事件将会发生:

  1. Activity A 调用 startActivity()
  1. 系统遍历 A 的视图节点,找到将要运行退退出转换的所有过渡视图

  2. A 的退出转换记录所有过渡视图的开始状态

  3. 系统将所有过渡视图的可见性设置为INVISIBLE

  4. 在下一帧,A 的退出转换记录所有过渡视图的结束状态

  5. A 的退出转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为退出动画,运行该动画。

2)Activity B 启动了

  1. 系统遍历 B 的视图节点,找到将要运行进入转换的所有过渡视图,设置这些过渡视图的可见性为INVISIBLE

  2. B 的进入转换记录所有过渡视图的开始状态

  3. 系统将所有过渡视图的可见性设置为VISIBLE

  4. 在下一帧,B 的进入转换记录所有过渡视图的结束状态

  5. B 的进入转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为进入动画,运行该动画。

通过上面的分析可以知道,所有的内容转换都需要记录每个过渡视图的开始状态和结束状态。而抽象类Visibility已经做了这部分内容了,Visibility的子类只需要实现 onAppear() 和 onDisappear() 方法,创建过渡视图进入或退出场景的 Animator。Android 5.0 中Visibility有三个子类 -- Fade、Slide、Explode,如果有需要的话也可以自定义Visibility子类。

1.3 Transitioning View & Transition Groups

这个小节将讨论系统如何找出过渡视图,以及通过 transition group 自定义过渡视图。

在转换开始前,系统通过遍历视图节点递归寻找过渡视图,这个递归查找是从对根视图调用ViewGroup.captureTransitioningViews方法开始的:

/** @hide */
@Override
public void captureTransitioningViews(List<View> transitioningViews) {
    if (getVisibility() != View.VISIBLE) {
        return;
    }
    if (isTransitionGroup()) {
        transitioningViews.add(this);
    } else {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.captureTransitioningViews(transitioningViews);
        }
    }
}

上面的递归查找会遍历整个视图树节点,直到找到一个 VISIBLE 的 View 或者一个 transition group。Transition group 允许我们把整个 ViewGroup 当作一个整体在运行动画时,所有的子视图都会一起作为一个元素进行动画。否则的话,递归继续进行,所有子视图会单独对待。

public boolean isTransitionGroup() {
    if ((mGroupFlags & FLAG_IS_TRANSITION_GROUP_SET) != 0) {
        return ((mGroupFlags & FLAG_IS_TRANSITION_GROUP) != 0);
    } else {
        final ViewOutlineProvider outlineProvider = getOutlineProvider();
        return getBackground() != null || getTransitionName() != null ||
                (outlineProvider != null && outlineProvider != ViewOutlineProvider.BACKGROUND);
    }
}

默认情况下,有非空背景、transitionName 不为空、outlineProvider 不为空且不等于 ViewOutlineProvider.BACKGROUND 的 ViewGroup 会当作 Transition Group。不过我们也可以通过setTransitionGroup(boolean)来设置是否为 Transition Group。特别在运行 WebView 相关的过渡动画时,需要调用webview.setTransitionGroup(true)来将 WebView 当作一个整体。

1.4 Transition Overlap

默认情况下,内容过渡动画的 进入/返回 转换会在 退出/重新进入 转换结束前一点点开始,产生一个小的重叠来让整体的效果更自然、更协调。这个行为其实可以通过 Window/Fragment 的setAllowEnterTransitionOverlap(boolean)setAllowReturnTransitionOverlap(boolean)方法来设置,默认 overlap 是 true,进入转换会退出转换开始后尽可能快地开始,如果设置为 false,进入转换只能在退出转换结束后开始。这个对 Activity 和 Fragment 的共享元素过渡动画也是生效的。

也可以在风格定义中设置这个属性:

<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
  <!-- specify transition overlap -->
  <item name="android:windowAllowEnterTransitionOverlap">true</item>
  <item name="android:windowAllowReturnTransitionOverlap">true</item>
</style>

2. Activity/Fragment 切换时的共享元素过渡动画

Activity/Fragment 切换时的内容过渡动画分为 calling Activity 的退出动画和 called Activity 的进入动画,两个动画实际上是没有什么关系的。而共享元素过渡动画,在确定开始状态和结束状态是分别在两个页面上的,可以实现共享元素从一个页面到另一个页面的动画。

Android 5.0(API 21)默认提供了下面四种恭喜元素转换:

  • changeBounds - 为目标视图的布局布局边界的变化添加动画

  • changeClipBounds - 为目标视图的裁剪边界的变化添加动画

  • changeTransform - 为目标视图的缩放与旋转变化添加动画

  • changeImageTransform - 为目标图像的大小与缩放变化添加动画

与内容过渡动画类似,共享元素过渡动画也分为 sharedElementExitTransition、sharedElementEnterTransition、sharedElementReturnTransition、sharedElementReenterTransition。

shared_element.png

2.1 启用共享元素过渡动画

一般启用了共享元素过渡动画时,都会启用内容过渡动画,所以下面的例子中也包含内容过渡动画。

在 Android 5.0 以上,要启用共享元素过渡动画要完成下面几步:

1. 打开窗口内容转换开关

在风格定义中设置:

<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
  <!-- enable window content transitions -->
  <item name="android:windowContentTransitions">true</item>
</style>

或者在代码中设置:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}

这步和启用内容过渡动画的第一步是一样的。

2. 指定共享元素转换

(1)可以在风格定义中指定转换:

<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
  <!-- specify shared element transitions -->
  <item name="android:windowSharedElementEnterTransition">@transition/changebounds</item>
  <item name="android:windowSharedElementExitTransition">@transition/changebounds</item>
</style>

(2) 也可以在代码中指定转换:

下面代码中是指定 Activity 共享元素转换,Fragment 的话只需要改为 Fragment 的同名方法即可。

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_transition);
    setupWindowAnimations();
}

private void setupWindowAnimations() {
    // inflate from xml
    ChangeImageTransform changeImageTransform = TransitionInflater.from(this).inflateTransition(R.transition.changebounds);
    // or create directly
    ChangeImageTransform changeImageTransform = new ChangeImageTransform();

    getWindow().setSharedElementExitTransition(changeImageTransform);
}

SharedElementActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_shared_element);
    setupWindowAnimations();
}

private void setupWindowAnimations() {
    // inflate from xml
    ChangeImageTransform changeImageTransform = TransitionInflater.from(this).inflateTransition(R.transition.changebounds);
    // or create directly
    ChangeImageTransform changeImageTransform = new ChangeImageTransform();

    getWindow().setSharedElementEnterTransition(changeImageTransform);
}

3. 为共享元素指定统一的 transition name

在布局文件中使用android:transitionName属性或者在代码中使用View.setTransitionName(String)方法来为两个页面的共享元素指定同样 的transitionName。

layout/activity_transition.xml

<ImageView
        android:id="@+id/small_blue_icon"
        style="@style/MaterialAnimations.Icon.Small"
        android:src="@drawable/circle"
        android:transitionName="@string/blue_name" />

layout/activity_shared_element.xml

<ImageView
        android:id="@+id/big_blue_icon"
        style="@style/MaterialAnimations.Icon.Big"
        android:src="@drawable/circle"
        android:transitionName="@string/blue_name" />

4.a 启动 Activity 时带上共享元素参数

使用ActivityOpstions.makeSceneTransitionAnimation()方法来指定共享元素的 origin view 和 transition name。如果要结束第二个 Activity 时也显示共享元素过渡动画,请调用Activity.finishAfterTransitino()而非Activity.finish()

MainActivity.java

blueIconImageView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent i = new Intent(MainActivity.this, SharedElementActivity.class);

        View sharedView = blueIconImageView;
        String transitionName = getString(R.string.blue_name);

        ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, sharedView, transitionName);
        startActivity(i, transitionActivityOptions.toBundle());

        // version may be lower than Android 5.0
        ActivityOptionsCompat transitionActivityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, sharedView, transitionName);
    }
});

以上代码产生的共享元素过渡动画效果如下图:

shared_element_anim.gif

4.b 切换 Fragment 时带上共享元素参数

在 FragmentTransaction 提交前使用FragmentTransaction.addSharedElement()方法加上共享元素信息。

getFragmentManager().beginTransaction()
        .replace(R.id.content, fragmentB)
        .addSharedElement(blueView, getString(R.string.blue_name))
        .commit();

2.2 共享元素过渡动画的原理

和内容过渡动画类似的是,系统也会在创建共享元素动画前直接修改共享元素视图的属性。当 Activity A 启动 Activity B 时,下面的事件将会发生:

  1. A 调用startActivity(intent, bundle)后,B 启动时,窗口的背景是透明的。

  2. 系统以 A 为标准重新设置 B 的每个共享元素视图的大小和位置,过一会 B 的进入转换会记录 B 中所有共享元素的开始状态,而对于内容过渡来说,其他的 transitioning view 的可见性都是 INVISIBLE。

  3. 系统再重新将 B 的每个共享元素视图的大小和位置设置为原来的样子,过一会 B 的进入转换会记录 B 中所有共享元素的结束状态。

  4. B 的进入转换比较每个共享元素的开始状态和结束状态,创建 Animator 作为共享元素动画。

  5. 系统将隐藏 A 的所有共享元素视图,然后开始运行 B 的共享元素动画。在 B 的共享元素动画过程中,B 的窗口背景会逐渐变为不透明的。

对比内容过渡动画,内容过渡动画中系统会修改 transition views 的可见性,而共享元素过渡动画中系统会修改 shared element views 的位置、大小和显示。而且我们也可以看出实际上共享元素的 view 其实并没有在 Activity/ Fragment 之间共享,事实上,我们看到的进入或者返回的共享元素过渡动画都是直接在 B 的视图中运行的。

2.3 Shared Element Overlay

默认情况下,共享元素视图是绘制在整个视图结构之上的 -- 窗口的 ViewOverlay 层。ViewOverlay 是在 Android 4.3(API 18)引入的,方便在视图上面绘制内容。添加到视图的 ViewOverlay 层的 Drawables 或者 Views 都会在所有其他视图上面绘制,不会被遮盖。而共享元素默认会在 ViewOverlay 层绘制的原因是:共享元素是整个进入转换中的焦点,如果 transitioning view 忽然遮盖了共享元素的话,整体的效果会大打折扣。

虽然共享元素视图默认是绘制在 ViewOverlay 层的,但是也可以在必要情况下使用 Window.setSharedElementsUseOverlay(false) 来禁用。除非真的有需要,最好修改这个,否则共享元素过渡动画可以会出现预料之外的效果。

2.4 更新共享元素对应关系

想象这样这个场景:GalleryActivity 里面有一个图片列表 RecyclerView,点击进入 PreviewActivity,PreviewActivity 里面是一个 ViewPager,每项都显示图片,可以左右滑动。那么如何实现在左右滑动后返回 GalleryActivity 也能有相应的共享元素动画呢?效果如下图:

shared_element_anim2.gif

下面一步一步来实现:

1. PreviewActivity 的 ViewPager 滑动时更新共享元素

通过Activity.setEnterSharedElementCallback给 PreviewActivity 设置一个自定义的 SharedElementCallback,实现onMapSharedElements方法,当 ViewPager 滑动项时更新共享元素。

2. 返回 GalleryActivity 更新共享元素

通过Activity.setExitSharedElementCallback给 GalleryActivity 设置一个自定义的 SharedElementCallback,再实现onActivityReenter回调,在回调中更新共享元素(需要 PreviewActivity 返回时 setResult())。

1. 返回 GalleryActivity 时 RecyclerView 滑动到相应图片位置

onActivityReenter回调让 RecyclerView 滑动到相应图片位置。

这个小节的更多实现细节,请看 “Dynamic” Shared Element Transition: How to hold a common gallery flow

3. 同一个页面中的场景过渡动画

前面介绍的内容过渡动画和共享元素过渡动画都是在不同页面间的过渡动画,其实过渡动画也可以在同一个页面中,只要开始场景和结束场景在同一个页面,那么创建的过渡动画就会在一个页面中运行。

在一个页面中确定开始场景和结束场景有两种方式:手动创建 Scene 和系统自动监听布局变更。

3.1 手动创建 Scene

我们可以通过 Scene 类来手动创建场景,在创建 Scene 实例时需要指定一个sceneRoot,在调用TransitionManager.go()变换场景时,转换先记录sceneRoot子节点下所有 transitioning view 的状态作为开始场景,然后移除sceneRoot下的所有视图,将新场景的布局添加到sceneRoot,再记录sceneRoot子节点下的所有 transitioning view 的状态作为结束场景,最后根据两个场景创建并运行过渡动画。

scene1 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene1, this);
scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene2, this);
scene3 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene3, this);
scene4 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene4, this);

(...)

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button1:
            TransitionManager.go(scene1, new ChangeBounds());
            break;
        case R.id.button2:
            TransitionManager.go(scene2, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds));
            break;
        case R.id.button3:
            TransitionManager.go(scene3, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential));
            break;
        case R.id.button4:
            TransitionManager.go(scene4, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential_with_interpolators));
            break;  
    }
}

上面代码的运行效果如下图:

scenes_anim.gif

3.2 系统自动监听布局变更

还有另外一种方式可以自动创建场景,原理和上面一种其实是一样的。

1. 开始延迟转换

TransitionManager.beginDelayedTransition(sceneRoot);

TransitionManager.beginDelayedTransition(sceneRoot) 时会马上记录sceneRoot子节点下所有 transitioning view 的状态作为开始场景,然后在下一帧中再次记录sceneRoot子节点下所有 transitioning view 的状态作为结束场景,所以在下一帧之前我们修改布局属性可以被感知到,因此根据这些差异转换可以创建并运行过渡动画。

2. 修改视图的布局属性

ViewGroup.LayoutParams params = greenIconView.getLayoutParams();
params.width = 200;
greenIconView.setLayoutParams(params);
view_layout_anim.gif

4. 共享元素过渡动画 + 揭露效果(circular reveal)

在 Android 5.0(API 21)中也引入揭露效果(circular reveal),当你显示或隐藏一组 UI 元素时,揭露动画可为用户提供视觉连续性。使用ViewAnimationUtils.createCircularReveal()方法可以为裁剪区域添加动画以揭露或隐藏视图。更详细的介,绍请看官网文档 -- 使用揭露效果

揭露动画可以和共享元素过渡动画结合起来创造更有意义的动画,来引导用户发生了什么。看下面这个效果:

shared_reveal_anim.gif

一步一步来分析上面示例:

  • 橙色的圆圈是 MainActivity 跳转到 RevealActivity 的共享元素。

  • RevealActivity 的共享元素转换有一个 listener 监听共享元素转换的结束。监听到转换结束后,做了两件事情:(1)在 Toolbar 上运行了揭露动画;(2)对 RevealActivity 底部的四个按钮运行了放大动画。

监听共享元素转换结束

Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_with_arcmotion);
getWindow().setSharedElementEnterTransition(transition);
transition.addListener(new Transition.TransitionListener() {
    @Override
    public void onTransitionEnd(Transition transition) {
        animateRevealShow(toolbar);
        animateButtonsIn();
    }
    
    (...)

});

Toolbar 的揭露效果

private void animateRevealShow(View viewRoot) {
    int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
    int cy = (viewRoot.getTop() + viewRoot.getBottom()) / 2;
    int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());

    Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
    viewRoot.setVisibility(View.VISIBLE);
    anim.setDuration(1000);
    anim.setInterpolator(new AccelerateInterpolator());
    anim.start();
}

底部四个按钮的放大动画

private void animateButtonsIn() {
    for (int i = 0; i < bgViewGroup.getChildCount(); i++) {
        View child = bgViewGroup.getChildAt(i);
        child.animate()
                .setStartDelay(100 + i * DELAY)
                .setInterpolator(interpolator)
                .alpha(1)
                .scaleX(1)
                .scaleY(1);
    }
}

还有很多其他的方式创建揭露动画,例如从上开始揭露、从点击位置开始揭露等,但是重要的使用动画帮助用户理解 app 的使用。

参考文章:

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

推荐阅读更多精彩内容