编程

当前位置:永利皇宫463登录 > 编程 > 控制器转场动画自定义,AppCoda翻译系列

控制器转场动画自定义,AppCoda翻译系列

来源:http://www.makebuLuo.com 作者:永利皇宫463登录 时间:2019-09-12 14:29

本文翻译总结自AppCoda以下两篇文章:

大家好,我是KK黄凯展,自从iOS7出来后,自定义转场动画就变得非常流行,可以做出很炫的效果,如果设计得当,那么与用户的交互会变得有趣的多,因此在这里跟大家分享一下自定义动画的心得,废话不多说,直接上图!

自定义转场动画

这张图是自己在翻译官方文档Customizing the Transition Animations的总结:

图片 1

转场动画过程

接下来是漫长的翻译部分,英文水平有限,有些单词不知道怎么去表达(比如persentations,dismissals,chrome等等),各位看官多多指教,在下感激不尽。

  • Introduction to Custom View Controller Transitions and Animations
  • Creating a Slide Down Menu Using View Controller Transition
  • 这是默认push和pop的效果

自定义转场(过渡)动画

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

iOS 7开始,苹果为开发者提供了自定义控制器转场动画相关的API,而实现该功能需要以下三个步骤:

图片 2

转场动画序列

转场动画将一个控制器的内容交换为另一个的内容。有两种类型的转场:presentationsdismissals。一个presentations转场会向应用程序的视图控制器层级添加一个新的视图控制器,而dismissal转场会从层级中移除一个或多个视图控制器。

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

  • 创建一个类作为动画管理器,该类需继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议,我们在这个类中编写我们的动画执行代码。
  • 为目标控制器指定转场动画代理,既可以使用上一步创建的动画管理器对象,也可以指定来源控制器作为这个代理。
  • 实现代理协议中的相应方法,在方法中返回第一步创建的动画管理器对象。
  • 这是自定义push和pop的效果
转场的代理

转场代理是转场动画和自定义presentations的起点。转场代理是一个你定义的并遵守UIViewControllerTransitioningDelegate协议的对象。它的工作是为UIKit提供以下对象:

  1. 动画对象。动画对象是负责创建用于显示或隐藏一个视图控制器的视图的动画。转场代理可以提供用于presentingdismissing视图控制器的单独的动画对象。动画对象遵守UIViewControllerAnimatedTransitioning协议。
  2. 交互式动画对象。交互式动画对象使用触摸事件或者手势识别器来驱动自定义动画的定时。交互式动画对象遵守UIViewControllerInteractiveTransitioning协议。创建交互式动画的最简单方法是将UIPercentDrivenInteractiveTransition子类化,并向你的子类添加事件处理代码。该类控制使用现有动画对象创建的动画的时间。如果你创建自己的交互式动画,你必须自己渲染动画的每个帧。
  3. 呈现的控制器(persentation controller)。在视图控制器在屏幕上时呈现控制器管理着呈现风格。系统提供了内置呈现样式的呈现控制器,你可以为你自己的呈现样式提供自定义呈现控制器。有关创建自定义的呈现控制器的更多信息,请看Creating Custom Presentations.

给视图控制器的transitioningDelegate赋值一个转场代理对象,告诉UIKit你想要执行自定义转场或呈现。你的代理可以选择它提供的哪些对象。如果你不提供动画对象,UIKit会在视图控制器的modalTransitionStyle属性中使用标准的转场动画。

图10-1显示转场代理和动画对象与被呈现视图控制器的关系。presentation controller仅在视图控制器的modalPresentationStyle的属性设置为UIModalPresentationCustom时使用。

图片 3

图10-1

有关如何实现你的转场代理的更多信息,请看Implement the Transitioning Delegate.有关转场代理对象的方法的更多信息,请看UIViewControllerTransitioningDelegate Protocol Reference.

下载示例程序,地址在这里。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里。)

图片 4

转场动画序列

当一个被呈现的视图控制器的transitioningDelegate属性包含一个有效的对象时,UIKit会使用你提供的自定义动画对象呈现那个视图控制器。在准备呈现时,UIKit会调用你的转场代理的animationControllerForPresentedController:presentingController:sourceController:方法来获取自定义动画对象。如果一个对象是有效的,UIKit会执行下面的步骤:

  • 如果一个交互式动画对象是有效的,UIKit会调用转场代理的interactionControllerForPresentation:方法。如果该方法返回nil,UIKit会执行没有用户交互的动画。
  • UIKit会调用动画对象的transitionDuration:方法来获取动画时长。
  • UIKit会调用恰当的方法来开始动画:
    • 对于非交互式动画,UIKit会调用动画对象的animateTransition:方法
    • 对于交互式动画,UIKit会调用交互式动画对象的startInteractiveTransition:方法。
  • UIKit等待一个动画对象调用上下文转场对象的completeTransition:方法。你的自定义动画对象在动画完成后调用这个方法,尤其是在动画的完成回调里。调用这个方法来结束转场,并且让UIKit知道它可以调用presentViewController:animated:completion的完成处理和调用动画对象自己的animationEnded:方法。

dismissing一个视图控制器时,UIKit调用你的转场代理的animationControllerForDismissedController:方法并执行下面的步骤:

  • 如果一个交互式动画对象时有效的,UIKit会调用转场代理的interactionControllerForDismissal:方法。如果方法返回nil,UIKit会执行没有用户交互的动画。
  • UIKit会调用动画对象的transitionDuration:方法来获取动画时长。
  • UIKit会调用恰当的方法来开始动画:
    • 对于非交互式动画,UIKit会调用动画对象的animateTransition:方法
    • 对于交互式动画,UIKit会调用交互式动画对象的startInteractiveTransition:方法。
  • UIKit等待一个动画对象调用上下文转场对象的completeTransition:方法。你的自定义动画对象会在他的动画完成后调用这个方法,尤其是在动画的完成回调里。调用这个方法来结束转场,并让UIKit知道它可以调用presentViewController:animated:completion:方法的完成处理回调和调用动画对象自己的animationEnded:方法。

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

示例程序如下图所示,点击导航栏上的Action按钮会modal出一个目标控制器,点击Dismiss按钮会返回来源控制器,只不过现在使用的是系统默认的modal动画,接下来我们就来实现自定义转场动画。

从上面的图我们可以清晰的看到,控制器跳转的动画效果已截然不同,那么究竟是怎样实现的呢?下面我将为大家揭秘!

转场上下文对象

在转场动画开始前,UIKit会创建一个转场上下文对象,并给它填充关于如何执行动画的信息。转场上下文兑现是你的代码中重要的一部分。它实现了UIViewControllerContextTransitioning协议,并储存着在转场中视图控制器和视图相关的引用。它也储存着一些信息,关于你应该如何执行转场,包括动画是否是交互的。你的动画对象需要所有的这些信息来设置和执行真实的动画。

注意:在设置自定义动画时,总是使用转场上下文对象中的对象和数据而不是使用你管理自己的缓存信息。转场在多种条件下会发生,有一些可能会改变动画的参数。转场上下文对象保证了你需要执行动画的正确信息,而你的缓存信息可能会在你调用动画的方法时失效。

图10-2显示转场上下文对象如何和其他对象交互。你的动画对象在它的animateTransition:方法中接受对象。你创建的动画应该在提供的容器视图中进行。例如,当正在呈现一个视图控制器时,添加它的视图作为这个容器视图的子视图。这个容器视图可能会是窗口或者一个有规律的视图,但是它总是被配置来运行你的动画。
图:

图片 5

图10-2

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

图片 6


转场协调器

对于内置的转场和你自定义的转场,UIKit会创建一个转场协调器对象来促进任何你可能需要执行的特别的动画。除了视图控制器的presentationdismissal外,转场会在界面发生旋转或视图控制器的frame改变时产生。所有这些转场代表着视图层级的改变。转场协调器是一种方式来跟踪这些变化,并同时使你的内容动画。访问转场协调器,从受影响的视图控制器的transitionCoordinator属性中获取该对象。转场协调器只在转场过程中存在。

图10-3显示转场协调器与有关presentation的视图控制器的关系。使用转场协调器来获取转场的信息,并注册要与转场动画同时执行的动画块。转场协调器对象遵守UIViewControllerTransitionCoordinatorContext协议,它提供定时信息,动画的当前状态信息,在转场中涉及的视图和视图控制器。当你的动画块被执行时,它一样会收到一个上下文对象带有相同的信息。

图片 7

图10-3

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

创建一个类名称为CustomPresentAnimationController,继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议。这个协议有两个必须实现的方法,我们的实现代码如下:

1.push和pop原理

1.先简述下系统默认的push做了什么操作:
    当导航控制器push或者pop一个控制器的时候,导航控制器会检查自己的代理,看有没有被其它对象实现,如果没有,那么就使用系统默认的动画代理对象,如果有,那么就使用我们自定义的动画代理对象。
2.实现自定义动画代理条件
    2.1 成为导航控制器的代理,告诉导航控制器动画代理是哪一个
    2.2 实现动画代理方法,以便控制器执行跳转时调用

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

使用自定义的动画来呈现一个视图控制器,需要做以下你的视图控制器的动作方法:

  • 创建一个你想要呈现的视图控制器
  • 创建你的自定义的转场代理对象,并把它赋值给视图控制器的transitioningDelegate属性。你的转场代理的方法应该当调用时创建和返回你的自定义动画对象。
  • 调用presentViewController:animated:completion:方法来呈现这个视图控制器。

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

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 2.5} func animateTransition(transitionContext: UIViewControllerContextTransitioning) { let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) let containerView = transitionContext.containerView() let bounds = UIScreen.mainScreen().bounds toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height) containerView.addSubview(toViewController.view) UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: { fromViewController.view.alpha = 0.5 toViewController.view.frame = finalFrameForVC }, completion: { finished in transitionContext.completeTransition fromViewController.view.alpha = 1.0 })}

2.动画实现

请下载我的demo项目

下载完毕后,打开项目:01_转场动画初谈的KKListTableViewController
代码:实现条件2.1

// 设置导航控制器的代理为当前对象
self.navigationController.delegate = self;
  • 以下方法是实现导航代理UINavigationControllerDelegate的方法
// 导航控制器代理方法
 - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    // 这里返回当前控制器,所以要在当前控制器实现动画
    // 仔细看返回值类型,要求返回值必须实现(id<UIViewControllerAnimatedTransitioning>)
    // 也就是当前控制器要实现这个代理才有效
    return self;
}

实现转场代理

转场代理的作用是用来创建和返回你的自定义对象。表10-1显示了转场方法的实现可以如此简单。这个例子创建并返回了一个自定义动画对象。大多数实际工作都是由动画对象自己来处理。

//Listing 10-1Creating an animator object
- (id<UIViewControllerAnimatedTransitioning>)
    animationControllerForPresentedController:(UIViewController *)presented
                     presentingController:(UIViewController *)presenting
                         sourceController:(UIViewController *)source {
    MyAnimator* animator = [[MyAnimator alloc] init];
    return animator;
}

转场代理的其它方法可以像上面清单上的方法一样简单。你也可以结合自定义的逻辑,根据你的应用程序的当前状态返回不同的动画对象。有关转场代理的方法的更多信息,请看UIViewControllerTransitioningDelegate Protocol Reference.

第一个方法很简单,设定动画执行时间。第二个方法则用来编写我们自定义的动画代码,在这个方法中我们可以利用transitionContext来获得我们将来的来源控制器、目标控制器、动画完成后的最终frame,还可以获得用来管理来源或目标视图的容器视图。

代码:实现条件2.2

以下两个方法都是实现动画代理UIViewControllerAnimatedTransitioning的方法

// 返回动画执行时长
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

// 实现动画,目前这个方法push和pop都使用了淡入,后面会分出
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 1.从哪里来的控制器,这里是当前控制器
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    // 2.要去哪里的控制器,这是是UIViewController
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 2.1不透明咯
    toViewController.view.alpha = 0;

#warning 没事,就是建议你打断点看一下控制器类型

    // 3.这个view是包含视图,怎么说呢,几个控制器的view做的动画都是在这里面操作的,看我文章图
    UIView *containerView = [transitionContext containerView];
    // 3.1要显示必须添加到里面哦
    [containerView addSubview:toViewController.view];

    // 4.动画效果
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
        // 用了淡入
        toViewController.view.alpha = 1.0;
    } completion:^(BOOL finished) {
        // 移除从哪里来的控制器的view
        [fromViewController.view removeFromSuperview];
        // 必须一定肯定要调用,用来告诉上下文说已经结束动画了
        [transitionContext completeTransition:YES];
    }];
}

实现你的动画对象

一个动画对象是任何遵守了UIViewControllerAnimatedTransitioning协议的对象。动画对象创建在固定时间段内执行的动画。动画对象的关键是它的animateTransition:方法,你用来创建真实的动画。动画的过程大致分为以下几个部分:

  • 获取动画参数
  • 使用CoreAnimationUIView的动画方法创建动画
  • 清理并完成转场

然后我们将目标视图调整到屏幕下方并将其添加到容器视图内。接下来在动画执行的闭包内,将目标视图的位置变为最终位置,并将来源视图的透明度降为0.5,使其在目标视图进入的过程中产生一个淡出的效果。在动画完成的闭包内,我们告知transitionContext动画已完成,并将来源视图的透明度改回1.0。

3.讲解

从代码看到,我设置了导航控制器的代理对象为 KKListTableViewController控制器,然后实现导航代理方法,告诉当前的动画代理也是KKListTableViewController控制器,那么在push/pop的时候,就会调用KKListTableViewController控制器中动画方法,项目中用的是淡入,什么乱七八糟的,看图图片 8

获取动画参数

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

  • 调用两次viewControllerForKey:方法,以获取转场中涉及的”from”和”to”视图控制器。不要假设你知道哪些视图控制器参与了转场。UIKit可能会在适应新的特殊环境或响应你的应用程序请求时更换视图控制器。
  • 为了动画调用contrainerView方法来获取父视图。给这个视图添加关键的子视图。例如,在呈现期间,给这个视图添加被呈现的视图控制器的视图。
  • 调用viewForKey:方法来获取要添加或删除的视图。视图控制器的视图在转场过程中可能不是唯一的要被添加或被移除。presentation控制器可能插入视图到视图层级中,也必须被添加或移除。viewForKey:方法会返回包含所有你需要添加或移除的根视图。
  • 调用finalFrameForViewController:方法可以获取要添加或移除的视图的最终的frame矩形。

上下文转场对象使用”from”和”to”术语来标识视图控制器, 视图,和转场中涉及到的frame矩形。在转场开始的时候”from”视图控制器的视图总是在当前屏幕上,在转场结束的时候”to”视图控制器将会被看见。在图10-4中你可以看到,在presentationdismissal时“from”和”to"视图控制器交换了位置。

图片 9

图10-4

交换值使它更容易写一个单个的动画来处理presentationsdismissals。当你设计你的动画时,你总是必须要做的是包含一个属性来知道它的动画是presentation还是dismissal。两者之间唯一需要的区别如下:

  • 对于presentation, 添加”to”视图到容器视图的层级。
  • 对于dismissal,从容器视图的层级移除”from”视图。

接下来我们需要为目标控制器设置转场动画代理,这里我们指定来源控制器作为我们的代理。在ItemsTableViewController中,让其遵守UIViewControllerTransitioningDelegate协议,在storyboard中找到我们modal的segue,设置它的Identifier为showAction。然后在ItemsTableViewController中添加如下代码:

4.动画代理的 transitionContext

@protocol UIViewControllerContextTransitioning <NSObject>
// 展示的view,控制器的view需作为它的子view才能显示
- (UIView *)containerView;

// 结束转场动画,告诉上下文是否完成了,一般放在结束动画后调用
- (void)completeTransition:(BOOL)didComplete;

// 传入key获取控制器:UITransitionContextToViewControllerKey
// UITransitionContextFromViewControllerKey
- (UIViewController *)viewControllerForKey:(NSString *)key;

// 传入控制器获取对应的view最后的frame
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
创建转场动画

在典型的presentation期间,属于被呈现的视图控制器的视图被动画化到位。其它的视图可能作为你的presentation的一部分动画,但是你的动画的主要目标总是被添加到视图层级中的视图。
当动画主要视图时,你配置你的动画的基本动作是相同的。你从转场上下文获取你需要的对象和数据,并使用这些信息来创建你的真实的动画。

  • Presentation动画:
    • 使用viewControllerForKey:viewForKey:方法来获取涉及到转场中的视图控制器和视图。
    • 设置”to”视图的开始位置。将任何其它属性设置为其初始值。
    • 从转场上下文的finalFrameForViewController:获取”to”视图的最终位置。
    • 把”to"视图作为子视图添加到容器视图中。
    • 创建动画
      • 在你的动画块中,将”to”视图动画到它在容器视图中的最终位置。将任何其它属性设置为其最终值。
      • 在完成块中,调用completeTransition:方法,并执行其它的清理。
  • Dismissal动画:
    • 使用viewControllerForKey:viewForKey:方法来获取涉及到转场中的视图控制器和视图。
    • 计算”from”视图的结束位置。属于被呈现的视图控制器的视图开始消失。
    • 将”to"视图作为子视图添加到容器视图中。
    • presentation期间,当转场完成时属于呈现的视图控制器的视图被移除。结果,你必须在dismissal操作期间将那个视图添加回容器中。
    • 创建动画
      • 在你的动画块中,将”from”视图动画到它在容器视图中的最终位置。将任何其它属性设置为其最终值。
      • 在完成块中,将”from”视图从你的视图层级中移除并调用completeTransition:方法。根据需要执行任何其它的清理。

图10-5显示了一个自定义的presentationdismissal转场,动画它的对角视图。在presentation期间,被呈现的视图空屏幕开始并向对角线向左上方动画直到它是可见的。在dismissal期间,视图反转方向,向右下方动画直到它再次离开屏幕。

图片 10

图10-5

列表10-2显示你将如何实现图10-5上显示的转场。在获取到动画需要的对象后,animateTransition:方法计算被影响的视图的frame矩形。在presentation期间,被呈现的视图由“toView”表示。在dismissal期间,被消失的视图由”fromView”表示。presenting属性是动画对象自己的自定义属性,在创建动画对象的时候将转场代理设置为合适的值。

//Listing 10-2Animations for implementing a diagonal presentation and dismissal    
- (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];
                 }];
 }
let customPresentAnimationController = CustomPresentAnimationController() override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "showAction" { let toViewController = segue.destinationViewController as UIViewController toViewController.transitioningDelegate = self }}func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return customPresentAnimationController}

好了,那么push和pop的转场动画就暂时讲到这里,接下来我会讲modal和dismiss的实现,后面还会讲到封装,希望你不要错过哦!
动画结束后清理

在转场动画结束后,请务必调用completeTransition:方法。调用这个方法告诉UIKit转场完成了,用户可以开始使用被呈现的视图控制器。调用这个方法也会引发其它完成处理程序的级联,包括一个来自于presentViewController:animated:completion:方法和动画对象自己的animationEnded:方法。
调用completeTransition:方法最好的地方是在你的动画块的完成处理方法里。
因为转场能够被取消,你应该使用上下文对象的transitionWasCancelled方法的返回值来确定需要清理。当一个presentation被取消时,你的动画必须撤销对视图层级所做的任何修改。一个成功的dismissal需要相同的操作。

我们创建了一个动画管理器对象,设置目标控制器的转场代理为来源控制器,然后实现代理协议中的animationControllerForPresentedController方法,该方法用于指定modal过程中展示视图的动画,在该方法中返回我们自定义的动画管理器对象。

将交互式添加到你的转场

最容易的方法使你的动画交互是使用UIPercentDrivenInteractiveTransition对象。UIPercentDrivenInteractiveTransition对象与你现有的动画对象配合使用来控制其动画时间。它使用你提供的完成百分比值。你必须要做的是编写需要的事件处理的代码来计算完成百分比值并新的事件达到时更新它。
你可以使用带有子类或不带有子类的UIPercentDrivenInteractiveTransition类。如果你子类化了,使用你的子类的init方法(或startInteractiveTransition:方法)来执行一次创建你的事件处理代码。之后,使用你的自定义的事件处理代码来计算新的完成百分比值并调用updateInteractiveTransition:方法。当你的代码确定转场应该完成了,调用finishInteractiveTransition方法。
列表10-3显示UIPrecentDrivenInteractiveTransition子类的startInteractiveTransition:方法的自定义实现。这个方法设置一个平移手势识别器来跟踪触摸事件并将该手势识别器安装在动画的容器视图上。它还保存转场上下文的引用以供以后使用。

Listing 10-3Configuring a percent-driven interactive animator
- (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];
}

在任何新的事件达到时手势识别器会调用它的动作方法。你的动作方法的实现可以使用手势识别器的状态信息来确定手势是否成功,失败或一直在进行。同时,你可以使用最新的触摸事件信息来计算手势的新的百分比值。
列表10-4显示由列表10-3中配置的平移手势识别器调用的方法。当新的事件到达时,此方法使用垂直行程距离来计算动画的完成百分比值。当手势结束时,该方法结束转场。

// Listing 10-4Using events to update the animation progress
-(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或转场动画之上执行附加动画。例如,被呈现的视图控制器可能在转场期间动画它自己的视图层级,并在转场出现时添加移动效果或其它视觉反馈。任何对象能够创建动画,只要它能够访问被呈现或呈现的视图控制器的transitionCoordinator属性。转场协调器属性只在转场正在进行时存在。
为了创建动画,调用转场协调器的animateAlongsideTransition:completion:animateAlongsideTransitionInView:animation:completion:方法。这个你提供的块被存储直到转场动画开始,此时它们与其余的动画一起执行。

图片 11

使用Presentation控制器与你的动画

对于自定义presentation,你可以提供你自己的presentation控制器来给被呈现的视图控制器一个自定义的外观。Presentation控制器管理着视图控制器和其内容分离的任何自定义的chrome.例如,位于视图控制器视图的后面的调光视图将由presentation控制器管理。事实上它没有管理一个特殊的视图控制器的视图意味着你可以在你的应用程序中使用相同的presentation控制器与任何视图控制器。

你提供一个自定义的presentation控制器来自于被呈现视图控制器的转场代理。(视图控制器的modalTransitionStyle属性必须设置为UIModalPresentationCustom) presentation控制器与任何动画对象并行操作。由于动画对象将视图控制器的视图动画到位置,presentation控制器会将任何附加视图动画到位置。转场结束时,presentation控制器有机会对视图层级执行任何最终调整。
关于如何创建一个自定义的presentation控制器的更多信息,请看Creating Custom Presentations

跟系统默认modal效果差不多,不过带有弹簧效果。如果你希望有不同的效果,你可以对下面这句代码进行修改。

toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)

比如将其改为如下代码:

toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)

再次运行程序,我们的modal动画就变为从上往下了。

图片 12

我们的程序现在点击Dismiss退出目标控制器时,仍然是系统默认的动画,接下来实现这个自定义动画。

步骤同前面基本一样,创建一个叫做CustomDismissAnimationController的动画管理器,实现如下代理方法:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 2} func animateTransition(transitionContext: UIViewControllerContextTransitioning) { let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) let containerView = transitionContext.containerView() toViewController.view.frame = finalFrameForVC toViewController.view.alpha = 0.5 containerView.addSubview(toViewController.view) containerView.sendSubviewToBack(toViewController.view) UIView.animateWithDuration(transitionDuration(transitionContext), animations: { fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2) toViewController.view.alpha = 1.0 }, completion: { finished in transitionContext.completeTransition}

这次我们使用一个新的动画方式,让来源视图从中心点开始逐渐变小直到消失。首先我们将目标控制器设置为最终位置,透明度为0.5,并将其添加到容器视图的底层中使其开始时不可见。在动画执行过程中,来源视图逐渐变小,露出底层的目标视图,并将目标视图透明度过渡到1.0。

接下来在ItemsTableViewController中添加如下代码:

let customDismissAnimationController = CustomDismissAnimationController()func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return customDismissAnimationController}

animationControllerForDismissedController这个代理方法指定了modal过程中退出视图的动画。运行程序,你会发现我们的动画有点小Bug。

图片 13

我们可以看到,白色的背景视图确实如我们所愿从中心点逐渐缩小,但是图片视图的大小却保持不变,这是因为改变来源视图的时候,它的子控件的大小并不会跟着发生改变,我们可以通过视图快照的技术来解决这一问题。

将animateTransition方法的实现修改为如下代码:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) { let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) let containerView = transitionContext.containerView() toViewController.view.frame = finalFrameForVC toViewController.view.alpha = 0.5 containerView.addSubview(toViewController.view) containerView.sendSubviewToBack(toViewController.view) let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates snapshotView.frame = fromViewController.view.frame containerView.addSubview(snapshotView) fromViewController.view.removeFromSuperview() UIView.animateWithDuration(transitionDuration(transitionContext), animations: { snapshotView.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2) toViewController.view.alpha = 1.0 }, completion: { finished in snapshotView.removeFromSuperview() transitionContext.completeTransition }

我们给来源视图生成了一个快照,将它添加到容器视图中利用它来做动画,并将来源视图从父控件中移除。再次运行程序,我们的动画效果就正常了。

图片 14

在UITabBarController和UINavigationController的管理下,你无需为每个目标控制器都设置转场代理,可以直接设置UITabBarControllerDelegate或UINavigationControllerDelegate即可。

接下来我们演示如何为导航控制器设置自定义转场动画。首先,仍然是创建一个动画管理器类叫做CustomNavigationAnimationController,然后实现UIViewControllerAnimatedTransitioning协议的方法。这里的动画代码采用的是一个开源的三维旋转动画,读者可以到这里自行研究。

var reverse: Bool = false func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 1.5} func animateTransition(transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView() let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! let toView = toViewController.view let fromView = fromViewController.view let direction: CGFloat = reverse ? -1 : 1 let const: CGFloat = -0.005 toView.layer.anchorPoint = CGPointMake(direction == 1 ? 0 : 1, 0.5) fromView.layer.anchorPoint = CGPointMake(direction == 1 ? 1 : 0, 0.5) var viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat, 0.0, 1.0, 0.0) var viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat, 0.0, 1.0, 0.0) viewFromTransform.m34 = const viewToTransform.m34 = const containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0) toView.layer.transform = viewToTransform containerView.addSubview UIView.animateWithDuration(transitionDuration(transitionContext), animations: { containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0) fromView.layer.transform = viewFromTransform toView.layer.transform = CATransform3DIdentity }, completion: { finished in containerView.transform = CGAffineTransformIdentity fromView.layer.transform = CATransform3DIdentity toView.layer.transform = CATransform3DIdentity fromView.layer.anchorPoint = CGPointMake toView.layer.anchorPoint = CGPointMake if (transitionContext.transitionWasCancelled { toView.removeFromSuperview() } else { fromView.removeFromSuperview() } transitionContext.completeTransition(!transitionContext.transitionWasCancelled }

注意这里我们添加了一个reverse变量,用来指定转场动画的方向,这样我们可以将导航控制器push和pop过程的动画封装在一个动画管理器中。

在ItemsTableViewController中更改它的声明使其遵守UINavigationControllerDelegate协议,在viewDidLoad方法中设置代理为自己navigationController?.delegate = self,然后添加如下代码:

let customNavigationAnimationController = CustomNavigationAnimationController()func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { customNavigationAnimationController.reverse = operation == .Pop return customNavigationAnimationController}

上面这个导航控制器的代理方法用于指定push或pop时的转场动画,其中operation参数可以用来判断转场的方向。运行程序,如下图所示:

图片 15

我们知道苹果官方为导航控制器添加了一个默认的手势交互,就是在屏幕左侧向右滑动可以返回上一界面并带有pop动画,接下来我们为我们的自定义动画添加手势交互。

手势交互的管理器需要遵守的是UIViewControllerInteractiveTransitioning协议,该协议需要实现startInteractiveTransition方法指定开始交互,不过苹果官方为我们提供了另一个已经实现该协议的交互管理器类UIPercentDrivenInteractiveTransition,并提供以百分比的形式来控制交互过程的功能,比如控制交互的更新、取消、完成等,我们直接使用它来实现我们的交互控制。

创建一个类叫做CustomInteractionController并继承自UIPercentDrivenInteractiveTransition,添加如下代码:

var navigationController: UINavigationController!var shouldCompleteTransition = falsevar transitionInProgress = falsevar completionSeed: CGFloat { return 1 - percentComplete} func attachToViewController(viewController: UIViewController) { navigationController = viewController.navigationController setupGestureRecognizer(viewController.view)} private func setupGestureRecognizer(view: UIView) { view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePanGesture:"))} func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) { let viewTranslation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!) switch gestureRecognizer.state { case .Began: transitionInProgress = true navigationController.popViewControllerAnimated case .Changed: var const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 200.0), 0.0), 1.0)) shouldCompleteTransition = const > 0.5 updateInteractiveTransition case .Cancelled, .Ended: transitionInProgress = false if !shouldCompleteTransition || gestureRecognizer.state == .Cancelled { cancelInteractiveTransition() } else { finishInteractiveTransition() } default: println("Swift switch must be exhaustive, thus the default") }}

attachToViewController方法用于将来传入导航控制器的目标控制器,我们为目标控制器的整个view添加了滑动手势以便将来可以实现滑动返回的pop动画,在监听手势滑动的方法中,我们根据手势的状态做如下处理:

  • 开始滑动:设置transitionInProgress为true,并开始执行导航控制器的pop返回。
  • 滑动过程中:更新交互过程的百分比,我们假设指定滑动200点即为交互完成。
  • 取消或结束:设置transitionInProgress为false,如果交互过程执行50%以上则认为交互完成。

接来下来到我们的ItemsTableViewController,添加如下代码:

let customInteractionController = CustomInteractionController()

然后修改我们之前实现的导航控制器的代理方法如下:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == .Push { customInteractionController.attachToViewController } customNavigationAnimationController.reverse = operation == .Pop return customNavigationAnimationController}

当我们push一个目标控制器时,就为该目标控制器设定交互控制。最后实现导航控制器代理中的另一个方法用于指定交互控制器,代码如下:

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return customInteractionController.transitionInProgress ? customInteractionController : nil}

运行程序,如下图所示:

图片 16

完整的示例程序链接地址请点击这里。

推荐阅读:

  • objc中国:自定义 ViewController 容器转场
  • 喵神的好文:WWDC 2013 Session笔记 - iOS7中的ViewController切换

Demo实现效果如下图所示,下载完整的Demo代码请点击这里。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里。)

图片 17

实现过程同我们前面讲的自定义转场动画过程一样,首先创建一个动画管理器类MenuTransitionManager,然后设置目标控制器的转场代理,这次我们使用动画管理器对象作为代理,所以MenuTransitionManager既遵守了UIViewControllerAnimatedTransitioning协议,也遵守了UIViewControllerTransitioningDelegate协议。动画的执行代码比较简单,只是通过改变transform控制来源和目标视图的上下移动,目标视图我们仍然使用了快照技术。

我们还为来源视图的快照添加了一个点击的手势,这样在显示下拉菜单后,除了点击相应的菜单选项,点击下部的快照也可以返回到主页视图。只不过点击手势的处理我们使用了代理设计模式,而点击手势的添加我们使用了Swift的属性观察器语法,读者可以自行研究学习。

最后,希望大家学的愉快!

本文由永利皇宫463登录发布于编程,转载请注明出处:控制器转场动画自定义,AppCoda翻译系列

关键词:

上一篇:SWIFT世界中的OBJECTIVE

下一篇:没有了