从 iOS 的动画说到转场

动画与转场,我的认为在概念上并不复杂,只是在代码的组织和形式上比较复杂,所以我尝试先讲讲概念,再讲讲实现,让思绪清晰一些。html

什么是动画(Animation)?

所谓动画,就是在一段时间内,一些 view 的位置、颜色等属性会逐渐变化的一个现象。那么要完成一个动画,咱们只须要肯定三点:动画有多久、动画涉及到哪些 view 、这些 view 都有哪些属性改变了,说简单点儿就是时间、元素、变化形式。明确了这三点,各类 API 的变化只是在代码的简洁性和复用度上不停的作文章而已。app

那,什么是转场(Transition)?

咱们说到,动画的三个主要元素是时间、元素、变化形式,在元素这里动画并无作过多的约束,而从概念上讲,转场就是一个动画的子集,其约束动画的元素必须为两个元素,而且通常都是两个 view controller 的主 view 进行的转换(因此说转场是针对两个 vc 的动画也没啥大毛病)。ide

iOS 中动画怎么作?

了解了动画的关键概念,咱们来看看在 iOS 中,应该如何用代码去描述这三个概念。动画

第一种:使用UIView 的 begin/commit :ui

_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
    [UIView beginAnimations:nil context:nil]; 
    [UIView setAnimationDuration:1.0f];// 这里描述时间
    _demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 这里同时描述了元素和变化形式
    [UIView commitAnimations];

第二种:直接经过 block 调用spa

_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
   [UIView animateWithDuration:1.0f delay:1.0f // 这里是时间
   options:UIViewAnimationOptionCurveEaseIn // 这里是一些封装的变化形式
   animations:^{
            _demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 这里同时描述了元素和变化形式
        } completion:nil];

第三种:将对属性的变化封装到 CoreAnimation 对象中,而后应用到某个 view 的 layer 上code

CABasicAnimation *anima = [CABasicAnimation animationWithKeyPath:@"position"];
    anima.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, SCREEN_HEIGHT/2-75)];
    anima.toValue = [NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-75)];// 这里描述了变化形式
    anima.duration = 1.0f;// 这里描述了时间
    [_demoView.layer addAnimation:anima forKey:@"positionAnimation"];// 这里则是描述了元素

这三种方式中,第一种是好久之前(iOS 4.0)使用的形式,不管是便捷度和复用度都不是很高。第二种是最方便的,可是缺点在于很差复用(除非把 block 保存起来,能够在一个 vc 中实现复用)。第三种是一种很容易复用的形式,将动画的三个元素中时间、变化形式单独抽离出来,使得其能够自由的应用在任意的元素上。(由此能够看出,若是想要代码的复用度更高,就须要不断的减小一段代码或者一个对象在概念上的职责)htm

iOS 中转场怎么作?

前面咱们说过,转场是针对于两个特定的 view 的动画,因此咱们须要先约定一下术语,假如咱们有两个 VC A/B,咱们要从 A 转换到 B,咱们称呼 A 为 presentingViewController(或者 fromViewController),称呼 B 为 presentedViewController(或者 toViewController)。当从 B 结束转换回到 A 时,咱们仍然称呼 A 为 presentingViewController,B 为 presentedViewController,可是咱们会称呼 A 为 toViewController ,而 B 为 fromViewController。明白区别了么?from/to 是针对一次动画的,而 presented/presenting 是针对一次完整的转场的。对象

虽然从概念上来讲,转场是一种特定的动画,可是实际上转场须要考虑的事情要比通常的动画要多(好比通常的动画可能不须要交互,可是转场可能须要),所以在代码的组织结构上,转场使用了更多的对象去更加细致的拆分概念上的职责。blog

最基本的一种实现转场的方式,很是相似于上面所说的第二种动画的表现形式:

[self transitionFromViewController:self.fromVC
                      toViewController:self.toVC // 元素
                              duration:5 // 时间
                               options:UIViewAnimationOptionCurveEaseInOut // 变化形式的封装
                            animations:^{
        CGRect frame = self.thirdVC.view.frame;
        frame.origin.y  = 150;
        self.thirdVC.view.frame = frame;
    }
                            completion:nil];

这个转场通常在容器 VC 中使用。缺点实际上是和最基本的动画调用方式同样,都是不容易复用,而且使用场景有限,只能用在容器 vc 中,不能用在两个平级的 vc 中。也就是说,为了从 A 转到 B,咱们必须首先有一个 C ,而后让 A、B 做为 C 的 child vc ,显然很不方便啊,那么咱们就须要考虑一种新的代码组织形式,将转场的职责进行拆分。

转场的职责划分

在一次自定义的转场中,咱们会将指责进行以下形式的划分:

首先,咱们须要有两个 vc(废话(╬▔皿▔)),而后设置 presentingVC 的 modalPresentationStyleUIModalPresentationCustom,接下来将 presentingVC 的 transitioningDelegate 属性指向一个实现了 UIViewControllerTransitioningDelegate协议的对象上。这样就告诉 UIKit 任意一个 vc 用 prensentViewController:animated:completion 方法展现 presentingVC 时,presentingVC 的转场效果彻底由 transitioningDelegate 属性所指向的对象来负责。

// PresentingVC
self.transitioningDelegate = [TransitionDelegate new];// 转场效果这一部分职责从 vc 中剥离了出去

TransitionDelegate 是一个实现了 UIViewControllerTransitioningDelegate 协议的对象,在这个协议中又将转场效果的职责分为三个对象去负责:一个负责转场动画效果的 Animator,一个负责转场过程当中交互的 InteractiveAnimator,和一个则负责转场过程当中 view 的层级关系以及在不一样屏幕上的适配。这三个对象的职责,在代码上的表现形式就是将UIViewControllerTransitioningDelegate的内容分为三组。咱们来一个个了解一下。

TransitionAnimator

这个对象负责转场的动画效果,具体点儿来讲,他决定了可见的视图从 PresentingViewController 的 view 到可见视图变为 PresentedViewController 的 view 的过程当中,两个 view 应该如何去变化。在UIViewControllerTransitioningDelegate协议中,该对象能够经过两个方法返回:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed

两个方法中,前者决定了 present 过程当中的动画效果,后者则决定了 dismiss 过程当中的动画效果。而具体 Animator 如何去控制转场过程当中的动画,咱们就须要看看 UIViewControllerAnimatedTransitioning 这个协议中的方法都有些什么:

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

第一个方法决定了转场的时间,第二个方法则是经过一个 transitionContext 对象传递给 Animator 对象转场过程当中的 FromVC/ToVC,以及 containerView ,也就是转场过程当中的元素,而后咱们就能够经过 UIKit 的动画 API 决定转场的变化形式了。在这个方法中咱们要作的就是:

  1. 获得 ToVC 的 view,设定其初始状态

  2. 将 ToVC 的 view 添加到 containerView 中

  3. 经过任意一种动画形式对 ToVC 的 view 作动画,而后在结束的时候调用 transitionContext 对象的 completeTransition: 方法告知系统咱们的动画作完了。

更具体的内容,能够参见以下的一段代码:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    // 获取全部须要的 view 以及 vc
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = transitionContext.containerView;
    // 设定初始状态
    toVC.view.frame = CGRectMake(0, - CGRectGetHeight(fromVC.view.frame), CGRectGetWidth(fromVC.view.frame), CGRectGetHeight(fromVC.view.frame));
    toVC.view.alpha = 0.0f;
    
    // 必定要本身手动添加 subview, fromVC 的 view UIKit 会自动移除,可是 UIKit 不会自动添加 toVC 的 view
    [containerView addSubview:toVC.view];
    
    // 获取动画时间
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    
    // 开始动画
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        toVC.view.alpha = 1.0f;
        toVC.view.frame = fromVC.view.frame;
    } completion:^(BOOL finished) {
        if (finished) {
            [transitionContext completeTransition:YES];
            NSLog(@"finished");
        }
    }];
}

InteractiveAnimator

对于通常的转场来讲,实现了基本的动画效果可能就够了,可是实际开发中,咱们可能对于转场有更加深刻的需求,好比但愿转场可以带有用户交互,像系统的全局返回手势那样,这个时候,咱们就须要额外返回一个 InteractiveAnimator 来告诉 UIKit 随着用户的手势变化,动画应该执行到百分之多少或者是否须要取消,这些操做咱们均可以经过 context 对象中的方法来完成:

- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;

所以,若是想实现一个交互式的转场,咱们须要作以下几件事儿:

  1. 在 presentingVC 中添加一个 button 点击之外的『触发器』(通常来讲,都是一个 Gesture Recognizer),好比添加一个边缘滑动的 Gesture Recognizer,当一个边缘滑动开始时,咱们在对应的回调中 present PresentedVC。

  2. 在 presentedVC 的 transitionDelegate 中,返回一个 InteractiveAnimator。

  3. 在 Animator 中的 startInteractiveTransition: 方法中将 context 对象保存起来。

  4. 想办法将 Gesture Recognizer 传递给 InteractiveAnimator,使得在 Animator 中能够获取当前手势的信息,结合 context 对象中的 containerView 等信息,咱们能够知道当前手势在 view 中更具体的信息。

  5. 根据预先设定好的规则,在 Gesture Recognizer 的回调中调用 context 对象的 cancel/finished/update 方法

好比,若是咱们想实现一个边缘滑动的交互动画效果,咱们能够这么来写代码:

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 把 context 对象保存起来
    self.transitionContext = transitionContext;
    [super startInteractiveTransition:transitionContext];
}


// 根据手势的偏移来计算当前动画应该有的完成度
- (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture
{
    // 根据 container view 以及 gesture recognizer 计算偏移量
    UIView *transitionContainerView = self.transitionContext.containerView;
    CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
    
    // 根据偏移量得出百分比
    CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
    return (width - locationInSourceView.x) / width;
}


// gesture recognizer 的回调
- (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer
{
    switch (gestureRecognizer.state)
    {
        case UIGestureRecognizerStateBegan:
            break;
        case UIGestureRecognizerStateChanged:
            // 计算百分比,并返回
            [self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
            break;
        case UIGestureRecognizerStateEnded:
            // 根据预先设定的阈值决定是结束仍是取消,这里咱们设定 view 中间是分界线
            if ([self percentForGesture:gestureRecognizer] >= 0.5f)
                [self finishInteractiveTransition];
            else
                [self cancelInteractiveTransition];
            break;
        default:
            // 其余状况,取消转场
            [self cancelInteractiveTransition];
            break;
    }
}

PresentationController

以上的两组接口,分别让咱们自定义了转场过程当中的动画、动画执行百分比,可是不论是哪一个,都会在最后将 fromVC 的 view 从 containerView 上移除,而且整个转场过程当中若是咱们想添加一些额外的 view 也是没法作到的。若是想要实现这些功能,就须要咱们建立一个 UIPresentationController 的子类,而后重载其 四个转场的生命周期方法:

  • presentationTransitionWillBegin

  • presentationTransitionDidEnd:

  • dismissalTransitionWillBegin

  • dismissalTransitionDidEnd:

在重载这些方法时,咱们也可使用其 presentingViewController 属性的 transitionCoordinator 来同步的为咱们新添加的 view 执行动画(所谓同步就是和咱们以前在 Animator 中写的动画同时执行)。
好比,咱们能够为咱们添加的一个 dimming view 的透明度设置一个动画:

id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
        
        self.dimmingView.alpha = 0.f;
        [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
            self.dimmingView.alpha = 0.5f;
        } completion:NULL];

总结一下来讲,若是咱们想要使用 UIPresentationController ,咱们须要:

  1. 设置 presentedVC 的 presentStyle 为 UIModalPresentationCustom

  2. 在 presentedVC 的 transitionDelegate 中返回咱们建立的 UIPresentationController 的子类

  3. 在子类中重载转场生命周期的四个方法,添加咱们所须要的自定义的view

扩展阅读

相关文章
相关标签/搜索