iOS review系列之自定义转场动画

iOS review系列之使用Segues
iOS review系列之Presenting a View Controller
iOS review系列之UIViewController
iOS review系列之Creating Custom Presentations

转场动画可提供有关应用界面变化的视觉反馈。UIKit提供了一组在展示视图控制器时要使用的标准转场样式,您可以用自己的自定义转场来补充标准转场。

转场动画序列

转场动画将一个视图控制器的内容交换为另一个视图控制器的内容。转场有两种类型:presentations and dismissals。presentation转场向您的应用程序的视图控制器层次结构中添加了一个新的视图控制器,而dismissal转场则从该层次结构中移除了一个或多个视图控制器。

实现转场动画需要很多对象。UIKit提供了转场中涉及的所有对象的默认版本,您可以自定义所有对象或仅自定义子集。如果选择正确的对象集,则应该仅需少量代码即可创建动画。如果利用UIKit提供的现有代码,甚至包括交互的动画也可以轻松实现。

转场代理

转场代理是转场动画和自定义presentation的起点。转场代理是由您定义并符合UIViewControllerTransitioningDelegate协议的对象。它的工作是为UIKit提供以下对象:

  • Animator objects Animator对象负责创建用于显示或隐藏视图控制器视图的动画。转场delegate可以提供单独的Animator对象,以展示和关闭视图控制器。Animator对象遵循UIViewControllerAnimatedTransitioning协议。

  • Interactive animator objects 交互式Animator对象使用触摸事件或手势识别器来驱动自定义动画的计时。交互式Animator对象遵循UIViewControllerInteractiveTransitioning协议。

    创建交互式Animator的最简单方法是对类UIPercentDrivenInteractiveTransition进行子类化,并将事件处理代码添加到子类中。该类控制使用现有Animator对象创建的动画的时间。如果创建自己的交互式Animator,则必须自己渲染动画的每一帧。

  • Presentation controller 当视图控制器在屏幕上时,Presentation controller管理Presentation样式。系统提供了用于内置Presentation样式的Presentation controller,并且您可以为自己的Presentation样式提供自定义Presentation controller。有关创建自定义Presentation controller的更多信息,请参见创建自定义Presentation

将转场代理分配给视图控制器的transitioningDelegate属性会告诉UIKit您要执行自定义转场或Presentation。您的代理可以选择要提供的对象。如果不提供Animator对象,则在视图控制器的modalTransitionStyle属性中UIKit将使用标准转场动画。

下图显示了转场代理和Animator对象与所呈现的视图控制器之间的关系。仅当视图控制器的modalPresentationStyle属性设置为UIModalPresentationCustom时,才使用

custom-presentation-and-animator-objects

有关如何实现转场代理的信息,请参见实现转场代理。有关转场委托对象的方法的更多信息,请参见《UIViewControllerTransitioningDelegate协议参考》

自定义动画序列

当要被展示的视图控制器的属性transitioningDelegate包含有效对象时,UIKit会使用您提供的自定义Animator对象来呈现该视图控制器。在准备presentation时,UIKit会调用你的转场代理方法animationControllerForPresentedController:presentingController:sourceController:来检索自定义动画对象。如果对象可用,则UIKit将执行以下步骤:

  1. UIKit调用转场代理的interactionControllerForPresentation:方法,以查看交互式Animator对象是否可用。如果该方法返回nil,则UIKit将执行而无用户交互的动画。

  2. UIKit调用Animator对象的transitionDuration:方法来获取动画持续时间。

  3. UIKit调用适当的方法来启动动画:

    • 对于非交互式动画,UIKit调用动画对象的animateTransition:方法。

    • 对于交互式动画,UIKit调用交互式Animator对象的startInteractiveTransition:方法。

  4. UIKit等待Animator对象调用上下文转换对象的completeTransition:方法。

    您的自定义Animator在动画完成后(通常在动画的完成的Block中)调用此方法。调用此方法将结束转场,并让UIKit知道它可以调用presentViewController:animated:completion:方法的完成处理程序并调用Animator对象自己的animationEnded:方法。

在关闭视图控制器时,UIKit会调用转场代理的animationControllerForDismissedController:方法并执行以下步骤:

  1. UIKit调用转场代理的interactionControllerForDismissal:方法,以查看交互式Animator对象是否可用。如果该方法返回nil,则UIKit将执行而无用户交互的动画。

  2. UIKit调用Animator对象的transitionDuration:方法来获取动画持续时间。

  3. UIKit调用适当的方法来启动动画:

    • 对于非交互式动画,UIKit调用Animator对象animateTransition:的方法。

    • 对于交互式动画,UIKit调用交互式Animator对象的startInteractiveTransition:方法。

  4. UIKit等待Animator对象调用上下文转换对象的completeTransition:方法。

    您的自定义Animator在动画结束后调用此方法,通常在动画的完成Block中。调用此方法将结束转场,并让UIKit知道它可以调用该presentViewController:animated:completion:方法的完成处理程序并调用Animator对象自己的animationEnded:方法。

需要在动画末尾调用completeTransition:方法。在调用该方法之前,UIKit不会结束转场过程,因此不会将控制权返回给您的app。

转场上下文对象

在转场动画开始之前,UIKit将创建转场上下文对象,并向其填充有关如何执行动画的信息。转场上下文对象是你代码的重要组成部分。它实现UIViewControllerContextTransitioning协议,并存储对视图控制器和转场中涉及的视图的引用。它还存储有关如何执行转场的信息,包括动画是否是交互式的。Animator对象需要所有这些信息来设置和执行实际的动画。

设置自定义动画时,请始终使用转场上下文对象中的对象和数据,而不要使用您自己管理的任何缓存信息。转场可以在多种条件下发生,其中某些条件可能会更改动画参数。转场上下文对象可以确保具有执行动画所需的正确信息,而在调用Animator的方法时,缓存的信息可能已过时。

下图显示了转场上下文对象如何与其他对象交互。Animator对象通过其animateTransition:方法接收该对象。您创建的动画应在提供的容器视图内进行。例如,当呈现视图控制器时,将其视图添加为容器视图的子视图。容器视图可以是窗口视图,也可以是常规视图,但始终将其配置为可运行动画。

transitioning-context-object

有关转场上下文对象的更多信息,请参见《UIViewControllerContextTransitioning协议参考》。**

转场协调器

对于内置转场和自定义转场,UIKit都会创建一个转场协调器对象,以简化您可能需要执行的其他动画。除了展示和关闭视图控制器之外,当发生界面旋转或视图控制器的框架发生更改时,可能还会发生转场。所有这些转场都表示对视图层次结构的更改。转场协调器是一种跟踪这些更改并同时为自己的内容设置动画的方法。要访问转场协调器,请在受影响的视图控制器的transitionCoordinator属性中获取对象。转场协调器仅在转场期间存在。

下图显示了转换协调器与演示中涉及的视图控制器的关系。使用转场协调器获取有关转场的信息,并注册要与转场动画同时执行的动画Block。转场协调器对象符合UIViewControllerTransitionCoordinatorContext协议,该协议提供计时信息,有关动画当前状态的信息以及转场中涉及的视图和视图控制器的信息。执行动画Block时,它们同样会接收具有相同信息的上下文对象。

transition-coordinator-objects

有关转场协调器对象的更多信息,请参见《UIViewControllerTransitionCoordinator协议参考》。有关可用于配置动画的上下文信息的信息,请参见《UIViewControllerTransitionCoordinatorContext协议参考》。****

使用自定义动画呈现视图控制器

要使用自定义动画显示视图控制器,请在现有视图控制器的操作方法中执行以下操作:

  1. 创建要呈现的视图控制器。

  2. 创建您的自定义转场委托对象,并将其分配给视图控制器的transitioningDelegate属性。在需要时,转场代理的方法应创建并返回您的自定义动画对象。

  3. 调用presentViewController:animated:completion:方法以显示视图控制器。

当您调用presentViewController:animated:completion:方法时,UIKit将启动呈现过程。Presentations在下一个运行循环run loop迭代期间开始,并一直持续到自定义Animator调用该completeTransition:方法为止。交互式转场允许您在转场进行期间处理触摸事件,但非交互式转场会在动画对象指定的持续时间内运行。

实现转场代理Transitioning Delegate

转场代理的目的是创建并返回您的自定义对象。下面代码显示了转场方法的实现可以多么简单。本示例创建并返回一个自定义Animator对象。大部分实际工作是由Animator对象本身处理的。

- (id<UIViewControllerAnimatedTransitioning>)
    animationControllerForPresentedController:(UIViewController *)presented
                         presentingController:(UIViewController *)presenting
                             sourceController:(UIViewController *)source {
    MyAnimator* animator = [[MyAnimator alloc] init];
    return animator;
}

转场代理的其他方法可以与前面代码中的方法一样简单。您还可以合并自定义逻辑,以根据APP的当前状态来返回不同的Animator对象。有关转场代理方法的更多信息,请参见《UIViewControllerTransitioningDelegate协议参考》

实现Animator对象Animator Objects

Animator对象是采用UIViewControllerAnimatedTransitioning协议的任何对象。动画对象可创建在固定时间段内执行的动画。Animator对象的关键是其animateTransition:方法,您可以使用该方法创建实际的动画。动画过程大致分为以下几个部分:

  1. 获取动画参数。
  2. 使用Core AnimatinUIView动画方法创建动画。
  3. 清理并完成转场。

获取动画参数

传递给animateTransition:方法的上下文转换对象包含执行动画时使用的数据。当您可以从上下文转换对象获取更多最新信息时,不要使用您自己的缓存信息或从视图控制器获取的信息。呈现和取消视图控制器有时涉及视图控制器外部的对象。例如,自定义presentation控制器可以添加背景视图作为presentation的一部分。上下文转换对象接收了额外的视图和对象,并为您提供了正确的动画视图。

  • 调用方法viewControllerForKey:两次以获取转场中涉及的“from”和“to”视图控制器。永远不要以为您知道哪些视图控制器正在参与转场; UIKit可能会在适应新特性环境的同时更改视图控制器,或者响应app请求时更改。
  • 调用containerView方法以获取动画的父视图。将所有关键子视图添加到该视图。例如,在presentation过程中,将presentation的视图控制器的视图添加到该视图上。
  • 调用viewForKey:方法以获取要添加或删除的视图。在转场期间,视图控制器的视图可能不是唯一添加或删除的视图。presentation控制器可能会将视图插入到层次结构中,这些视图也必须添加或删除。viewForKey:方法返回包含您需要添加或删除的所有内容的根视图。
  • 调用finalFrameForViewController:方法以获取要添加或删除的视图的最终frame rectangle。

上下文转换对象使用from和to命名法来标识转换中涉及的视图控制器,视图和frame rectangle。 from视图控制器始终是在转场开始时显示在屏幕上的视图,而to视图控制器是其视图在转场结束时可见的视图。在下图所见,from和to视图控制器在presentation和dismissal之间交换位置。

from-and-to-objects

交换值可以更轻松地编写一个可以处理presentation和dismissal的Animator。设计Animator时,您要做的就是添加一个属性,以了解它是对c进行动画处理还是对dismissal进行动画处理。两者之间唯一需要的区别如下:

  • 对于presentation,将to视图添加到容器视图层次结构中。
  • 对于dismissal,在容器视图层次结构中删除from视图。

创建转场动画

在典型的presentation过程中,属于被呈现的视图控制器的视图被动画化。其他视图可以作为presentation的一部分进行动画处理,但是动画的主要目标始终是添加到视图层次结构中的视图。

在给主视图设置动画时,您用来配置动画的基本操作是相同的。您可以从转场上下文对象中获取所需的对象和数据,并使用该信息来创建实际的动画。

  • presentation动画:

    1. 使用viewControllerForKey:viewForKey:方法检索转场中涉及的视图控制器和视图。
    2. 设置“ to”视图的开始位置。还要将其他所有属性设置为其初始值。
    3. 从上下文转场上下文的finalFrameForViewController:方法获取“to”视图的结束位置。
    4. 将“ to”视图添加为容器视图的子视图。
    5. 创建动画。
      • 在 animation block中,将“to”视图动画化为其在容器视图中的最终位置。还要将其他所有属性设置为其最终值。
      • 在completion block中,调用completeTransition:方法,然后执行任何其他清理。
  • dismissal动画:

    1. 使用viewControllerForKey:viewForKey:方法检索转场中涉及的视图控制器和视图。

    2. 计算“from”视图的结束位置。该视图属于presented视图控制器,该视图控制器现在已被dismissed。

    3. 将“ to”视图添加为容器视图的子视图。

      在presentation过程中,转场完成后,将移除属于presented视图控制器的视图。因此,您必须在dismissal操作期间将该视图重新添加到容器中。

    4. 创建动画。

      • 在 animation block中,将“from”视图动画化为其在容器视图中的最终位置。还要将其他所有属性设置为其最终值。
      • 在completion block中,从视图层次结构中移除“ from”视图,然后调用该completeTransition:方法。根据需要执行任何其他清理。

    下图显示了一个自定义presentation和dismissal转场,它以对角方式animate视图。在presentation期间,被presented视图从屏幕外启动,并沿对角线向上和向左移动,直到可见为止。在dismissal期间,视图会反转其方向,并向下和向右移动,直到再次离开屏幕。

custom-presentation-and-dismissal

下面代码显示了如何实现上图的转场。检索动画所需的对象后,animateTransition:方法将计算受影响视图的frame rectangle 。在presentation中,所呈现的视图由toView变量表示。在dismissal中,被dismiss的视图由fromView变量表示。presenting属性是动画对象本身的自定义属性,转场代理在创建动画时将其设置为适当的值。

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    // Get the set of relevant objects.
    UIView *containerView = [transitionContext containerView];
    UIViewController *fromVC = [transitionContext
            viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC   = [transitionContext
            viewControllerForKey:UITransitionContextToViewControllerKey];
 
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
 
    // Set up some variables for the animation.
    CGRect containerFrame = containerView.frame;
    CGRect toViewStartFrame = [transitionContext initialFrameForViewController:toVC];
    CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toVC];
    CGRect fromViewFinalFrame = [transitionContext finalFrameForViewController:fromVC];
 
    // Set up the animation parameters.
    if (self.presenting) {
        // Modify the frame of the presented view so that it starts
        // offscreen at the lower-right corner of the container.
        toViewStartFrame.origin.x = containerFrame.size.width;
        toViewStartFrame.origin.y = containerFrame.size.height;
    }
    else {
        // Modify the frame of the dismissed view so it ends in
        // the lower-right corner of the container view.
        fromViewFinalFrame = CGRectMake(containerFrame.size.width,
                                      containerFrame.size.height,
                                      toView.frame.size.width,
                                      toView.frame.size.height);
    }
 
    // Always add the "to" view to the container.
    // And it doesn't hurt to set its start frame.
    [containerView addSubview:toView];
    toView.frame = toViewStartFrame;
 
    // Animate using the animator's own duration value.
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                     animations:^{
                         if (self.presenting) {
                             // Move the presented view into position.
                             [toView setFrame:toViewFinalFrame];
                         }
                         else {
                             // Move the dismissed view offscreen.
                             [fromView setFrame:fromViewFinalFrame];
                         }
                     }
                     completion:^(BOOL finished){
                         BOOL success = ![transitionContext transitionWasCancelled];
 
                         // After a failed presentation or successful dismissal, remove the view.
                         if ((self.presenting && !success) || (!self.presenting && success)) {
                             [toView removeFromSuperview];
                         }
 
                         // Notify UIKit that the transition has finished
                         [transitionContext completeTransition:success];
                     }];
 
}

动画后清理

在转场动画结束时,至关重要的是调用completeTransition:方法。调用该方法将告诉UIKit转场已完成,并且用户可以开始使用提供的视图控制器。调用该方法还会触发其他完成处理程序的级联,包括presentViewController:animated:completion:方法中的一个和Animator对象自己的animationEnded:方法。调用该completeTransition:方法的最佳位置是动画块的completion handler。

因为可以取消转场,所以应该使用transitionWasCancelled上下文对象的方法的返回值来确定需要进行的清理。取消presentation后,Animator必须撤消对视图层次结构所做的任何修改。成功地dismissal也需要采取类似的措施。

为转场添加交互性

使动画具有交互性的最简单方法是使用UIPercentDrivenInteractiveTransition对象。UIPercentDrivenInteractiveTransition对象与您现有的Animator对象配合使用,以控制其动画的时间安排。它使用您提供的完成百分比值来执行此操作。您所需要做的就是设置事件处理代码,以计算完成百分比值,并在每个新事件到来时对其进行更新。

你可以使用一个UIPercentDrivenInteractiveTransition类,它可以包含子类,也可以不包含子类。如果您创建了子类,请使用子类的init方法(或startInteractiveTransition:方法)来执行事件处理代码的一次性设置。之后,使用您的自定义事件处理代码来计算一个新的完成百分比值,并调用updateInteractiveTransition:方法。当您的代码确定转场应该完成时,调用finishInteractiveTransition方法。

下面代码显示了startInteractiveTransition的自定义实现:UIPercentDrivenInteractiveTransition子类的方法。该方法设置一个pan手势识别器来跟踪触摸事件,并将该手势识别器安装到动画的容器视图中。它还保存了对转场上下文的引用,供以后使用。

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
   // Always call super first.
   [super startInteractiveTransition:transitionContext];
 
   // Save the transition context for future reference.
   self.contextData = transitionContext;
 
   // Create a pan gesture recognizer to monitor events.
   self.panGesture = [[UIPanGestureRecognizer alloc]
                        initWithTarget:self action:@selector(handleSwipeUpdate:)];
   self.panGesture.maximumNumberOfTouches = 1;
 
   // Add the gesture recognizer to the container view.
   UIView* container = [transitionContext containerView];
   [container addGestureRecognizer:self.panGesture];
}

一个手势识别器为每一个到达的新事件调用它的动作方法。操作方法的实现可以使用手势识别器的状态信息来确定手势是否成功、失败或仍在进行中。同时,您可以使用最新的触摸事件信息来计算手势的新百分比值。

下面代码显示了上面代码中配置的pan手势识别器调用的方法。当新的事件到达时,这个方法使用垂直移动距离来计算动画的完成百分比。当手势结束时,该方法完成转场。

-(void)handleSwipeUpdate:(UIGestureRecognizer *)gestureRecognizer {
    UIView* container = [self.contextData containerView];
 
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        // Reset the translation value at the beginning of the gesture.
        [self.panGesture setTranslation:CGPointMake(0, 0) inView:container];
    }
    else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        // Get the current translation value.
        CGPoint translation = [self.panGesture translationInView:container];
 
        // Compute how far the gesture has travelled vertically,
        //  relative to the height of the container view.
        CGFloat percentage = fabs(translation.y / CGRectGetHeight(container.bounds));
 
        // Use the translation value to update the interactive animator.
        [self updateInteractiveTransition:percentage];
    }
    else if (gestureRecognizer.state >= UIGestureRecognizerStateEnded) {
        // Finish the transition and remove the gesture recognizer.
        [self finishInteractiveTransition];
        [[self.contextData containerView] removeGestureRecognizer:self.panGesture];
    }
}

您计算的值表示整个动画长度的完成百分比。对于交互式动画,您可能希望在动画本身中避免诸如初始速度、阻尼值和非线性完成曲线等非线性效果。这样的效果往往会将事件的触摸位置与任何底层视图的移动分离开来。

创建与转场同时运行的动画

涉及转场的视图控制器可以在任何presentation或transition动画之上执行额外的动画。例如,呈现的视图控制器可以在转场期间为其自身的视图层次设置动画,并在转场发生时添加运动效果或其他视觉反馈。任何对象都可以创建动画,只要它能够访问被呈现或呈现视图控制器的transitionCoordinator属性。转场协调器仅在转场正在进行时存在。

要创建动画,请调用转场协调器的animateAlongsideTransition:completion:animateAlongsideTransitionInView:animation:completion:方法。提供的block将一直存储到转场动画开始时,此时它们将与其他转场动画一起执行。

在动画中使用演示控制器

对于自定义presentation,您可以提供自己的presentation控制器,以给被呈现的视图控制器一个自定义外观。presentation控制器管理任何与视图控制器及其内容分离的自定义chrome。例如,放置在视图控制器的视图后面的dimming视图将由presentation控制器管理。它不管理特定视图控制器的视图这一事实意味着你可以将相同的presentation控制器与app中的任何视图控制器一起使用。

您可以从被呈现视图控制器的转场代理中提供自定义presentation控制器。(视图控制器的modalTransitionStyle属性必须是UIModalPresentationCustom。)表示控制器与任何Animator对象并行操作。当Animator对象将视图控制器的视图放入适当的位置时,presentation controller将任何额外的视图放入适当的位置。在转场结束时,presentation控制器有机会对视图层次结构执行任何最终调整。

有关如何创建自定义演示文稿控制器的信息,请参见创建自定义演示文稿

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

推荐阅读更多精彩内容