自定义 ViewController 容器转场

本文转载至 http://blog.csdn.net/yongyinmg/article/details/40621463

话题 #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 基本上都是以协议的方式提供的,这也使其可以非常灵活的使用,因为你可以很简单地将它们插入到你的类中。最主要的五个组件如下:

  1. 动画控制器 (Animation Controllers) 遵从 UIViewControllerAnimatedTransitioning 协议,并且负责实际执行动画。
  2. 交互控制器 (Interaction Controllers) 通过遵从 UIViewControllerInteractiveTransitioning 协议来控制可交互式的转场。
  3. 转场代理 (Transitioning Delegates) 根据不同的转场类型方便的提供需要的动画控制器和交互控制器。
  4. 转场上下文 (Transitioning Contexts) 定义了转场时需要的元数据,比如在转场过程中所参与的视图控制器和视图的相关属性。 转场上下文对象遵从 UIViewControllerContextTransitioning 协议,并且这是由系统负责生成和提供的。
  5. 转场协调器(Transition Coordinators) 可以在运行转场动画时,并行的运行其他动画。 转场协调器遵从UIViewControllerTransitionCoordinator 协议。

正如你从其他的阅读材料中得知的那样,转场有不可交互式和可交互式两种方式。在本文中,我们将集中精力于不可交互的转场。这种转场是最简单的转场,也是我们学习的一个好的开始。这意味着我们需要处理上面提到的动画控制器 (animation controllers),转场代理 (transitioning delegates) 和转场上下文 (transitioning contexts)。

闲话少说,让我们开始动手吧…

示例工程

通过三个阶段,我们将要实现一个简单自定义的视图控制器容器,它可以对子视图控制器提供自定义的转场动画的支持。

你可以在这里找到这三个阶段的 Xcode 工程的源代码。

阶段 1: 基础

我们应用中的核心类是 ContainerViewController,它持有一个UIViewController实例的数组,每个实例是一个普通的ChildViewController。容器视图控制器设置了一个带有可点击图标,并代表每个子视图控制器的私有的子视图:

我们通过点击图标在不同的子视图控制器之间切换。在这一阶段,子视图控制器之间切换时是没有转场动画的。

你可以在这里查看阶段-1的源代码。

阶段 2: 转场动画

当我们添加转场动画时,我们想要使用一个遵从 UIViewControllerAnimatedTransitioning 协议的动画控制器(animation controllers)。这个协议声明了 3 个方法,前面的 2 个方法是必须实现的:

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animationEnded:(BOOL)transitionCompleted;

通过这些方法,我们可以获得我们所需的所有东西。当我们的视图控制器容器准备执行动画时,我们可以从动画控制器中获取动画的持续时间,并让其去执行真正的动画。当动画执行完毕后,如果动画控制器实现了可选的 animationEnded: 方法,我们可以调用动画控制器中的 animationEnded: 方法。

但是,首先我们必须把一件事情搞清楚。正如你在上面的方法签名中看到的那样,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从 UIViewControllerContextTransitioning 协议的对象。通常情况下,当我们使用系统内建的类时,系统框架为我们创建了转场上下文对象,并把它传递给动画控制器。但是在我们这种情况下,我们需要自定义转场动画,所以我们需要承担系统框架的责任,自己去创建这个转场上下文对象。

这就是大量使用协议的方便之处。我们可以不用必须复写一个私有类,而复写私有类这种方法是明显不可行的。我们可以定义自己的类,并使其遵从文档中相应的协议就可以了。

尽管在 UIViewControllerContextTransitioning 协议中声明了很多方法,而且它们都是必须要实现 (required) 的,但是我们现在可以暂时忽略它们中的一些方法,因为我们现在仅仅支持不可交互式的转场。

同 UIKit 类似,我们定义了一个私有类 NSObject <UIViewControllerContextTransitioning>。在我们的特定例子中,这个私有类是 PrivateTransitionContext,它的初始化方法如下实现:

- (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 类的实例来做转场动画的核心代码如下所示:

[fromViewController willMoveToParentViewController:nil];
[self addChildViewController:toViewController];

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

NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController];
NSUInteger toIndex = [self.viewControllers indexOfObject:toViewController];
PrivateTransitionContext *transitionContext = [[PrivateTransitionContext alloc] initWithFromViewController:fromViewController toViewController:toViewController goingRight:toIndex > fromIndex];

transitionContext.animated = YES;
transitionContext.interactive = NO;
transitionContext.completionBlock = ^(BOOL didComplete) {
    [fromViewController.view removeFromSuperview];
    [fromViewController removeFromParentViewController];
    [toViewController didMoveToParentViewController:self];
};

[animator animateTransition:transitionContext];

这其中的大部分是在对视图控制器容器的操作,计算出我们是在向左切换还是向右切换。做动画的部分基本上只有 3 行代码:1) 创建 animator,2) 创建转场上下文,和 3) 触发动画执行。

有了上面的代码,转场效果看起来如下图所示:

非常酷,我们甚至没有写一行动画相关的代码。

你可以在 阶段-2 标签下看到这部分代码的变化。在与 阶段-1 的对比这里你可以看到 阶段-2 和 阶段-1 相对比的完整的代码改变。

阶段 3: 封装

我想我们最后要做的一件事情是封装 ContainerViewController ,使其能够:

  1. 提供默认的转场动画。
  2. 提供替换默认动画控制器的代理。

这意味着我们需要把对 Animator 类的依赖移除,同时需要创建一个代理协议。

我们如下定义这个协议:

@protocol ContainerViewControllerDelegate <NSObject>
@optional
- (void)containerViewController:(ContainerViewController *)containerViewController didSelectViewController:(UIViewController *)viewController;
- (id <UIViewControllerAnimatedTransitioning>)containerViewController:(ContainerViewController *)containerViewController animationControllerForTransitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController;
@end

containerViewController:didSelectViewController: 方法使 ContainerViewController 可以很更容易的集成于功能齐全的应用中。

containerViewController:animationControllerForTransitionFromViewController:toViewController: 方法挺有趣的,当然,你可以把它和下面的 UIKit 中的视图控制器容器的代理协议做对比:

  • tabBarController:animationControllerForTransitionFromViewController:toViewController:(UITabBarControllerDelegate)
  • navigationController:animationControllerForOperation:fromViewController:toViewController:(UINavigationControllerDelegate)

所有的这些方法都返回一个 id<UIViewControllerAnimatedTransitioning> 对象。

与之前一直使用一个 Animator 对象不同, 我们现在可以从我们的代理那里获取一个动画控制器:

id<UIViewControllerAnimatedTransitioning>animator = nil;
if ([self.delegate respondsToSelector:@selector (containerViewController:animationControllerForTransitionFromViewController:toViewController:)]) {
    animator = [self.delegate containerViewController:self animationControllerForTransitionFromViewController:fromViewController toViewController:toViewController];
}
animator = (animator ?: [[PrivateAnimatedTransition alloc] init]);

如果我们有代理并且它返回了一个 animator,那么我们就使用这个 animator。否则,我们使用内部私有类PrivateAnimatedTransition 创建一个默认的 animator。接下来我们将实现 PrivateAnimatedTransition 类。

尽管默认的动画和 Animator 有一些不同,但是代码看起来惊人的相似。下面是完整的代码实现:

@implementation PrivateAnimatedTransition

static CGFloat const kChildViewPadding = 16;
static CGFloat const kDamping = 0.75f;
static CGFloat const kInitialSpringVelocity = 0.5f;

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 1;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    // When sliding the views horizontally, in and out, figure out whether we are going left or right.
    BOOL goingRight = ([transitionContext initialFrameForViewController:toViewController].origin.x < [transitionContext finalFrameForViewController:toViewController].origin.x);

    CGFloat travelDistance = [transitionContext containerView].bounds.size.width + kChildViewPadding;
    CGAffineTransform travel = CGAffineTransformMakeTranslation (goingRight ? travelDistance : -travelDistance, 0);

    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0;
    toViewController.view.transform = CGAffineTransformInvert (travel);

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:kDamping initialSpringVelocity:kInitialSpringVelocity options:0x00 animations:^{
        fromViewController.view.transform = travel;
        fromViewController.view.alpha = 0;
        toViewController.view.transform = CGAffineTransformIdentity;
        toViewController.view.alpha = 1;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

@end

需要注意的一点是,上面的代码没有通过设置视图的 frame 来反应它们之间的位置关系,但是代码仍然可以正常工作,只不过转场总是在同一个方向上。因此,这个类也可以被其他的代码库使用。

转场动画现在看起来如下所示:

在 阶段-3 的代码中,app delegate 中设置代理的部分被注释掉了,这样就可以看到默认的动画效果了。你可以将其设置回再使用Animator 类。你可能想查看同 阶段-2 相比所有的修改

我们现在有一个自包含的提供了默认转场动画的 ContainerViewController 类,这个默认的转场动画可以被开发者自己定义的iOS 7 自定义动画控制器 (UIViewControllerAnimatedTransitioning) 的对象代替,甚至都可以不用关心我们的源代码就可以方便的替换。

结论

在本文中我们通过使用 iOS 7 提供的自定义视图控制器转场的新特性,使我们自定义的视图控制器容器成为了 UIKit 的一等公民。

这意味着你可以把自定义的非交互式的转场动画应用到自定义的视图控制器容器中。你可以看到我们把 7 个话题之前使用的转场类直接拿过来使用,而且没有做任何修改。

译者注 即 issue #5 中的 View Controller 转场中的 Animator 类。

如果你想让自己的容器视图控制器作为一个类库或者框架,或者仅仅想使你的代码得到更好的复用,这将是非常完美的。

我们现在仅仅支持非交互式的转场,下一步就是对交互式的转场也提供支持。

我把它留给你当作一个练习。这有一些复杂,因为我们基本上是要模仿系统的行为,而这真的全是猜测性的工作。

时间: 2024-11-08 05:42:25

自定义 ViewController 容器转场的相关文章

容器转场

iOS7之前,ViewController切换主要有4种方式: 1.Push/Pop NavigationViewController 2.Present and dismis Modal 3.UITabBarController 4.addChildViewController iOS5添加函数: - (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIVie

Docker &nbsp; none模式、DNS/HOSTNAME自定义、容器互联(网络三)

玩转Docker必要要了解的网络基础知识: 机器需要一个网络接口来发送和接受数据包,路由表来定义如何到达哪些地址段.这里的网 络接口可以不是物理接口.事实上,每个 linux  机器上的 lo   环回接口( docker容器中也 有)就是一个完全的 linux 内核虚拟接口,它直接复制发送缓存中的数据包到接收缓存中. docker 让宿主主机和容器使用特殊的虚拟接口来通信 -- 通信的 2 端叫" peers",他们 在主机内核中连接在一起,所以能够相互通信.创建他们很简单,前面介绍

【转】WPF中实现自定义虚拟容器(实现VirtualizingPanel)

在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题.有下面一种常见的情况:原始数据源数据量很大,但是某一时刻数据容器中的可见元素个数是有限的,剩余大多数元素都处于不可见状态,如果一次性将所有的数据元素都渲染出来则会非常的消耗性能.因而可以考虑只渲染当前可视区域内的元素,当可视区域内的元素需要发生改变时,再渲染即将展现的元素,最后将不再需要展现的元素清除掉,这样可以大大提高性能.在WPF中System.Windows.Controls命名空间下的VirtualizingStackP

ViewController容器

在我的一个项目中,我需要实现一种容器式的 view controller.我感觉几乎是寸步难行,因为这种技术用的人是那么的少.因为很显然,开发者更喜欢重用和利用已有的view controller,而不是发明新的容器. 但是在某些情况下你更需要定制自己的容器.比起UINavigationController 和 UITabBarController,自己的容器更能简化你的代码.想起你什么时候以及什么情况下会使用这两个控制器吗? 我很容易就想到一个例子.当你想用 view controller 去

Focusky教程 | 自定义帧的转场时间

转场时间与转场效果也是动画演示当中值得重视的一个细节之处, 您可在Focusky动画演示大师(以下简称为"FS软件")中自定义帧的转场时间. 具体步骤如下: 点击帧下方的设置按键, 开始自定义帧的转场时间. 如图所示,可自行修改设置合适的转场时间,并且选择应用到当前帧或者应用到所有路径. 原文地址:https://www.cnblogs.com/focusky/p/10129753.html

NetCore3.0实现自定义IOC容器注入

原文:NetCore3.0实现自定义IOC容器注入 在之前的ASP.NET MVC实现依赖注入一文中,通过替换默认的ControllerFactory来达到对Controller生命周期的拦截,实现自定义的对象注入,在NetCore3.0中需要重新实现,步骤如下: 1.获取所有相关业务程序集 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static List<Assembly> GetFrameworkAssemblys()       {    

自定义presentViewController的转场动画(Swift)

原创Blog,转载请注明出处 我的StackFlow 前言: iOS默认的presentViewController的切换动画是从底部推入,消失是从顶部推出.但是,因为iOS系统默认的是适配所有转场上下文的.而针对特定的转场上下文,我们能做出更好的效果. Tips:所谓的转场上下文,就是转场的开始View和结束View,以及对应的ViewController 目标效果 最终的效果 准备工作 首先写出一个CollectionView,每个Cell是一个图片,由于本文的核心是如何转场,所以Colle

Android 自定义ViewGroup(自定义布局容器)

1.先创建一个控件类间接或者直接继承ViewGroup类 2.重载onMeasure方法来测量控件 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 3.重载onLayout方法来布局子空间 protected void onLayout(boolean changed, int l, int t, int r, int b) 4.重载返回ViewGroup.LayoutParams的方法 public V

自定义docker容器网络

1.通过bridge 驱动创建类似前面默认的 bridge 网络:docker network create --driver bridge my_net如果没有指定网段默认为172.18.0.0/16: 2.以自己制定网段只需在创建网段时指定 --subnet 和 --gateway 参数:docker network create --driver bridge --subnet 172.28.16.0/24 --gateway 172.28.16.1 my_net2 3.通过brctl s