在话题 5# 中,Chris Eidhof 向我们介绍了 iOS7 引入的新特性自定义 View Controller 转场。他给出了一个结论:
我们在本文只探讨了在 navigation controller 中的两个 view controller 之间的转场动画,但是这些做法在 tab bar controller 或者任何你自己定义的 view controller 容器中也是通用的…
尽管从技术角度来讲,使用 iOS 7 的 API,你可以对自定义容器中的 view controllers 做自定义转场,但是这不是能直接使用的,实现这种效果非常不容易。
请注意我正在讨论的自定义视图控制器容器 (custom container view controllers) 都是 UIViewController
的直接子类,而不是 UITabBarController
或者 UINavigationController
的子类。
对于你自定义的继承于 UIViewController
的容器子类,并没有现成可用的 API 允许一个任意的动画控制器 (animation controller) 将一个子视图控制器自动转场到另外一个,不管是可交互式的转场还是不可交互式的转场。 我甚至都觉着苹果根本就不想支持这种方式。苹果支持下面的这几种转场方式:
- Navigation controller 推入和推出页面
- Tab bar controller 选择的改变
- Modal 页面的展示和消失
在本文中,我将向你展示如何自定义视图控制器容器,并且使其支持第三方的动画控制器。
如果你需要复习一下 iOS 5 引入的视图控制器容器,请阅读话题#1 中 Ricky Gregersen 写的文章 “View Controller 容器”。
预热准备
看到这里,你可能对上文我们说到的一些问题犯嘀咕,让我来告诉你答案吧:
为什么我们不直接继承 UINavigationController
或 UITabBarController
,并且使用它们提供的功能的?
有些时候这是你不想要的。可能你想要一个非常特殊的外观或者行为,和这些类能够提供给你的差别非常大,因此你必须使用一些黑客式的手段去达到你想要 的结果,同时还要担心系统框架的版本更新后这些黑客式的手段是否还仍然有效。或者,你就是想完全控制你的视图控制器容器,避免不得不支持一些特定的功能。
好吧, 那么为什么不使用 transitionFromViewController:toViewController:duration:options:animations:completion:
去实现呢?
这又是一个好问题,你可能想用这种方式去实现,但是或许你对代码的整洁性比较在意,想把这种转场相关的代码封装在内部。那么为什么不使用一个既存的、被良好验证的设计模式呢?这种设计模式可以非常方便的支持第三方的转场动画。
介绍相关的API
在我们开始写代码之前,让我们先花一分钟的时间来简单看一下我们需要的组件吧。
iOS 7 自定义视图控制器转场的 API 基本上都是以协议的方式提供的,这也使其可以非常灵活的使用,因为你可以很简单地将它们插入到你的类中。最主要的五个组件如下:
- 动画控制器 (Animation Controllers) 遵从
UIViewControllerAnimatedTransitioning
协议,并且负责实际执行动画。 - 交互控制器 (Interaction Controllers) 通过遵从
UIViewControllerInteractiveTransitioning
协议来控制可交互式的转场。 - 转场代理 (Transitioning Delegates) 根据不同的转场类型方便的提供需要的动画控制器和交互控制器。
- 转场上下文 (Transitioning Contexts) 定义了转场时需要的元数据,比如在转场过程中所参与的视图控制器和视图的相关属性。 转场上下文对象遵从
UIViewControllerContextTransitioning
协议,并且这是由系统负责生成和提供的。 - 转场协调器(Transition Coordinators) 可以在运行转场动画时,并行的运行其他动画。 转场协调器遵从
UIViewControllerTransitionCoordinator
协议。
正如你从其他的阅读材料中得知的那样,转场有不可交互式和可交互式两种方式。在本文中,我们将集中精力于不可交互的转场。这种转场是最简单的转场,也是我们学习的一个好的开始。这意味着我们需要处理上面提到的动画控制器 (animation controllers),转场代理 (transitioning delegates) 和转场上下文 (transitioning contexts)。
闲话少说,让我们开始动手吧…
示例工程
通过三个阶段,我们将要实现一个简单自定义的视图控制器容器,它可以对子视图控制器提供自定义的转场动画的支持。
你可以在这里找到这三个阶段的 Xcode 工程的源代码。
阶段 1: 基础
我们应用中的核心类是 ContainerViewController
,它持有一个UIViewController
实例的数组,每个实例是一个普通的 ChildViewController
。容器视图控制器设置了一个带有可点击图标,并代表每个子视图控制器的私有的子视图:
我们通过点击图标在不同的子视图控制器之间切换。在这一阶段,子视图控制器之间切换时是没有转场动画的。
你可以在这里查看阶段-1的源代码。
阶段 2: 转场动画
当我们添加转场动画时,我们想要使用一个遵从 UIViewControllerAnimatedTransitioning
协议的动画控制器(animation controllers)。这个协议声明了 3 个方法,前面的 2 个方法是必须实现的:
1 2 3 |
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext; - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext; - (void)animationEnded:(BOOL)transitionCompleted; |
通过这些方法,我们可以获得我们所需的所有东西。当我们的视图控制器容器准备执行动画时,我们可以从动画控制器中获取动画的持续时间,并让其去执行真正的动画。当动画执行完毕后,如果动画控制器实现了可选的 animationEnded:
方法,我们可以调用动画控制器中的 animationEnded:
方法。
但是,首先我们必须把一件事情搞清楚。正如你在上面的方法签名中看到的那样,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从 UIViewControllerContextTransitioning
协议的对象。通常情况下,当我们使用系统内建的类时,系统框架为我们创建了转场上下文对象,并把它传递给动画控制器。但是在我们这种情况下,我们需要自定义转场动画,所以我们需要承担系统框架的责任,自己去创建这个转场上下文对象。
这就是大量使用协议的方便之处。我们可以不用必须复写一个私有类,而复写私有类这种方法是明显不可行的。我们可以定义自己的类,并使其遵从文档中相应的协议就可以了。
尽管在 UIViewControllerContextTransitioning
协议中声明了很多方法,而且它们都是必须要实现 (required) 的,但是我们现在可以暂时忽略它们中的一些方法,因为我们现在仅仅支持不可交互式的转场。
同 UIKit 类似,我们定义了一个私有类 NSObject <UIViewControllerContextTransitioning>
。在我们的特定例子中,这个私有类是 PrivateTransitionContext
,它的初始化方法如下实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight { NSAssert ([fromViewController isViewLoaded] && fromViewController.view.superview, @"The fromViewController view must reside in the container view upon initializing the transition context."); if ((self = [super init])) { self.presentationStyle = UIModalPresentationCustom; self.containerView = fromViewController.view.superview; self.viewControllers = @{ UITransitionContextFromViewControllerKey:fromViewController, UITransitionContextToViewControllerKey:toViewController, }; CGFloat travelDistance = (goingRight ? -self.containerView.bounds.size.width : self.containerView.bounds.size.width); self.disappearingFromRect = self.appearingToRect = self.containerView.bounds; self.disappearingToRect = CGRectOffset (self.containerView.bounds, travelDistance, 0); self.appearingFromRect = CGRectOffset (self.containerView.bounds, -travelDistance, 0); } return self; } |
我们把视图的出现和消失时的状态记录了下来,比如初始状态和最终状态的 frame。
请注意一点,我们的初始化方法需要我们提供我们是在向右切换还是向左切换。在我们的 ContainerViewController
中,按钮是一个接一个水平排列的,转场上下文通过设置每个的 frame 来记录它们之间的位置关系。动画控制器或者说 animator,在生成动画时可以使用这些 frame。
我们也可以通过另外的方式去获取这些信息,但是那样的话,就会使 animator 和 ContainerViewController
及其视图控制器耦合在一起了,这是不好的,我们并不想这样。animator 应该只关心它自己以及传递给它的上下文,因为这样,在理想情况下,animator 可以在不同的上下文中得到复用。
在下一步实现我们自己的动画控制器时,我们应该时刻记住这一点,现在让我们来实现转场上下文吧。
你可能记得我们在 issue #5 中的View Controller 转场已经做过相同的事情了,为什么我们不使用它呢?事实上,由于使用了非常灵活的协议,我们可以直接把那个工程中的动画控制器,也就是 Animator
类直接拿过来使用,不需要任何修改。
使用 Animator
类的实例来做转场动画的核心代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[fromViewController willMoveToParentViewController:nil]; [self addChildViewController:toViewController]; Animator *animator = [[Animator alloc] init]; NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController]; NSUInteger toIndex = [self.viewControllers indexOfObject id="crayon-5812e4280df7c525588096-7">NSUInteger toIndex = [self.viewControllers indexOfObjecte="color: #888888;">我们在本文只探讨了在 navigation controller 中的两个 view controller 之间的转场动画,但是这些做法在 tab bar controller 或者任何你自己定义的 view controller 容器中也是通用的…
尽管从技术角度来讲,使用 iOS 7 的 API,你可以对自定义容器中的 view controllers 做自定义转场,但是这不是能直接使用的,实现这种效果非常不容易。 请注意我正在讨论的自定义视图控制器容器 (custom container view controllers) 都是 对于你自定义的继承于
在本文中,我将向你展示如何自定义视图控制器容器,并且使其支持第三方的动画控制器。 如果你需要复习一下 iOS 5 引入的视图控制器容器,请阅读话题#1 中 Ricky Gregersen 写的文章 “View Controller 容器”。 预热准备看到这里,你可能对上文我们说到的一些问题犯嘀咕,让我来告诉你答案吧: 为什么我们不直接继承 有些时候这是你不想要的。可能你想要一个非常特殊的外观或者行为,和这些类能够提供给你的差别非常大,因此你必须使用一些黑客式的手段去达到你想要 的结果,同时还要担心系统框架的版本更新后这些黑客式的手段是否还仍然有效。或者,你就是想完全控制你的视图控制器容器,避免不得不支持一些特定的功能。 好吧, 那么为什么不使用 这又是一个好问题,你可能想用这种方式去实现,但是或许你对代码的整洁性比较在意,想把这种转场相关的代码封装在内部。那么为什么不使用一个既存的、被良好验证的设计模式呢?这种设计模式可以非常方便的支持第三方的转场动画。 介绍相关的API在我们开始写代码之前,让我们先花一分钟的时间来简单看一下我们需要的组件吧。 iOS 7 自定义视图控制器转场的 API 基本上都是以协议的方式提供的,这也使其可以非常灵活的使用,因为你可以很简单地将它们插入到你的类中。最主要的五个组件如下:
正如你从其他的阅读材料中得知的那样,转场有不可交互式和可交互式两种方式。在本文中,我们将集中精力于不可交互的转场。这种转场是最简单的转场,也是我们学习的一个好的开始。这意味着我们需要处理上面提到的动画控制器 (animation controllers),转场代理 (transitioning delegates) 和转场上下文 (transitioning contexts)。 闲话少说,让我们开始动手吧… 示例工程通过三个阶段,我们将要实现一个简单自定义的视图控制器容器,它可以对子视图控制器提供自定义的转场动画的支持。 你可以在这里找到这三个阶段的 Xcode 工程的源代码。 阶段 1: 基础我们应用中的核心类是 我们通过点击图标在不同的子视图控制器之间切换。在这一阶段,子视图控制器之间切换时是没有转场动画的。 你可以在这里查看阶段-1的源代码。 阶段 2: 转场动画当我们添加转场动画时,我们想要使用一个遵从
通过这些方法,我们可以获得我们所需的所有东西。当我们的视图控制器容器准备执行动画时,我们可以从动画控制器中获取动画的持续时间,并让其去执行真正的动画。当动画执行完毕后,如果动画控制器实现了可选的 但是,首先我们必须把一件事情搞清楚。正如你在上面的方法签名中看到的那样,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从 这就是大量使用协议的方便之处。我们可以不用必须复写一个私有类,而复写私有类这种方法是明显不可行的。我们可以定义自己的类,并使其遵从文档中相应的协议就可以了。 尽管在 同 UIKit 类似,我们定义了一个私有类
我们把视图的出现和消失时的状态记录了下来,比如初始状态和最终状态的 frame。 请注意一点,我们的初始化方法需要我们提供我们是在向右切换还是向左切换。在我们的 我们也可以通过另外的方式去获取这些信息,但是那样的话,就会使 animator 和 在下一步实现我们自己的动画控制器时,我们应该时刻记住这一点,现在让我们来实现转场上下文吧。 你可能记得我们在 issue #5 中的View Controller 转场已经做过相同的事情了,为什么我们不使用它呢?事实上,由于使用了非常灵活的协议,我们可以直接把那个工程中的动画控制器,也就是 使用
|