翻译:iOS视图控制器编程指南(十)——自定义过渡动画(Customizing the Transition Animations)

过渡动画提供应用界面改变的视觉反馈。UIKit提供一组标准过渡样式,用于present视图控制器时使用,你可以自定义过渡补充标准过渡。

过渡动画序列

过渡动画互换视图控制器的内容。有两种类型的过渡:present和dismiss。present过渡会在应用视图层级结构中添加一个新的视图控制器,而dismiss过渡会从层级结构中删除一个或多个视图控制器。

过渡动画的实现需要很多对象。UIKit提供所有涉及对象的默认版本,你可以自定义这些对象或仅仅继承它们。如果你选择正确的对象,你可以使用少量的代码来创建动画。如果你利用UIKit提供的现成代码,可以很容易实现交互式动画。

过渡委托

过渡动画和自定义present的起点是过渡委托。过渡委托是你定义的一个对象,符合 UIViewControllerTransitioningDelegate协议。它的工作是为UIKit提供以下对象:

  • 动画对象,一个动画对象负责创建动画,用于显示或隐藏视图控制器的视图。过渡委托可以提供独立的动画对象,用来present和dismiss视图控制器。动画对象符合 UIViewControllerAnimatedTransitioning协议。
  • 交互式动画对象。交互式动画对象使用触摸事件或手势识别器,驱动自定义动画时间。交互式动画对象符合UIViewControllerInteractiveTransitioning协议。

创建一个交互式动画最简单的方法是继承 UIPercentDrivenInteractiveTransition 类,并往子类中添加事件处理代码。该类使用现有的动画对象来控制创建动画的时间。如果你创建自己的交互式动画,你必须自己渲染动画的每一帧。

  • present控制器。present控制器管理屏幕上视图控制器的present风格。系统提供了内置的present风格,你可以给自定义present控制器设置你自己的present风格。关于创建自定义present控制器的更多信息,参见创建自定义present(Creating Custom Presentations)。

分配过渡代理到视图控制的 transitioningDelegate属性,告诉UIKit你希望执行一个自定义的过渡或present。你的代理可以选择哪个对象。如果你不提供动画对象,UIKit使用视图控制器的modalTransitionStyle属性中的标准过渡动画。

图10-1展示了过渡代理、presented视图控制器动画对象之间的关系。只有当视图控制器的 modalPresentationStyle属性被设置为UIModalPresentationCustom,才使用present控制器。

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

关于如何实现过渡代理的更多信息,参见过渡代理的实现( Implementing the Transitioning Delegate)。关于过渡代理对象的方法的更多信息,参见UIViewControllerTransitioningDelegate协议引用(UIViewControllerTransitioningDelegate Protocol Reference)。

自定义动画序列

当presented视图控制器的 transitioningDelegate属性包含一个有效的对象,UIKit使用你提供的自定义动画对象present视图控制器。当它准备present,UIKit调用过渡代理的animationControllerForPresentedController:presentingController:sourceController: 方法,检索自定义动画对象。如果对象可用,UIKit执行以下步骤:

  1. UIKit调用过渡代理的 interactionControllerForPresentation:方法,查看是否有可用的交互动画对象。如果该方法返回nil,UIKit执行没有交互的动画。
  2. UIKit调用动画对象的 transitionDuration: 方法来获取动画持续时间。
  3. UIKit调用适当的方法启动动画:对于非交互式动画,UIKit调用动画对象的 animateTransition:方法。对于交互式动画,UIKit调用交互式动画对象的 startInteractiveTransition:方法。
  4. UIKit等待动画对象调用环境过渡对象的 completeTransition:方法。

在动画完成后,自定义动画调用该方法,一般在动画的完成block中。调用该方法会结束过渡,让UIKit知道可以调用完成处理器的 presentViewController:animated:completion:方法,调用动画对象自己的animationEnded:方法。

当dismiss一个视图控制器,UIKit调用过渡代理的 animationControllerForDismissedController:方法并执行以下步骤:

  1. UIKit调用过渡代理的 interactionControllerForDismissal:方法,查看是否有可用的交互式动画对象。如果该方法返回nil,UIKit执行没有交互的动画。
  2. UIKit调用动画对象的 transitionDuration: 方法来获取动画持续时间。
  3. UIKit调用适当的方法启动动画:对于非交互式动画,UIKit调用动画对象的 animateTransition:方法。对于交互式动画,UIKit调用交互式动画对象的 startInteractiveTransition:方法。
  4. UIKit等待动画对象调用环境过渡对象的 completeTransition:方法。

在动画完成后,自定义动画调用该方法,一般在动画的完成block中。调用该方法会结束过渡,让UIKit知道可以调用完成处理器的 presentViewController:animated:completion:方法,调用动画对象自己的animationEnded:方法。

重要:必须在动画结束时调用 completeTransition: 方法。直到调用该方法,UIKit才结束过渡过程,从而返回到应用。

过渡环境对象

过渡动画开始前,UIKit创建过渡环境对象,并添加如何执行动画的信息。过渡环境对象是代码中的重要部分。它实现了UIViewControllerContextTransitioning协议并存储与过渡相关的视图控制器和视图的引用。它还存储如何执行过渡的信息,包括该动画是否为交互式。动画对象需要这些信息来建立和执行实际动画。

重要:当设置自定义动画时,总是使用过渡环境对象中的对象和数据,而不是你自己管理的任何缓存信息。可以在任何条件下过渡,其中一些情况会改变动画参数。保护过渡环境对象,这样可以保证执行动画的正确信息,而你的缓存信息可能是调用动画方法时产生的过期信息。

图10-2展示了过渡环境对象如何与其他对象交互。动画对象接收其 animateTransition:方法的对象。你创建的动画应该发生在提供的容器视图中。例如,当present一个视图控制器,将其视图添加到容器视图中作为子视图。容器视图可能是窗口或一个普通视图,但是配置容器视图是为了运行动画。

图10-2 过渡环境对象

关于过渡环境对象的更多信息,参见UIViewControllerContextTransitioning 协议引用(UIViewControllerContextTransitioning Protocol Reference)。

过渡协调器

内置过渡和自定义过渡,UIKit创建过渡协调器对象帮助需要执行的额外动画。除了视图控制器present和dismiss,当界面发生旋转或视图控制器的frame改变时会发生过渡。所有这些过渡代表视图层级有变化。过渡协调器总是跟踪这些变化同时渲染内容。访问过渡协调器,获取受影响的视图控制器的 transitionCoordinator 属性的对象。过渡协调器只存在过渡过程中。

图10-3展示了present中视图控制器与过渡协调器的关系。使用过渡协调器获取过渡信息、注册动画block。过渡协调器对象符合 UIViewControllerTransitionCoordinatorContext协议,该对象提供时间信息、动画当前状态信息和过渡相关的视图和视图控制器。当执行动画block,他们接收到具有相同信息的环境对象。

图10-3 过渡协调器对象

关于过渡协调器对象的更多信息,参见UIViewControllerTransitionCoordinator 协议引用(UIViewControllerTransitionCoordinator Protocol Reference)。环境信息可以用来配置动画,关于环境信息,参见UIViewControllerTransitionCoordinatorContext协议引用(UIViewControllerTransitionCoordinatorContext Protocol Reference)。

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

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

  1. 创建想要present的视图控制器。
  2. 创建自定义过渡代理对象,并将其分配给视图控制器的 transitioningDelegate属性。必须创建过渡代理的方法,在调用时返回自定义动画对象。
  3. 调用 presentViewController:animated:completion:方法present视图控制器。

当调用presentViewController:animated:completion:方法,UIKit启动present过程。在下一个run loop中启动present,并持续到自定义动画调用 completeTransition: 方法。交互式过渡允许你在过渡进行中处理触摸事件,但非交互式过渡期间运行指定动画对象。

实现过渡代理

过渡代理的目的是创建并返回自定义对象。列表10-1中展示了简单过渡方法的实现。本例创建并返回一个自定义动画对象,大部分实际工作由动画对象本身处理。

列表10-1 创建动画对象

<pre><code>
-(id<UIViewControllerAnimatedTransitioning>)

animationControllerForPresentedController:(UIViewController *)presented

presentingController:(UIViewController *)presenting

sourceController:(UIViewController *)source {

MyAnimator* animator = [[MyAnimator alloc] init];

return animator;

}
</pre></code>

过渡代理的其他方法和前面的方法一样简单。你可以基于应用的当前状态,自定义逻辑返回不同动画对象。关于过渡代理方法的更多信息,参见UIViewControllerTransitioningDelegate 协议引用(UIViewControllerTransitioningDelegate Protocol Reference)。

实现动画对象

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

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

获取动画参数

传递给方法的环境过渡对象包含执行动画所需使用的数据。当你可以从环境过渡对象中获取最新信息,不要使用缓存信息或者从视图控制器获取信息。present和dismiss视图控制器有时涉及视图控制器之外的对象。例如,自定义present控制器可能会添加一个背景视图作为present的一部分。环境过渡对象考虑额外的视图,为你提供正确的视图用于动画。

  • 调用 viewControllerForKey:方法两次获取过渡中的“from”和“to”视图控制器。永远不要认为你知道哪些视图控制器参与过渡。在适应新环境或者响应app请求时,UIKit可能改变视图控制器。
  • 调用 containerView方法获取动画的父视图。添加所有关键子视图到该视图上。例如,在present期间,添加presented视图控制器的视图到该视图上。
  • 调用 viewForKey:方法获取添加或删除的视图。视图控制器的视图可能不是唯一一个在过渡期间添加或删除的视图。present控制器可能添加视图到层级结构,这些视图也必须添加或删除。 viewForKey:方法返回包含所有你需要添加或删除的根视图。
  • 调用 finalFrameForViewController:方法获取添加或删除的视图的最终frame。

环境过渡对象使用“from”和“to”命名法来识别视图控制器、视图和过渡中涉及的frame。“from”视图控制器总是在过渡前显示在屏幕上的视图,“to”视图控制器是在过渡之后显示的视图。如图10-4,“from”和“to”视图控制器在present和dismiss时交换位置。

图10-4 “from”和“to”对象

交换值使编写一个同时处理present和dismiss的动画变得简单。当你设计动画时,你需要做的是了解该属性是渲染present还是dismiss。两者的唯一区别如下:

  • 对于present,添加“to”视图控制器到视图层级结构。
  • 对于dimiss,从视图层级结构中删除“from”视图控制器。

创建过渡动画

在典型的present中,属于presented视图控制器的视图会被渲染。其他视图可能作为present中的一部分,但动画的主要目标是添加到视图层级结构中的视图。

当渲染主要视图,动画需要配置的基础动作都是一样的。从过渡环节对象中获取对象和数据,使用这些信息创建实际动画。

  • present动画使用 viewControllerForKey:viewForKey:方法检索过渡中涉及的视图控制器和视图。设置“to”视图的起始位置。设置其他属性的初始值。通过过渡环境对象的 finalFrameForViewController:方法获取“to”视图的结束位置,添加“to”视图作为容器视图的子视图。在动画block中创建动画,渲染“to”视图到容器视图。设置其他属性为最终值。在完成block中,调用 completeTransition:方法执行其他清理。
  • dismiss动画使用 viewControllerForKey:viewForKey:方法检索过渡中涉及的视图控制器和视图。计算“from”视图的结束位置。该视图属于presented视图控制器,现在被dismiss。添加“to”视图到容器视图作为其子视图。

在present期间,当过渡完成时,属于presenting视图控制器的视图被删除。因此,在dismiss过程中,必须添加该视图到容器视图控制器。

  1. 在动画block中创建动画,渲染“from”视图到容器视图。设置其他属性为最终值。在完成block中,调用completeTransition: 方法从视图层级结构中删除“from”视图。根据需要执行其他清理。

图10-5 展示了自定义present和dismiss过渡,以对角线的方式渲染。在present期间,presented视图从屏幕外开始以对角线方式渲染到左边,直到可见。在dismiss期间,视图改变其方向,从右下角开始消失直到在屏幕外。

图10-5 自定义present和dismiss

列表10-2 展示了如何实现过渡,如图10-5。在检索到动画所需的对象后,animateTransition: 方法计算受影响的视图的frame。在present期间,toView变量代表presented视图。在dismiss期间,fromView变量代表dismiss视图。presenting属性是动画对象自定义属性,当创建动画时,过渡代理设置一个适当的值。

列表10-2 实现present和dismiss动画
<pre><code>
-(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];

}];

}
</pre></code>

动画后的清理

在过渡动画结束后,关键要调用 completeTransition: 方法。调用该方法告诉UIKit过渡完成,用户可能开始使用视图控制器。调用该方法也会触发一连串其他完成处理程序,包括 presentViewController:animated:completion:方法和动画对象的 animationEnded:方法。最好在动画block的完成处理程序中调用completeTransition: 方法。

因为过渡可以被取消,可以使用环境对象的transitionWasCancelled方法返回值来决定是否需要清理。当present被取消,动画必须撤销视图层级结构的任何修改。一个成功的dismiss也需要类似的动作。

在过渡中添加交互

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

你可以使用UIPercentDrivenInteractiveTransition类或继承该类。如果继承,在子类中使用init方法(或startInteractiveTransition:方法)来执行一次性事件处理代码。之后,使用自定义事件处理代码来计算新完成百分比并调用updateInteractiveTransition:方法。当代码确定过渡完成,调用 finishInteractiveTransition方法。

列表10-3 展示了UIPercentDrivenInteractiveTransition类的startInteractiveTransition:方法的自定义实现。该方法设置pan手势识别器来跟踪触摸事件,并设置手势识别器到容器视图。它也保存过渡环境的引用以备后用。

列表10-3 配置percent-driven交互式动画

<pre><code>
-(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];

}
</pre></code>

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

列表10-4 展示pan手势识别器调用的方法,如列表10-3所配置。新事件到达,该方法使用垂直距离计算完成动画的百分比。当手势结束时,该方法完成过渡。

列表10-4 使用事件更新动画进程
<pre><code>
-(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];

}

}
</pre></code>

注意:计算的值代表了完成整个动画长度的百分比。对于交互式动画,你可能希望在动画中避免非线性效果如初始速度,阻尼值和非线性完成曲线。这些效果往往将事件触摸位置与底层视图分割开。

创建额外动画

过渡中涉及的视图控制器可以在present或过渡动画上执行额外的动画。例如,在过渡过程中,presented视图控制器可能渲染其视图层级结构,并添加运动效果或其他视觉反馈。任何对象都可以创建动画,只要它能访问presented或presenting视图控制器的transitionCoordinator属性。过渡协调器只存在过渡过程中。

调用过渡协调器的 animateAlongsideTransition:completion:animateAlongsideTransitionInView:animation:completion: 方法创建动画。提供的block被存储直到过渡动画开始,此时他们和其他过渡动画一起执行。

使用present控制器与动画

对于自定义present,可以提供自己的present控制器给presented视图控制器一个自定义外观。present控制器管理任何独立于视图控制器和其内容的自定义chrome。例如,放置在视图控制器视图之后的模糊视图由present控制器管理。事实上,它并不管理特定视图控制器的视图,这表明在应用中的任何视图控制器可以使用相同的present控制器。

presented视图控制器的过渡代理提供一个自定义present控制器。(视图控制器的 modalTransitionStyle属性必须为UIModalPresentationCustom。)present控制器与其他动画对象一起运行。因为动画对象渲染视图控制器的视图,present控制器渲染其他额外的视图。在过渡结束时,present控制器可以执行视图层级结构的任何调整。

关于如何创建自定义present控制器的信息,参见创建自定义present( Creating Custom Presentations)。

官方原文地址:

https://developer.apple.com/library/prerelease/ios/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html#//apple_ref/doc/uid/TP40007457-CH16-SW1

推荐阅读更多精彩内容