flutter如何自定义转场动画

近期在flutter开发过程中,遇到一些需要自定义转场动画的情况,就想研究一下相关的知识,有兴趣的小伙伴可以跟我一起了解下这块的逻辑和用法

通过自定义转场可以实现常用的滑动转场、透明度转场、缩放转场,以及一些复杂的复合转场等

效果如下:

IMB_3BpRSp.gif

一.初识

首先我们来看下flutter常见的转场逻辑:

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) {
      return const NewPage();
    },
  ),
);

通过push方法,传入一个MaterialPageRoute包裹的新页面,就可以实现默认的转场了(默认的从右到左,返回的时候是逆向的)

如果不想使用这种默认的转场,我们可以通过自定义一个PageRoute,实现各种各样的转场动画

自定义PageRoute

class SlideUpPageRoute<T> extends MaterialPageRoute<T> {
  SlideUpPageRoute({
    required WidgetBuilder builder,
    RouteSettings settings,
  }) : super(builder: builder, settings: settings);

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0.0, 1.0),
        end: Offset.zero,
      ).animate(animation),
      child: child,
    );
  }
}

使用自定义的PageRoute完成转场:

Navigator.of(context).push(
  SlideUpPageRoute(
    builder: (context) {
      return const SlideTransitionPage();
    },
  ),

要想弄清楚转场动画的流程,接下来我们先来了解下转场相关的一些概念

如果只想了解用法,可以直接跳到第三部分:自定义转场动画

二、转场动画相关逻辑及流程

1. Navigator导航器

WidgetsAppMaterialApp创建和配置了一个导航器,负责管理一个[Route]对象堆栈

/// Push the given route onto the navigator that most tightly encloses the given context.
 
@optionalTypeArgs
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}

......

@optionalTypeArgs
Future<T?> push<T extends Object?>(Route<T> route) {
   assert(_debugCheckIsPagelessRoute(route));
   _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push));
   return route.popped;
}

可以通过Navigator.of(context)获取当前导航器对象,通过push和pop方法控制路由对象的进栈和出栈,实现导航器的跳转

/// You can create your own subclass of one of the widget library route classes
/// like [PopupRoute], [ModalRoute], or [PageRoute], to control the animated
/// transition employed to show the route, the color and behavior of the route's
/// modal barrier, and other aspects of the route.

其中,Route是Flutter中通用的路由管理类, 是一个抽象类,定义了导航器和路由之间的抽象接口, 我们可以通过Route来自定义路由的创建、销毁、跳转等行为,并且可以根据自己的需求来实现不同的路由管理策略

页面路由对象route由两部分组成,即页面page和过渡效果transition,页面一般只构建一次,过渡是在每个帧的持续时间内动态构建的

Navigator.of(context).push(PageRouteBuilder(
  opaque: false,
  pageBuilder: (BuildContext context, _, __) {
    return Center(child: Text('My PageRoute'));
  },
  transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
    return FadeTransition(
      opacity: animation,
      child: RotationTransition(
        turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
        child: child,
      ),
    );
  }
));

过渡效果transition就是我们这一次探究的重点,即转场动画效果

2. MaterialPageRouteCupertinoPageRoute

/// See [MaterialPageRoute] for a route that replaces the entire screen with a 
platform-adaptive transition.

Navigator的介绍中建议参考MaterialPageRoute来实现转场路由

MaterialPageRouteCupertinoPageRoute都是Flutter提供的一个具体的路由实现类,二者作用相似,提供不同风格的转场动画,下面主要讲解下MaterialPageRoute的相关内容

MaterialPageRoute继承自PageRoute类,并混入MaterialRouteTransitionMixin类,MaterialRouteTransitionMixin提供了根据平台自适应的转场动画。

在源码中介绍如下:

/// A modal route that replaces the entire screen with a platform-adaptive transition.
一种模式路线,用平台自适应过渡代替整个屏幕。

初始化

MaterialPageRoute({
  required this.builder,
  RouteSettings? settings,
  this.maintainState = true,
  bool fullscreenDialog = false,
}) : assert(builder != null),
     assert(maintainState != null),
     assert(fullscreenDialog != null),
     super(settings: settings, fullscreenDialog: fullscreenDialog) {
  assert(opaque);
}
  • builder参数
    • 构建器函数,Function类型,返回一个Widget,即为路由跳转的具体内容
    • 路由用构建器函数而不是子窗口小部件来定义其窗口小部件,因为它将根据推送和弹出的时间在不同的上下文中构建和重建。
  • settings参数
    • 用于传递路由信息,每个路由都有一个唯一的RouteSettings对象,它包含路由的名称和参数
    • RouteSettings的主要作用是帮助Flutter在路由导航过程中正确地管理路由栈,从而实现页面跳转和返回。在使用Navigator进行页面跳转时,我们可以通过RouteSettings来传递路由参数,同时也可以通过它来获取当前路由的名称和参数,方便进行一些路由相关的操作。
    • 另外,RouteSettings还可以用于实现路由拦截和重定向。通过重写Navigator的onGenerateRoute方法,我们可以在路由跳转时对路由进行拦截和修改,例如在用户没有登录的情况下,可以将路由重定向到登录页面。
  • maintainState参数
    • 默认情况下,当一个路由被另一个替换时,上一个路由将保留在内存中。若要在不需要时释放所有资源,请将[maintainState]设置为false
  • fullscreenDialog参数
    • 指定传入路由是否为全屏模式对话框

MaterialPageRoute在不同平台上有不同的转场动画表现,比如在iOS设备上提供了一个默认的从右向左的滑动转场,并附带二次动画和手势转场。

3.ModalRoute 模态路由

PageRouteMaterialPageRoute的父类,ModalRoute又是PageRoute的父类,二者都是是抽象类,提供了一些属性和方法供开发者自定义实现

ModalRoute(模态路由),是一种特殊的路由,它会覆盖在前一个路由之上,并且需要用户进行一些操作后才能返回前一个路由,该抽象类定义了两个关键的方法

  • buildPage方法
    • 用于构建路由页面的Widget
  • buildTransitions方法
    • 抽象方法,实现该方法以提供自定义的页面过渡动画效果,会在模态路由进入或退出时被调用
    • 参数
      • context 上下文对象
      • animation Animation<double>类型,进入或退出的动画,当Navigator将一条路线推到其堆栈顶部时,新路线的主要动画从0.0运行到1.0。当导航器退出最上面的路线时,主动画从1.0运行到0.0。
      • secondaryAnimation 辅助动画,用于被推入界面,即该界面push到下一界面时的转场动画;当Navigator将新路线推到堆栈顶部时,旧的最顶层路线的secondary动画从0.0运行到1.0。当Navigator退出最上方的路线时,其下方的路线的secondary从1.0运行到0.0
      • child 转场的子元素

buildTransitions方法决定了路由转场动画的形式

上面提到的MaterialPageRoute的默认转场动画,就是因为其buildPagebuildTransitions方法由MaterialRouteTransitionMixin混入类完成了实现

@override
Widget buildPage(
  BuildContext context,
  Animation<double> animation,
  Animation<double> secondaryAnimation,
) {
  final Widget result = buildContent(context);
  assert(() {
    if (result == null) {
      throw FlutterError(
        'The builder for route "${settings.name}" returned null.\n'
        'Route builders must never return null.',
      );
    }
    return true;
  }());
  return Semantics(
    scopesRoute: true,
    explicitChildNodes: true,
    child: result,
  );
}
    
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
  final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
  return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}

......
......

Duration get transitionDuration => const Duration(milliseconds: 300);

如果想自定义转场路由,

  • 可以创建一个继承于PageRoute的类,并重写其抽象方法

    @override
    bool get opaque => true;
    
    @override
    bool get barrierDismissible => false;
    
    @override
    bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => nextRoute is PageRoute;
    
    @override
    bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => previousRoute is PageRoute;
    

    以及ModalRoute的抽象方法

    Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
    
    Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
    ) {
    return child;
    }
    
  • 也可以直接继承MaterialPageRoute类,其中对很多抽象方法已经做了实现,这样我们只需要实现buildTransitions方法即可,比较常见的写法

    如果想要更改转场动画时长,可以重写transitionDuration属性的get方法

    Duration get transitionDuration => const Duration(milliseconds: 300);
    
  • 也可以通过PageRouteBuilder来创建路由,效果一致

    PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500),
    pageBuilder: (_, __, ___) => NewPage(),
    transitionsBuilder: (_, animation, __, child) =>
      SlideTransition(
        position: Tween<Offset>(
          begin: Offset(0, 1),
          end: Offset.zero,
        ).animate(animation),
        child: child,
      ),
    );
    

4.TransitionRoute 转场路由

TransitionRoute类是ModalRoute的父类,也是一个抽象类,是Flutter中路由动画的基础,它为我们提供了一个统一的接口,可以方便地实现各种路由动画效果

上面提到的transitionDuration转场时间就是在这里定义的,甚至可以修改返回动画的转场时间reverseTransitionDuration与进入动画不一致

进入动画animation和退出动画secondaryAnimation的动画对象也在这里定义

Duration get transitionDuration;


Duration get reverseTransitionDuration => transitionDuration;

Animation<double>? get animation => _animation;
Animation<double>? _animation;

Animation<double>? get secondaryAnimation => _secondaryAnimation;
final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);

在这个类里面创建了动画控制器_controller,负责控制动画进度

5.OverlayRoute 悬浮窗口路由

OverlayRoute类是TransitionRoute的父类,同样也是一个抽象类,主要作用是在当前Widget树上添加一个悬浮窗口,路由的转场基于这个功能实现,

可以通过调用Navigator.of(context).push方法来显示OverlayRoute,调用Navigator.of(context).pop方法来关闭OverlayRoute

综上:路由类的层级为 MaterialPageRoute/CupertinoPageRoute => PageRoute => ModalRoute => TransitionRoute => OverlayRoute => Route

三、自定义转场动画

自定义转场动画,需要实现buildTransitions方法,根据传入的Animation对象来构建动画的具体实现Widget

关于动画的部分,可以使用Animation和AnimatedWidget来完成

  • Animation
    • 一个表示动画的抽象类,它包含有关动画的状态和进度信息,Animation对象本身并不能实现动画效果
    • 通常使用Tween对象来定义动画的开始和结束状态,并根据一些插值器计算动画的中间状态
  • AnimatedWidget
    • 一个用于动画效果的抽象类,当Listenable参数发生改变时,会重新构建。
    • 可以使用Animation对象的值来构建其自己的UI

AnimatedWidget是一个抽象类,具体可以使用它的一些子类Widget,例如FadeTransition、SlideTransition等,也可以是自定义的动画AnimatedBuilder。

1.系统提供的转场动画类

可用于转场的动画类

  • SlideTransition 滑动动画
  • ScaleTransition 缩放动画
  • RotationTransition 旋转动画
  • FadeTransition 透明度动画

用于小部件内动画的类

  • PositionedTransition 位置动画
  • RelativePositionedTransition 相对位置动画
  • SizeTransition 尺寸大小动画
  • AlignTransition 位置变化动画
  • SliverFadeTransition Sliver过渡动画
  • DefaultTextStyleTransition 默认文本样式过渡
  • DecoratedBoxTransition 装饰盒子过渡

下面来介绍一下这几种转场动画的用法

1.1 SlideTransition 滑动转场动画

const SlideTransition({
  Key? key,
  required Animation<Offset> position,
  this.transformHitTests = true,
  this.textDirection,
  this.child,
}) : assert(position != null),
     super(key: key, listenable: position);

监测对象为positio动画,包含位置信息

  • transformHitTests参数 用于指示命中测试是否应该转换为父级坐标系中的坐标

  • textDirection参数 文本方向

可以用Tween来实现,用于在一段时间内从一个值转换到另一个值的类,它可以用于动画效果、渐变效果等,支持不同类型的值变化,如Color、Offset等等。

 @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0.0, 1.0),
        end: Offset.zero,
      ).chain(CurveTween(curve: Curves.ease)).animate(animation),
      child: child,
    );
  }

这里我们通过animation属性创建了一个从下到上,先加速后减速的转场动画.

底部弹出效果.gif

上面部分有提到secondaryAnimation是辅助动画, 用于被推入界面,即该界面push到下一界面时的转场动画, 我们来试一下效果

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(-1.0, 0),
        end: Offset.zero,
      ).animate(animation),
      child: SlideTransition(
        position: Tween<Offset>(
          begin: Offset.zero,
          end: const Offset(1.0, 0),
        ).animate(secondaryAnimation),
        child: child,
      ),
    );
  }

这里创建的还是从下到上的转场动画,但是当新push的界面再次发生转场时,该界面会出现一个从上到底部的转场。

即A界面push到B时,B界面执行从右到左的侧滑动画,当B界面push到C时,B界面从顶部滑动到底部消失,同时C界面执行自己的转场动画

侧滑+二次动画.gif

1.2 ScaleTransition 缩放转场动画

const ScaleTransition({
  super.key,
  required Animation<double> scale,
  this.alignment = Alignment.center,
  this.filterQuality,
  this.child,
}) : assert(scale != null),
     super(listenable: scale);

监测对象为scale动画,包含缩放比例信息

  • alignment参数为缩放对齐方式

  • filterQuality参数为图像过滤质量

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return ScaleTransition(
    scale: Tween<double>(
      begin: 2.0,
      end: 1.0,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    alignment: Alignment.topRight,
    child: child,
  );
}

可以通过以上方式实现一个以右上角为基点,从两倍视图缩放到正常大小的转场动画

缩放转场.gif

1.3 RotationTransition旋转转场动画

const RotationTransition({
  super.key,
  required Animation<double> turns,
  this.alignment = Alignment.center,
  this.filterQuality,
  this.child,
}) : assert(turns != null),
     super(listenable: turns);

监测对象为turns动画,包含旋转的角度信息,1.0表示旋转360度, 0.5表示旋转180度

  • alignment:旋转中心点的对齐方式,默认中心对齐

  • filterQuality参数为图像过滤质量

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return RotationTransition(
    turns: Tween<double>(
      begin: 0.5,
      end: 0,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    alignment: Alignment.topLeft,
    child: child,
  );
}

这里实现了一个180度,从左上角旋转进入的转场动画

旋转转场.gif

1.4 FadeTransition 透明度转场动画

const FadeTransition({
    Key? key,
    required this.opacity,
    this.alwaysIncludeSemantics = false,
    Widget? child,
  }) : assert(opacity != null),
       super(key: key, child: child);

监测对象为opacity动画,包含透明度信息,

  • alwaysIncludeSemantics: 用于控制是否将子组件的语义信息始终包括在FadeTransition的语义树中,参数为true时,不论子组件的透明度是多少,子组件的语义信息始终会被包括在FadeTransition的语义树中,这意味着子组件在语义上仍然是可访问的。当alwaysIncludeSemantics参数为false时,子组件在不可见时也不会被包括在语义树中,因此无法被访问。
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return FadeTransition(
    opacity: Tween<double>(
      begin: 0,
      end: 1,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    child: child,
  );
}

这里实现了一个透明度渐变显示从0到1的转场动画

透明度转场.gif

2.界面内转场动画

上面提到的都是路由转场时自定义的动画,我们在这里简称为路由转场动画

除了路由转场动画外,在界面build时也可以添加自定义的转场动画,为了便于区分,我们称之为界面内转场动画

界面内转场动画与路由转场动画相互独立,但与路由转场动画一致的是,界面内转场动画也仅会在界面路由push和pop时生效

界面内转场动画可以使用ModalRoute.of(context)!.animation获取对应的转场动画进度相关信息来实现

下面实现了一个界面出现时,除了路由转场动画外,还有一个逐步放大的图片的界面转场动画

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('子部件尺寸动画转场界面'),
    ),
    body: Center(
      child: SizeTransition(
        sizeFactor: Tween<double>(begin: 0, end: 1.0).animate(
          CurvedAnimation(
            parent: ModalRoute.of(context)!.animation!,
            curve: Curves.easeInOut,
          ),
        ),
        child: Icon(
          Icons.flutter_dash,
          size: 400,
        ),
      ),
    ),
  );
}

路由转场这里我们使用自定义的无动画效果的转场,也可以结合上述的各种动画效果来实现

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return child;
  }
界面内动画.gif

界面内自定义转场动画灵活度较高,可以使用系统提供的各种动画Widget,也可以进行各种自定义

3.AnimatedBuilder实现自定义动画

对于涉及额外状态的更复杂情况,请考虑使用AnimatedBuilder。

const AnimatedBuilder({
  super.key,
  required Listenable animation,
  required this.builder,
  this.child,
}) : assert(animation != null),
     assert(builder != null),
     super(listenable: animation);
  • animation: 监听动画,值发生变化时调用builder函数
  • builder: 创建函数,将接收一个BuildContext和一个Widget参数,该Widget参数将在每次调用时重建并更新其状态
@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget? child) {
        return Transform.rotate(
          angle: animation.value * pi * 2,
          child: Transform.scale(
            scale: animation.value,
            child: child
          ),
        );
      },
      child: child,
    );
  }

上面我们实现了一个旋转+缩放的动画转场效果

旋转+缩放.gif
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return AnimatedBuilder(
    animation: animation,
    builder: (BuildContext context, Widget? child) {
      return Transform.translate(
        offset: Offset((1 - animation.value) * 375, (1 - animation.value) * 500),
        child: Transform.scale(
          scale: animation.value,
          child: Opacity(
              opacity: animation.value,
              child: child,
            ),
        ),
      );
    },
    child: child,
  );
}

上面实现了一个位置变换+渐显动画转场效果

位置+渐显.gif

四、拓展

转场过程添加手势

在iOS机型的日常使用过程中,大家会发现flutter提供的默认转场,支持通过手势进行侧滑返回,以及取消转场等,跟原生的转场效果类似

这些是通过CupertinoPageRoute混入CupertinoRouteTransitionMixin类来实现的

 static bool isPopGestureInProgress(PageRoute<dynamic> route) {
    return route.navigator!.userGestureInProgress;
  }
  
  bool get popGestureInProgress => isPopGestureInProgress(this);
  
  bool get popGestureEnabled => _isPopGestureEnabled(this);
  
  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
      ...
      ...
  }
  
  static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
    assert(_isPopGestureEnabled(route));

    return _CupertinoBackGestureController<T>(
      navigator: route.navigator!,
      controller: route.controller!, // protected access
    );
  }

popGestureInProgress属性用于指示是否正在进行手势返回操作

_isPopGestureEnabled方法用于控制Cupertino页面路由转换中手势返回操作的启用状态

_startPopGesture方法是开始手势返回操作

其中通过手势进行返回转场的核心逻辑是在buildPageTransitions方法中通过_CupertinoBackGestureDetector实现的

static Widget buildPageTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    // Check if the route has an animation that's currently participating
    // in a back swipe gesture.
    //
    // In the middle of a back gesture drag, let the transition be linear to
    // match finger motions.
    final bool linearTransition = isPopGestureInProgress(route);
    if (route.fullscreenDialog) {
      return CupertinoFullscreenDialogTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
        linearTransition: linearTransition,
        child: child,
      );
    } else {
      return CupertinoPageTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
        linearTransition: linearTransition,
        child: _CupertinoBackGestureDetector<T>(
          enabledCallback: () => _isPopGestureEnabled<T>(route),
          onStartPopGesture: () => _startPopGesture<T>(route),
          child: child,
        ),
      );
    }
  }

其核心内容是通过手势联动TransitionRoute类提供的controller动画对象,来实现手势对转场进度的控制

详细代码因为具体篇幅较长就不在文章内粘出了,有兴趣的可以参考下demo

实现了一个手势控制透明度转场,效果如下:

IMB_H13Fd7.gif

参考:

Flutter转场动画 SlideTransition

Flutter 转场动效大合集

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

推荐阅读更多精彩内容