iOS开发 - 自定义转场

iPhone.jpg

之前看过王巍写的ViewController切换来实现自定义转场,之后又看了唐巧的一篇控制器转场详解的文章,篇幅很长,讲的特别详细。转场有不少小细节,在这自己整理下,巩固下知识。

为了表示方便,转场前的控制器用FromVC表示,视图用FromView表示;转场后的控制器用ToVC表示,视图用ToView表示。

转场的基本步骤:

1.实现转场代理
// 1.UIViewController 的 transitioningDelegate  遵循的协议。
UIViewControllerTransitioningDelegate 

//2. UINavigationController 的 delegate 遵循的协议
UINavigationControllerDelegate。

//3. UITabBarController 的 delegate 遵循的协议。
UITabBarControllerDelegate
2.转场的动画控制器
//转场动画遵循的协议
UIViewControllerAnimatedTransitioning

该协议必须要实现的两个方法:

  • -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;转场动画时间
  • -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;转场动画的具体实现(各种炫酷的动画都是在这里实现的)
3.转场上下文环境
//转场上下文环境遵循的协议
UIViewControllerContextTransitioning
  • 不需要开发者自己实现,该协议提供了转场前后控制器的一写信息。
  • 其中几个重要的方法:
    • -(UIView *)containerView; 动画发生的view容器。
    • -(UIViewController *) viewControllerForKey:(NSString *)key; 通过key值,获取转场前后的控制器。
      • key:
        1 . UITransitionContextFromViewControllerKey 转场前
        2 . UITransitionContextToViewControllerKey 转场后
    • -(CGRect)initialFrameForViewController:(UIViewController *)vc; 获取控制器初始frame。
    • -(CGRect)finalFrameForViewController:(UIViewController *)vc;获取控制器结束时的frame。
    • -(void)completeTransition:(BOOL)didComplete; 转场结束时必须要调用的方法。
4.手势驱动视图

系统提供了一个实现UIViewControllerInteractiveTransitioning协议的UIPercentDrivenInteractiveTransition类,所以我们只要继承这个类,添加手势并在手势实现的方法中告知当前视图的百分比,通过此逻辑来驱动视图,在调用类中定义的一些方法就很容易实现视图的交互。
其中几个重要的方法:

  • -(void)updateInteractiveTransition:(CGFloat)percentComplete;更新视图的百分比。
  • -(void)cancelInteractiveTransition;取消视图的交互,返回交互前的状态。
  • -(void)finishInteractiveTransition;完成视图交互,更新到交互后的状态。

通过UINavigationController 实现转场

通过UICollectionView简单的实现神奇移动的效果。

移动.gif

设置好代理,并调用push方法

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    PushedViewController *pushVC=[[PushedViewController alloc]init];
    self.indexPath = indexPath;
    self.navigationController.delegate = pushVC;
    [self.navigationController pushViewController:pushVC animated:YES];
}

实现UINavigationControllerDelegate代理方法,此处返回实现UIViewControllerAnimatedTransitioning协议的动画控制器对象,若返回nil,则调用系统默认方式。

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC
{
//    return nil;
    return [PushPopTransfromAnimation transformWithType:(operation == UINavigationControllerOperationPush ? PushPopTransformPush :PushPopTransformPop)];
}

实现UIViewControllerAnimatedTransitioning协议的类:

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

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    if (_type == PushPopTransformPush) {
        [self pushAnimation:transitionContext];
    }
    else if (_type == PushPopTransformPop)
    {
        [self popAnimation:transitionContext];
    }
}

#pragma mark -
-(void)pushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
   //1.获取控制器
    CollectionViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    PushedViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //获取FromVC中点击的cell
    MyCollectionViewCell *cell = (MyCollectionViewCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.indexPath];
    
    UIView *containerView = [transitionContext containerView];
    
    UIView *tempView = [cell.myImgView snapshotViewAfterScreenUpdates:NO];
    tempView.frame  =[cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
    
    toVC.view.alpha = 0;

    //2.添加ToView
    [containerView addSubview:toVC.view];
    [containerView addSubview:tempView];
    toVC.imgView.hidden = YES;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:0 animations:^{
        
        tempView.frame  = [toVC.imgView convertRect:toVC.imgView.bounds toView:containerView] ;
        toVC.view.alpha = 1;
    } completion:^(BOOL finished) {
        toVC.imgView.hidden = NO;
        [tempView removeFromSuperview];
        //3.转场完成
        [transitionContext completeTransition:YES];
    }];
}

-(void)popAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
    //1.
    PushedViewController *fromVC = (PushedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    CollectionViewController *toVC = (CollectionViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    //2.
    UIView *containerView = [transitionContext containerView];
    
    MyCollectionViewCell *cell = (MyCollectionViewCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.indexPath];
    CGRect cellInitRect = [cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
    
    UIView *temp = fromVC.imgView;
    
    [containerView addSubview:toVC.view];
    [containerView sendSubviewToBack:toVC.view];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
        
        temp.frame = cellInitRect;
        fromVC.view.alpha = 0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES]];
    }];

注意:1. 在转场结束时,FromView会从视图结构中主动移除 。2.push和pop动画时都需要将ToView添加到ContainerView视图中。


Model转场

Model转场与UINavigationController 和 UITabBarController转场有些不同,需要注意。Model转场属性modalPresentationStyle可设置不同的模式,模式设置的不同,结果也不同,如:

  • UIModalPresentationFullScreen模式下:presentation后,FromView会被主动从视图结构中移除,dismissal时,ToView可以自己手动添加到containerView中,也可以不用手动添加,系统会自己添加。

  • UIModalPresentationCustom模式下:presentation后,FromView不会被主动移除,这与FullScreen模式是不同的,dismissal时,切记也不用添加ToView视图到containerView中,否则dismiss方法之后,presenting(将要显示)的视图不见了。

添加手势驱动
手势驱动.gif

Model方式实现转场并添加手势,在实现UIViewControllerTransitioningDelegate协议 和 UIViewControllerAnimatedTransitioning协议基础上还要实现一个继承UIPercentDrivenInteractiveTransition类的子类。

  • UIViewControllerAnimatedTransitioning协议接口中有- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator方法,这个方法中就是用来返回UIPercentDrivenInteractiveTransition类的对象的。
  • 该方法中还要判断是否正在手势驱动,手动驱动返回UIPercentDrivenInteractiveTransition类的对象,没有手动驱动则返回nil,如果没有判断返回nil,则返回时没有任何响应。

示例代码:

    PresentToViewController *presentToVC=[[PresentToViewController alloc]init];
    presentToVC.transitioningDelegate=self; //转场代理
    presentToVC.delegate=self;  //自定义代理,用于dismiss
    [self.swipeVC handleDismissViewController:presentToVC];//用于手势处理
    //presentToVC.modalPresentationStyle = UIModalPresentationCustom;
    [self presentViewController:presentToVC animated:YES completion:nil];

实现UIViewControllerTransitioningDelegate协议方法:

-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return self.presentAnimation;
}

-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return self.dismissAnimation;
}
//返回手势对象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
{
    return self.swipeVC.interacting ? self.swipeVC : nil;
}

实现UIViewControllerAnimatedTransitioning的类:

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

-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    if (_type == YJPPresentAnimationPresent) {
        [self presentVC:transitionContext];
    }
    else if (_type == YJPPresentAnimationDismiss)
    {
        [self dismissVC:transitionContext];
    }
}

-(void)presentVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
    PresentToViewController *toVC=(PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *containerView = [transitionContext containerView];
    toVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
    toVC.view.center = containerView.center;  
    [containerView addSubview:toVC.view];
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        
        toVC.view.frame = CGRectMake(0, 0, containerView.frame.size.width, containerView.frame.size.height);
        toVC.view.center = containerView.center;
    
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];
}
-(void)dismissVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
    PresentToViewController *fromVC = (PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    PresentFromViewController *toVC = (PresentFromViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    //添加视图
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    [containerView sendSubviewToBack:toVC.view];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        
        fromVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
        fromVC.view.center = containerView.center;
        
    } completion:^(BOOL finished) {
        //因添加了手势驱动,这里要判断是否取消
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

集成UIPercentDrivenInteractiveTransition类,实现子类:


- (void)handleDismissViewController:(PresentToViewController *)controller
{
    self.dissmissVC = controller;
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAction:)];
    [controller.view addGestureRecognizer:pan];
}

-(CGFloat )completionSpeed
{
    return 1 - self.persentCompleted;
}

-(void)panAction:(UIPanGestureRecognizer *)gesture
{
    CGPoint point = [gesture translationInView:self.dissmissVC.view];
    
    switch (gesture.state) {
        case UIGestureRecognizerStateBegan:
        {
            self.interacting = YES;
            
            [self.dissmissVC dismissViewControllerAnimated:YES completion:nil];
            break;
        }
           
        case UIGestureRecognizerStateChanged:
        {
            CGFloat persent = (point.y/500) <=1 ?(point.y/500):1;//百分比的程度
            self.persentCompleted = persent;
            [self updateInteractiveTransition:persent];
            break;
        }
         
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateEnded:
        {
            self.interacting = NO;

            if (gesture.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
            }else{
                [self finishInteractiveTransition];
            }
            break;
        }
            
        default:
            break;
    }
}


扩散效果
扩散.gif

通过自定义了一个UINavigationController实现扩散效果的转场。转场的方法基本就那几个方法,大同小异,不同的是动画的实现方式。扩散本质其实就是设置了视图的mask属性,并添加了动画,通过UIBezierPath、CAShapeLayer、CABasicAnimation就可以实现。
转场动画的核心代码:

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

    if (_type == MaskTransitionPush) {
        [self pushAnimaiton:transitionContext];
    }
    else if (_type == MaskTransitionPop)
    {
        [self popAnimaiton:transitionContext];
    }
}

-(void)pushAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
    //用于动画结束时使用
    self.transitionContext = transitionContext;
   
    MaskedViewController *toVC  = (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
   
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
   
    //计算圆的半径
    UIButton *button = toVC.button;
    CGFloat x =toVC.view.frame.size.width - button.center.x;
    CGFloat y =toVC.view.frame.size.height - button.center.y;
    CGFloat radius =sqrt(x*x+y*y);

    UIBezierPath  *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
    UIBezierPath  *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];

    //
    CAShapeLayer *shaperLayer = [CAShapeLayer layer];
    shaperLayer.path = finalPath.CGPath;
    toVC.view.layer.mask = shaperLayer;
    
    //添加动画
    CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    baseAnimation.fromValue = (id)initPath.CGPath;
    baseAnimation.toValue = (id)finalPath.CGPath;
    baseAnimation.duration = [self transitionDuration:transitionContext];
    baseAnimation.delegate =self; //设置代理
    [shaperLayer addAnimation:baseAnimation forKey:nil];  
}


-(void)popAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
    self.transitionContext = transitionContext;

    MaskedViewController *fromVC= (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    SystemViewController *toVC  = (SystemViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    //添加视图
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    [containerView sendSubviewToBack:toVC.view];
    
    //计算半径
    UIButton *button = fromVC.button;
    CGFloat x =fromVC.view.frame.size.width - button.center.x;
    CGFloat y =fromVC.view.frame.size.height - button.center.y;
    CGFloat radius =sqrt(x*x+y*y);
    
    UIBezierPath  *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
    UIBezierPath  *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
   
    //
    CAShapeLayer *shaperLayer = [CAShapeLayer layer];
    shaperLayer.path = initPath.CGPath;
    fromVC.view.layer.mask = shaperLayer;
   
    //
    CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    baseAnimation.fromValue = (id)finalPath.CGPath;
    baseAnimation.toValue =(id)initPath.CGPath;
    baseAnimation.duration = [self transitionDuration:transitionContext];
    baseAnimation.delegate =self;
    [shaperLayer addAnimation:baseAnimation forKey:nil];
}

//CABasicAnimation动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    [self.transitionContext completeTransition:YES];
    if (_type == MaskTransitionPush) {
        [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view.layer.mask = nil;
    }
    else if (_type == MaskTransitionPop)
    {
        [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
    }
}

自定义转场Demo


参考文章:
https://onevcat.com/2013/10/vc-transition-in-ios7/
http://blog.devtang.com/2016/03/13/iOS-transition-guide/
http://www.jianshu.com/p/45434f73019e

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容