TransitionAnimation自定义转场动画

栏目: IOS · 发布时间: 5年前

内容简介:在由图中可以看出要完成自定义转场动画,必须遵从先来看看网易严选App的转场效果,可以看出当前页面想要

iOS 7 之后,苹果就开放了自定义转场的相关 api ,现在都快 iOS 12 了,一直都没有好好研究转场动画,一个是之前没有重视,觉得花里胡哨的,另外一个是所做的项目中没有这样的转场动画需求。这里说的转场动画和上一篇CAAnimation 系统动画中 CATransition 动画不是一个概念,上一篇指的是单个View的转场特效,这里指的是整个控制器的转场特效。其实写上篇文章的目前也是为今天打下铺垫,复杂的转场效果也是由单个动画来组成的。

TransitionAnimation自定义转场动画

由图中可以看出要完成自定义转场动画,必须遵从 UIViewControllerAnimatedTransitioning 协议,协议中有两个必须实现的方法一个是返回转场时间,一个是具体转场的实现。文章会结合5个最常用的动画场景来说明转场动画。

先来看看网易严选App的转场效果,可以看出当前页面想要 Push 其他的页面的时候,当前页面会下沉同时其他页面从右边平移至左边。 Present 页面的时候,当前页面也会下沉,目标视图从底部弹出。

TransitionAnimation自定义转场动画
TransitionAnimation自定义转场动画

来看代码,在 ViewController 里面有两个按钮,分别是 PushSecondVCPresentThirdVC

- (IBAction)pushBtnClick:(id)sender
{
    SecondViewController * vc = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}


- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    [self presentViewController:vc animated:YES completion:nil];
}

复制代码

Push和Pop动画

UIViewControllerAnimatedTransitioning协议

这里新建一个 AnimatedTransitioningObject 类,然后要遵循 UIViewControllerAnimatedTransitioning 协议。这个为了方便,把 Push、Pop、Present、Dismiss 这四个效果写在一起,用枚举来区分,当然也可以把每种动画效果单独用一个 AnimatedTransitioningObject 类来实现。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger,TransitionAnimationObjectType) {
    TransitionAnimationObjectType_Push,
    TransitionAnimationObjectType_Pop,
    TransitionAnimationObjectType_present,
    TransitionAnimationObjectType_Dismiss
};

@interface TransitionAnimationObject : NSObject <UIViewControllerAnimatedTransitioning>

@property (nonatomic,assign) TransitionAnimationObjectType type;

- (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;

+ (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;

@end
复制代码

来看看两个必须实现的方法,在返回转场时间里也可以根据 type 来返回不同的动画时间,这里统一返回0.5秒。 pushAnimateTransition 里面实现 Push 效果转场。

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

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    switch (_type) {
        case TransitionAnimationObjectType_Push:
            [self pushAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_Pop:
            [self popAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_present:
            [self presentAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_Dismiss:
            [self dismissAnimateTransition:transitionContext];
            break;

        default:
            break;
    }
}
复制代码

Push实现

- (void)pushAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //获取目标View(secondVC.view) 和 来源View(ViewController.view)
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //这里截图做动画 隐藏来源View
    UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
    fromView.hidden = YES;

    //将需要做转场的View按照顺序添加到转场容器中
    UIView * containerView = [transitionContext containerView];
    [containerView addSubview:tempView];
    [containerView addSubview:toView];

    CGFloat width = containerView.frame.size.width;
    CGFloat height = containerView.frame.size.height;

    //设置目标View的初始位置
    toView.frame = CGRectMake(width, 0, width, height);

    //开始做动画
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
        toView.transform = CGAffineTransformMakeTranslation(-width, 0);
    } completion:^(BOOL finished) {
        //这里要标记转场成功 假如不标记 系统会认为还在转场中 无法交互
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];

        //转场失败 也要做相应的处理
        if ([transitionContext transitionWasCancelled])
        {
            fromView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];

}
复制代码

Pop实现

PushPop 是相对的关系,所以在 Pop 动画中,目标视图和来源视图互换身份,实现也是用 CGAffineTransformIdentity 来还原 Push 动画即可。

- (void)popAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //注意这里是还原 所以toView和fromView 身份互换了 toView是ViewController.view
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //获取相应的视图
    UIView * containerView = [transitionContext containerView];
    UIView * tempView = [[containerView subviews] firstObject];

    //在fromView 下面插入toView 不然回来的时候回黑屏
    [containerView insertSubview:toView belowSubview:fromView];

    //将动画直接还原即可
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        tempView.transform = CGAffineTransformIdentity;
        fromView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        //标记转场
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];

        //转场成功的处理
        if (![transitionContext transitionWasCancelled])
        {
            [tempView removeFromSuperview];
            toView.hidden = NO;
        }
    }];
}

复制代码

UINavigationControllerDelegate代理方法

完成 AnimatedTransitioningObject 类后,再返回 ViewController 中, ViewController 要遵循 UINavigationBarDelegateUIViewControllerTransitioningDelegate ,把 SecondVCtransitioningDelegate 设置为自己。然后根据不同的 operation ,来返回不同的动画实现。

@interface ViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate>

- (IBAction)pushBtnClick:(id)sender
{
    SecondViewController * vc = [[SecondViewController alloc] init];
    vc.transitioningDelegate = self;
    [self.navigationController pushViewController:vc animated:YES];
}

#pragma mark - Push && Pop
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
    if (operation == UINavigationControllerOperationPush)
    {
        return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Push];
    }
    else if (operation == UINavigationControllerOperationPop)
    {
        return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Pop];
    }
    return nil;
}
复制代码

看看实现效果

TransitionAnimation自定义转场动画

Present动画和Dismiss动画

Present实现

- (void)presentAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //获取目标View(ThirdVC.view) 和 来源View(ViewController.view)
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //截图做动画
    UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
    tempView.frame = fromView.frame;
    fromView.hidden = YES;

    //按照顺序假如转场动画容器中
    UIView * containerView = [transitionContext containerView];
    [containerView addSubview:tempView];
    [containerView addSubview:toView];

    CGFloat width = containerView.frame.size.width;
    CGFloat height = containerView.frame.size.height;

    //设置toView的初始化位置 在屏幕底部
    toView.frame = CGRectMake(0, height, width, 400);

    //做转场动画
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
        tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
        toView.transform = CGAffineTransformMakeTranslation(0, -400);
    } completion:^(BOOL finished) {
        //转场结束后一定要标记 否则会认为还在转场 无法交互
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if ([transitionContext transitionWasCancelled])
        {
            //转场失败
            fromView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];
}
复制代码

Dismiss实现

- (void)dismissAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //dismiss的时候 fromVC和toVC身份倒过来了
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //containerView里面的顺序也倒过来了 截图在最上面
    UIView * containerView = [transitionContext containerView];
    UIView * tempView = [[containerView subviews] firstObject];

    //做还原动画就可以了
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
        tempView.transform = CGAffineTransformIdentity;
        fromView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        //转场结束后一定要标记 否则会认为还在转场 无法交互
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if (![transitionContext transitionWasCancelled])
        {
            //转场成功
            toView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];

}
复制代码

UIViewControllerTransitioningDelegate代理方法

回到 ViewController ,把 ThirdVCtransitioningDelegate 设置为自己,然后在代理方法中自定类型。

- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    vc.transitioningDelegate = self;
    [self presentViewController:vc animated:YES completion:nil];
}

#pragma mark - Present && Dismiss
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_present];
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Dismiss];
}
复制代码

手势动画

UIPercentDrivenInteractiveTransition创建手势类

新建一个手势类 GestureObject 继承自 UIPercentDrivenInteractiveTransitionaddGestureToViewController 是给目标控制器添加手势。

#import <UIKit/UIKit.h>

@interface GestureObject : UIPercentDrivenInteractiveTransition

//判断是交互的手势
@property (nonatomic,assign) BOOL interacting;

- (void)addGestureToViewController:(UIViewController *)viewController;

@end
复制代码

然后再手势的状态之间来判断是否执行动画,这里是判断手势偏移量超过屏幕一半的高度就生效,执行相关动画,否则还原动画。

- (void)handleGesture:(UIPanGestureRecognizer *)ges
{
    CGPoint point = [ges translationInView:ges.view];

    switch (ges.state) {
        case UIGestureRecognizerStateBegan:
        {
            self.interacting = YES;
            [self.targetVC dismissViewControllerAnimated:YES completion:nil];
            break;
        }

        case UIGestureRecognizerStateChanged:
        {
            CGFloat fraction = point.y / ges.view.frame.size.height;
            //限制在0和1之间
            fraction = MAX(0.0, MIN(fraction, 1.0));
            self.shouldComplete = fraction > 0.5;
            [self updateInteractiveTransition:fraction];
            break;
        }
        
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        {
            self.interacting = NO;
            if (!self.shouldComplete || ges.state == UIGestureRecognizerStateCancelled)
            {
                //还原动画
                [self cancelInteractiveTransition];
            }
            else
            {
                //完成动画
                [self finishInteractiveTransition];
            }
            break;
        }

        default:
            break;
    }
}
复制代码

UIViewControllerTransitioningDelegate代理方法

回到 ViewController 中,在 PresentThirdVC 的时候添加手势,在代理方法 interactionControllerForDismissal 中指定手势。

- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    vc.transitioningDelegate = self;
    [self.gestureObject addGestureToViewController:vc];
    [self presentViewController:vc animated:YES completion:nil];
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
    return self.gestureObject.interacting ? self.gestureObject : nil;
}
复制代码

看看效果

TransitionAnimation自定义转场动画

小结

PushPopPresentDismiss 、手势动画都讲解完了,可以看出,自定义转场大致的步骤是

  • 根据 viewForKey 来获取转场上下文
  • 将要转场的视图加入转场容器中
  • 做出转场动画
  • 标记转场成功的状态,根据状态做相应的处理

理解了这些,再复杂的转场动画都能一步步分解出来,下面是格瓦拉App的转场效果,第一次看的时候,觉得很酷炫,现在了解了转场的核心后,觉得不那么难了,有时间再把它的效果写出来吧。

TransitionAnimation自定义转场动画

源码:TransitionAnimation


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法技术手册(原书第2版)

算法技术手册(原书第2版)

George T. Heineman、Gary Pollice、Stanley Selkow / 杨晨、曹如进 / 机械工业出版社 / 2017-8-1 / 89.00元

本书使用实际代码而非伪代码来描述算法,并以经验主导支撑数学分析,侧重于应用且规范严谨。本书提供了用多种程序设计语言实现的文档化的实际代码解决方案,还介绍了近40种核心算法,其中包括用于计算点集的Voronoi图的Fortune算法、归并排序、多线程快速排序、AVL平衡二叉树实现以及空间算法。一起来看看 《算法技术手册(原书第2版)》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具