UIViewController(四):自定义转场动画

转场动画提供了与应用程序的界面更改有关的视觉反馈。UIKit提供了一组用于呈现一个视图控制器的标准转场动画样式,我们也可以使用自定义转场动画来实现特定的视觉效果。

转场动画序列

转场动画将一个视图控制器的内容替换为另一个视图控制器的内容。有两种类型的转场:present和dismiss。present转场会将新的视图控制器添加到应用程序的视图控制器结构层次中,而dismiss转场会从视图控制器层次结构中移除一个或者多个视图控制器。

实现转场动画需要许多对象协同工作。UIKit提供了转场过程中涉及的所有对象的默认版本,我们可以自定义所有对象或者仅仅子类化UIKit提供的默认对象。

转场动画委托对象

转场动画委托对象是转场动画和自定义呈现的起点,它是我们定义的遵循UIViewControllerTransitioningDelegate协议的对象。其工作是为UIKit提供以下对象:

  • Animator对象:animator对象负责创建用于显示或者隐藏视图控制器的动画。转场动画委托对象可以提供对应的animator对象来呈现和移除视图控制器。animator对象需要遵循UIViewControllerAnimatedTransitioning协议。
  • 交互式animator对象:交互式animator对象使用触摸事件或者手势识别器来驱动自定义动画的校时,其遵循UIViewControllerInteractiveTransitioning协议。创建交互式animator对象的最简单方法是子类化UIPercentDrivenInteractiveTransition类并添加事件处理代码到子类中。其控制着动画的校时。如果需要自行创建特定的交互式animator对象,则必须自己渲染动画的每一帧。
  • Presentation controller:presentation controller管理着视图控制器在屏幕上显示时的呈现样式。系统为内置的呈现样式提供了presentation controller,我们也可以为自定义的呈现样式提供自定义的presentation controller。

为视图控制器的transitioningDelegate属性值分配一个转场动画委托对象会告知UIKit需要执行自定义转场。转场动画委托对象可以选择其自身提供的对象。如果我们没有提供animator对象,则UIKit在视图控制器的modalTransitionStyle属性中使用标准转场动画。

图10-1显示了转场动画委托和animator对象与被呈现的视图控制器的关系。仅当视图控制器的modalPresentationStyle属性值设为UIModalPresentationCustom时,才使用presentation controller。

图10-1

自定义动画序列

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

  1. UIKit调用转场动画委托的interactionControllerForPresentation:方法来查看交互式animator对象是否可用。如果该方法返回nil,UIKit将执行动画而不需要发生用户交互。
  2. UIKit调用animator对象的transitionDuration:方法来获取动画时长。
  3. UIKit调用适当的方法来启动动画:
    • 对于非交互式动画,UIKit调用animator对象的animateTransition:方法。
    • 对于交互式动画,UIKit调用交互式animator对象的startInteractiveTransition:方法。
  4. UIKit等待aniamtor对象调用转场动画上下文对象的completeTransition:方法。自定义animator对象会在动画执行完毕后,在动画的completion block中调用该方法。调用该方法结束了转场动画,并告知UIKit可以调用presentViewController:animated:completion:方法中的completion handler以及animator对象的animationEnded:方法。

移除视图控制器时,UIKit会调用转场动画委托的animationControllerForDismissedController:方法并执行以下步骤:

  1. UIKit调用转场动画委托的interactionControllerForDismissal:方法来查看交互式animator对象是否可用。如果该方法返回nil,UIKit将执行动画而不需要发生用户交互。
  2. UIKit调用animator对象的transitionDuration:方法来获取动画时长。
  3. UIKit调用适当的方法来启动动画:
    • 对于非交互式动画,UIKit调用aniamtor对象的animateTransition:方法。
    • 对于交互式动画,UIKit调用交互式animator对象的startInteractiveTransition:方法。
  4. UIKit等待animator对象调用转场动画上下文对象的completeTransition:方法。自定义animator对象会在动画执行完毕后,在动画的completion block中调用该方法。调用该方法结束了转场动画,并告知UIKit可以调用dismissViewControllerAnimated:completion:方法中的completion handler以及animator对象的animationEnded:方法。

重要:在动画结束时调用completeTransition:方法是必需的。在调用该方法之前,UIKit不会结束转场动画并将控制器返回给应用程序。

转场动画上下文对象

在转场动画开始执行前,UIKit会创建一个转场动画上下文对象,该转场动画上下文对象会保存关于如何执行动画的信息。转场动画上下文对象是我们代码的重要组成部分,它实现了UIViewControllerContextTransitioning协议并存储对转场动画中涉及的视图控制器和视图的引用。它还存储有关如何执行转场动画的信息,包括动画是否为交互式。animator对象需要所有这些信息来设置和执行实际的动画。

重要:在设置自定义动画时,请始终使用转场动画上下文对象中的对象和数据,不要使用自己管理的任何缓存信息。转场动画可能发生在各种情况下,其中一些情况可能会改变动画参数。转场动画上下文对象能够保证执行动画所需的信息正确,而在调用animator对象的方法时,我们自己缓存的信息可能已经过时。

图10-2显示了转场动画上下文对象如何与其他对象进行交互。animator对象在其animateTransition:方法中接收转场动画上下文对象,我们创建的动画应该在应该在提供的容器视图中执行。例如,当呈现一个视图控制器时,将该视图控制器的视图添加到容器视图中。容器视图可能是window或者常规视图,但其始终被配置去执行动画。

图10-2

有关转场动画上下文对象的更多信息,请参看UIViewControllerContextTransitioning Protocol Reference

转场动画协调器

对于系统提供的转场动画和自定义动画,UIKit都会创建一个转场动画协调器对象来促成我们可能需要执行的任何额外动画。除了呈现和移除视图控制器外,当屏幕旋转或者视图控制器的frame更改时,也可能会触发转场动画。所有的转场动画都意味着对视图层次结构的更改。转场动画协调器是一种跟踪这些更高并同时执行额外动画的方式。要访问转场动画协调器,请获取参与转场的视图控制器的transitionCoordinator属性,转场动画协调器仅在转场动画执行过程中存在。

图10-3显示了转场动画协调器与参与转换的视图控制器之间的关系。使用转场动画协调器获取有关转场动画的信息,并注册想要与转场动画一起同时执行的动画块。转场动画协调器遵循UIViewControllerTransitionCoordinatorContext协议,该协议提供时间信息、有关动画当前状态的信息以及转场过程中涉及的视图和视图控制器。当注册的动画快被执行时,同样会接收到一个具有含有相同信息的转场动画上下文对象。

图10-3

有关转场动画协调器对象的更多信息,请参看UIViewControllerTransitionCoordinator Protocol Reference
有关可用于配置动画的上下文信息的信息,请参看UIViewControllerTransitionCoordinatorContext Protocol Reference

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

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

  1. 创建需要呈现的视图控制器。
  2. 创建自定义转场动画委托对象并将其分配给视图控制器的transitioningDelegate属性。转场动画委托的方法应该在被访问前创建并返回自定义的animator对象。
  3. 调用presentViewController:animated:completion:方法来呈现视图控制器。

当我们调用presentViewController:animated:completion:方法时,UIKit会启动转场。转场会在runloop的下一周期开始执行并继续,直到自定义animator对象调用completeTransition:方法。交互式转场允许我们在转场过程中处理触摸事件,但非交互式转场会在aniamtor对象指定的持续时间内运行。

实现转场动画委托方法

转场动画委托的目的是创建并返回自定义animator对象。以下代码展示了转场动画委托方法的实现是多么简单。本示例创建并返回一个自定义aniamtor对象,大部分实际工作都由aniamtor对象本身处理。

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

转场动画委托的其他方法的实现与上面的方法一样简单。可以结合自定义逻辑,根据应用程序的当前状态返回不同的aniamtor对象。有关转场动画委托方法的更多信息,可以参看UIViewControllerTransitioningDelegate Protocol Reference

实现自定义animator对象

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

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

获取动画参数

传递给animateTransition:方法的转场动画上下文对象包含执行动画时需要使用的数据。绝对不要使用自己的缓存信息或者从视图控制器获取的信息,因为可以从转场动画上下文对象中获取更多最新信息,并且呈现和移除视图控制器时会涉及视图控制器之外的对象。例如,使用presentation controller可能会添加一个背景视图作为呈现内容的一部分。转场动画上下文对象会考虑额外的视图和对象,并为我们提供正确的视图去执行动画。

  • 两次调用viewControllerForKey:方法以获得转场过程中涉及的“from”和“to”视图控制器。切勿假设我们已经知道哪些视图控制器参与了转场,UIKit可能会在适应新的屏幕特性环境或者响应该应用程序的请求时更改视图控制器。
  • 调用containerView方法来获取动画的容器视图,并将所有关键子视图添加到此视图上。例如,在呈现期间,将被呈现的视图控制器的视图添加到此视图。
  • 调用viewForKey:方法获取需要被添加或者移除的视图。视图控制器的视图可能不是在转场动画期间添加或者删除的唯一视图。presentation controller可能会将某些视图插入到视图层次结构中,这些视图也必须被添加或者删除。viewForKey:方法返回包含需要添加或者删除的所有内容的根视图。
  • 调用finalFrameForViewController:方法来获取被添加或者删除的视图的最终frame

转场动画上下文对象使用“from”和“to”命名来标识转场中涉及的视图控制器、视图和frame。“from"视图控制器始终是其视图在转场动画刚开始时就在屏幕上的视图控制器,而“to”视图控制器是其视图在转场动画结束时可见的视图控制器。如图10-4所示,“from”和“to”视图控制器在呈现和移除之间交换位置。

图10-4

对于呈现视图控制器,请将“from”视图添加到容器视图层次结构中。对于移除视图控制器,请从容器视图层次结构中移除“from”视图。

创建转场动画

在典型的转场过程中,需要呈现的视图控制器的视图动画显示到屏幕上。其他视图也可能执行动画,该动画属于整个转场动画的一部分。动画的主要目标始终是视图被添加到视图层次结构中。

  • 呈现动画:

    • 使用viewControllerForKey:viewForKey:方法来得到转场过程中涉及的视图控制器和视图。
    • 设置"to"视图的起始位置,以及设置其他任何属性的起始值。
    • 从转场动画上下文对象的finalFrameForViewController:方法获取“to”视图的最终位置。
    • 添加“to”视图到容器视图上。
    • 创建动画。
      • 在动画Block中,将“to”视图从起始位置动画到在容器视图中的最终位置。同时,将其他任何属性也设置为它们的最终值。
      • 在完成Block中,调用completeTransition:方法并执行任何其他清理。
  • 移除动画:

    • 使用viewControllerForKey:viewForKey:方法来得到转场过程中涉及的视图控制器和视图。
    • 计算"from"视图的最终位置,此视图属于目前被移除的已经呈现的视图控制器。
    • 添加“to”视图到容器视图上。在执行呈现转场动画过程中,发起呈现的视图控制器的视图会在转场完成后被删除。因此,我们必须在移除转场动画过程中将该视图添加回容器视图中。
    • 创建动画。
      • 在动画Block中,将“from”视图从起始位置动画到在容器视图中的最终位置。同时,将其他任何属性也设置为它们的最终值。
      • 在完成Block中,从视图层中移除“from”视图并调用completeTransition:方法并执行需要的任何其他清理。

图10-5显示了一个自定义的呈现和移除转场动画,它们可以对角地呈现视图。在呈现过程中,需要呈现的视图从屏幕边缘开始沿着对角线向左上方移动,直至完全可见。在移除过程中,动画方向颠倒,视图向右下方移动,直到它完全离开屏幕。

图10-5

以下代码显示了如何实现图10-5中的转场动画:

- (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方法的返回值来确定需要进行的清理。当呈现被取消时,animator对象必须撤销对视图层次结构所做的任何修改。移除也需要采取类似的行动。

为转场动画添加交互性

使用动画交互的最简单方法是使用UIPercentDrivenInteractiveTransition对象。UIPercentDrivenInteractiveTransition对象与现有的animator对象配合使用来控制动画的时间,其使用我们提供的完成百分比来执行此操作。我们只需要配置好计算完成百分比和在每个新事件到达时更新完成百分比的代码。

我们可以直接使用UIPercentDrivenInteractiveTransition类,也可以使用其子类。如果使用其子类,则使用子类的init方(或者startInteractiveTransition:方法)来执行事件处理代码的一次性设置。之后,使用自定义的事件处理代码来计算新的完成百分比并调用updateInteractiveTransition:方法。当确定已完成转场时,请调用finishInteractiveTransition方法。

以下代码显示了UIPercentDrivenInteractiveTransition子类的startInteractiveTransition:方法的自定义实现。此方法设置了一个拖拽手势识别器来跟踪触摸事件,并将其与执行动画的容器视图相关联。该方法还保存了对转场动画上下文对象的引用以供以后使用。

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

每当有新的触摸事件传入时,手势识别器调用其关联的操作方法。实现操作方法时,可以使用手势识别器的状态信息来确定手势是成功、失败还是仍在进行中。同时,可以使用最新的触摸事件信息来计算新的完成百分比值。

以下代码显示了配置的拖拽手势识别器关联的操作方法。当有新事件传入时,此方法会使用触摸位置在垂直方向上移动的距离来计算动画的完成百分比。当手势结束时,完成转场。

- (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:方法。我们提供的动画块将被存储直到转场动画开始,此时它们将会与其余的转场动画一起执行。

在转场动画中使用presentation controller

对于自定义呈现,我们可以提供自定义的presentation controller,为需要呈现的视图控制器提供自定义外观。presentation controller管理着与被呈现的视图控制器及其内容分离的任何视图。例如,放置在被呈现的视图控制器的视图后面的调光视图就由presentation controller管理。

我们可以在呈现的视图控制器的转场动画代理中提供自定义presentation controller(视图控制器的modalTransitionStyle属性值必须为UIModalPresentationCustom)。presentation controller与任何animator对象并行操作。随着animator对象将视图控制器的视图动画到其最终位置,presentation controller也会同时将任何其他视图动画到它们的最终位置。在转场结束时,presentation controller有机会对视图层次结构执行任何最终调整。

有关如何创建自定义presentation controller的信息,请参看Creating Custom Presentations;

Demo

Demo地址:https://github.com/Jen668/UIViewControllerDemo

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

推荐阅读更多精彩内容