ViewController编程指南展示和过渡-自定义过渡动画

过渡动画可提供应用程序界面变更的视觉反馈。 UIKit提供了一组标准过渡样式,以便在呈现视图控制器时使用,并且您可以使用自己的自定义过渡来补充标准过渡。

过渡动画

过渡动画将一个视图控制器的内容交换为另一个的内容。 有两种类型的过渡:呈现和消失。 呈现转换会向应用程序的视图控制器层次结构添加新的视图控制器,而取消转换会从层次结构中删除一个或多个视图控制器。

实现过渡动画需要许多对象。 UIKit提供过渡中涉及的所有对象的默认版本,你可以自定义所有对象或仅定义一个子集。 如果你选择正确的对象集,你应该能够创建你的动画只有少量的代码。 即使包含交互的动画,如果你利用UIKit提供的现有代码,也可以轻松实现。

过渡代理

过渡代理是过渡动画和自定义呈现的起点。 过渡委托是你定义的并且符合UIViewControllerTransitioningDelegate协议的对象。 它的工作是为UIKit提供以下对象:

  • 动画对象。 animator对象负责创建用于显示或隐藏视图控制器视图的动画。 过渡委托可以提供用于呈现和解除视图控制器的单独的动画对象。 Animator对象符合UIViewControllerAnimatedTransitioning协议。
  • 交互式动画对象。 交互式动画对象使用触摸事件或手势识别器来驱动自定义动画的定时。 交互式动画对象符合UIViewControllerInteractiveTransitioning协议。 创建交互式动画制作最简单的方法是将UIPercentDrivenInteractiveTransition类子类化,并向你的子类添加事件处理代码。 该类控制使用现有动画对象创建的动画的时间。 如果你创建自己的交互式动画师,您必须自己渲染动画的每个帧。
  • 呈现控制器。 呈现控制器管理呈现风格,而视图控制器在屏幕上。 系统提供了内置呈现样式的演示控制器,您可以为自己的呈现样式提供自定义呈现控制器。

将过渡委托分配给视图控制器的transitioningDelegate属性会告诉UIKit你要执行自定义过渡或呈现。 您的委托可以选择性地提供哪些对象。 如果不提供animator对象,UIKit在视图控制器的modalTransitionStyle属性中使用标准的过渡动画。

图10-1显示了过渡委托和动画对象与提供的视图控制器的关系。 表示控制器仅在视图控制器的modalPresentationStyle属性设置为UIModalPresentationCustom时使用

图10-1自定义呈现和动画对象

VCPG_custom-presentation-and-animator-objects_10-1_2x.png

自定义动画

当呈现的视图控制器的transitioningDelegate属性包含有效对象时,UIKit使用您提供的自定义动画对象来呈现视图控制器。 当它准备一个呈现视图时,UIKit调用animationControllerForPresentedController:presentingController:sourceController:方法转换委托来检索自定义animator对象。 如果对象可用,UIKit将执行以下步骤:

  1. UIKit调用转换委托的interactionControllerForPresentation:方法来查看交互式animator对象是否可用。 如果该方法返回nil,UIKit执行动画而无需用户交互。
  2. UIKit调用animator对象的transitionDuration:方法来获取动画持续时间。
  3. UIKit调用适当的方法来启动动画:
    • 对于非交互式动画,UIKit会调用animator对象的animateTransition:方法。
    • 对于交互式动画,UIKit调用交互式动画对象的startInteractiveTransition:方法。
  4. UIKit等待animator对象调用上下文转换对象的completeTransition:方法。
    你的自定义动画在动画完成之后调用此方法,通常在动画的完成块中。 调用这个方法结束过渡,让UIKit知道它可以调用presentViewController的完成处理程序:animated:completion:方法,并调用animator对象自己的animationEnded:方法。

当关闭视图控制器时,UIKit调用您的转换委托的animationControllerForDismissedController:方法,并执行以下步骤:

  1. UIKit调用转换委托的interactionControllerForDismissal:方法来查看交互式animator对象是否可用。 如果该方法返回nil,UIKit执行动画而无需用户交互。
  2. UIKit调用animator对象的transitionDuration:方法来获取动画持续时间。
  3. UIKit调用适当的方法来启动动画:
    • 对于非交互式动画,UIKit会调用animator对象的animateTransition:方法。
    • 对于交互式动画,UIKit调用交互式动画对象的startInteractiveTransition:方法。
  4. UIKit等待animator对象调用上下文转换对象的completeTransition:方法。
  5. 你的自定义动画在动画完成后调用此方法,通常在动画的完成块中。 调用这个方法结束转换,让UIKit知道它可以调用presentViewController的完成处理程序:animated:completion:方法,并调用animator对象自己的animationEnded:方法。

重要

在动画结束时调用completeTransition:方法是必需的。 UIKit不会结束转换过
程,从而将控制权返回到您的应用程序,直到您调用该方法。

过渡上下文对象

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

重要

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

图10-2显示了过渡上下文对象如何与其他对象交互。 你的animator对象在其animateTransition:方法中接收对象。 你创建的动画应该在提供的容器视图中进行。 例如,在呈现视图控制器时,将其视图添加为容器视图的子视图。 容器视图可能是窗口或常规视图,但它始终配置为运行动画。

图10-2过渡上下文对象

VCPG_transitioning-context-object_10-2_2x.png

过渡协调

对于内置转换和您的自定义转换,UIKit创建一个转换协调器对象,以便于您可能需要执行的任何额外的动画。 除了视图控制器的呈现和消除之外,当发生界面旋转时或当视图控制器的框架改变时,可以发生过渡。 所有这些过渡关注对视图层次结构的改变。 过渡协调器是一种跟踪这些更改并同时动画制作您自己的内容的方法。 要访问过渡协调器,请获取受影响的视图控制器的transitionCoordinator属性中的对象。 过渡0协调器仅存在于过渡的持续时间。

图10-3显示了过渡协调器与呈现中涉及的视图控制器的关系。 使用过渡协调器获取有关过渡的信息,并注册要与过渡动画同时执行的动画块。 过渡协调器对象符合UIViewControllerTransitionCoordinatorContext协议,该协议提供定时信息,关于动画的当前状态的信息,以及过渡中涉及的视图和视图控制器。 当您的动画块被执行时,它们类似地接收具有相同信息的上下文对象。

图10-3过渡协调器对象

![VCPG_from-and-to-objects_10-4_2x.png](http://upload-images.jianshu.io/upload_images/133280-17ae5755207363a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

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

  1. 创建要显示的视图控制器。
  2. 创建您的自定义过渡委托对象并将其分配给视图控制器的transitioningDelegate属性。 过渡委托的方法应该在提出时创建并返回你的自定义动画对象。
  3. 调用presentViewController:animated:completion:方法来呈现视图控制器。

当调用presentViewController:animated:completion:方法时,UIKit启动呈现过程。 呈现在下一个运行循环迭代期间开始,并继续,直到您的自定义动画师调用completeTransition:方法。 交互式过渡允许你在过渡正在进行时处理触摸事件,但非交互式过渡在由animator对象指定的持续时间内运行。

实现Transitioning Delegate

过渡委托的目的是创建和返回自定义对象。 代码清单10-1显示了过渡方法的实现可以如何简单。 此示例创建并返回自定义动画对象。 大多数实际工作都是由animator对象本身处理的。

代码清单10-1创建animator对象

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

过渡委托的其他方法可以像前面列表中的一样简单。 您还可以结合自定义逻辑,根据应用程序的当前状态返回不同的动画对象。

实现您的动画对象

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

  1. 获取动画参数。
  2. 使用Core Animation或UIView动画方法创建动画。
  3. 清理并完成过渡。

获取动画参数

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

  • 调用viewControllerForKey:方法两次以获得从过渡中涉及的“从”和“到”视图控制器从不假设你知道哪些视图控制器参与转换UIKit可能改变视图控制器,同时适应新的特性 环境或响应你的应用程序的请求。
  • 调用containerView方法获取动画的superview。 将所有关键子视图添加到此视图。 例如,在呈现期间,将呈现的视图控制器的视图添加到此视图。
  • 调用viewForKey:方法来获取要添加或删除的视图。 视图控制器的视图可能不是在过渡期间添加或删除的唯一视图。 表示控制器可能将视图插入到层次结构中,也必须添加或删除。 viewForKey:方法返回包含您需要添加或删除的所有内容的根视图。
  • 调用finalFrameForViewController:方法来获取要添加或删除的视图的最终框架矩形。

上下文转换对象使用“from”和“to”命名法来标识转换中涉及的视图控制器,视图和框架矩形。 “从”视图控制器总是在转换开始时在屏幕上的视图,并且“到”视图控制器是其视图在转换结束时可见的那个。 如图10-4所示,“from”和“to”视图控制器在显示和关闭之间切换位置。

图10-4 from和to对象

交换值使得更容易编写处理呈现和解除的单个动画。 当你设计你的动画师时,你需要做的是包含一个属性来知道它是动画的演示还是解雇。 两者之间唯一需要的区别如下:

  • 对于呈现,将“到”视图添加到容器视图层次结构中。
  • 对于关闭,从容器视图层次结构中删除“from”视图。

创建过渡动画

在典型的呈现期间,将属于呈现的视图控制器的视图动画化到位。 其他视图可能会作为呈现视图的一部分进行动画处理,但是动画的主要目标是将视图添加到视图层次结构中。

当对主视图进行动画处理时,您配置动画所需的基本操作是相同的。 从过渡的上下文对象中获取所需的对象和数据,并使用该信息创建实际的动画。

  • 呈现动画

    • 使用viewControllerForKey:和viewForKey:方法来检索过渡中涉及的视图控制器和视图。
    • 设置“from”视图的起始位置。 将任何其他属性设置为其起始值。
    • 从上下文过渡上下文的finalFrameForViewController:方法中获取“to”视图的结束位置。
    • 将“to”视图添加为容器视图的子视图。
    • 创建动画。
      • 在动画块中,将“to”视图动画化到容器视图中的最终位置。 将任何其他属性设置为其最终值。
      • 在完成块中,调用completeTransition:方法,并执行任何其他清理。
  • 关闭动画:

    • 使用viewControllerForKey:和viewForKey:方法来检索过渡中涉及的视图控制器和视图。
    • 计算“from”视图的结束位置。 此视图属于提交的视图控制器,现在正在被关闭。
    • 将“to”视图添加为容器视图的子视图。在呈现期间,当过渡完成时,移除属于呈现视图控制器的视图。 因此,您必须在关闭操作期间将该视图添加回容器。
    • 创建动画。
      • 在动画块中,将“from”视图动画化到容器视图中的最终位置。 将任何其他属性设置为其最终值。
      • 在完成块中,从视图层次结构中删除“from”视图,并调用completeTransition:方法。 根据需要执行任何其他清理。

图10-5显示了一个自定义的显示和关闭过渡,动画是它的对角视图。 在呈现期间,呈现的视图从屏幕开始,向上和向左对角线动画,直到它可见。 在关闭期间,视图反转其方向,向下和向右动画,直到它再次离屏。

图10-5自定义呈现和关闭

VCPG_custom-presentation-and-dismissal_10-5_2x.png

代码清单10-2显示了如何实现图10-5中所示的过渡。 检索动画所需的对象后,animateTransition:方法计算受影响视图的框架矩形。 在呈现期间,所呈现的视图由toView变量表示。 在关闭中,已解除的视图由fromView变量表示。 presentation属性是animator对象本身的一个自定义属性,过渡委托在创建animator时设置为一个合适的值。

代码清单10-2用于实现对角呈现和关闭的动画

- (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:方法的最好的地方是在你的动画块的完成处理程序中。

因为过渡可以取消,所以您应该使用上下文对象的transitionWasCancelled方法的返回值来确定需要清除。 取消呈现视图时,您的动画必须撤消对视图层次结构所做的任何修改。 关闭的成功需要类似的操作。

向你的过渡添加交互性

使动画交互的最简单的方法是使用UIPercentDrivenInteractiveTransition对象。 UIPercentDrivenInteractiveTransition对象与你现有的动画对象配合使用,以控制其动画的时间。 它使用你提供的完成百分比值。 所有你需要做的是设置计算完成百分比值所需的事件处理代码,并在每个新事件到达时更新它。

您可以使用带有或不带有子类的UIPercentDrivenInteractiveTransition类。 如果是子类,使用子类的init方法(或startInteractiveTransition:方法)来执行事件处理代码的一次性设置。 之后,使用自定义事件处理代码计算新的完成百分比值,并调用updateInteractiveTransition:方法。 当代码确定过渡完成时,调用finishInteractiveTransition方法。

代码清单10-3显示了UIPercentDrivenInteractiveTransition子类的startInteractiveTransition:方法的自定义实现。 此方法设置一个摇动手势识别器来跟踪触摸事件,并将该手势识别器安装在动画的容器视图上。 它还保存对过渡上下文的引用以供以后使用。

代码清单10-3配置百分比驱动交互式动画设计器

- (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];
}

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

代码清单10-4显示了由代码清单10-3中配置的平移手势识别器调用的方法。 当新事件到达时,该方法使用垂直行程距离来计算动画的完成百分比。 当手势结束时,该方法结束过渡。

代码清单10-4使用事件更新动画进度

-(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];
    }
}

注意

您计算的值表示动画的整个长度的完成百分比。 对于交互式动画,您可能需要避免非
线性效应,例如动画本身中的初始速度,阻尼值和非线性完成曲线。 这种效应倾向于
将事件的触摸位置与任何下层视图的移动分离。

创建与过渡一起运行的动画

过渡中涉及的视图控制器可以在任何呈现或过渡动画之上执行其他动画。 例如,呈现的视图控制器可以在过渡期间对其自己的视图层级进行动画化,并且在发生过渡时添加运动效果或其他视觉反馈。 任何对象都可以创建动画,只要它能够访问呈现或呈现视图控制器的transitionCoordinator属性即可。 过渡协调器仅在转换正在进行时存在。

要创建动画,请调用过渡协调器的animateAlongsideTransition:completion:或animateAlongsideTransitionInView:animation:completion:方法。 您提供的块将被存储,直到过渡动画开始,此时它们与其余的过渡动画一起执行。

呈现控制器与你的动画

对于自定义呈现,您可以提供自己的呈现控制器,为所呈现的视图控制器提供自定义外观。 呈现控制器管理与视图控制器及其内容分离的任何自定义。 例如,放置在视图控制器视图后面的调光视图将由呈现控制器管理。 事实上,它不管理特定的视图控制器的视图意味着您可以使用相同的呈现控制器与您的应用程序中的任何视图控制器。

你从呈现的视图控制器的过渡委托提供自定义的显示控制器。 (视图控制器的modalTransitionStyle属性必须是UIModalPresentationCustom。)呈现控制器与任何动画对象并行操作。 由于动画师对象将视图控制器的视图动画化到位,呈现控制器将任何附加视图动画化到位。 在过渡结束时,呈现控制器有机会对视图层次执行任何最终调整。

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

推荐阅读更多精彩内容