自定义转场详解(一)

前言

本文是我学习了onevcat的这篇转场入门做的一点笔记。

今天我们来实现一个简单的自定义转场,我们先来看看这篇文章将要实现的一个效果图吧:

过程详解

热身准备

我们先创建一个工程,首先用storyboard快速的创建两个控制器,一个作为主控制器,叫ViewController,另外一个作为present出来的控制器,叫PresentViewController,并且用autoLayout快速搭建好界面。就像这样:

我们先做好点击ViewController上面的按钮,present出 PresentViewController,点击PresentViewController上面的按钮,dismiss掉PresentViewController的逻辑。这里有两个注意点:

  1. 因为此处我使用了segue,所以在ViewController按钮点击的时候,我们只需要这样调用就行。 #pragma mark - 点我弹出 -(IBAction)presentBtnClick:(UIButton *)sender { [self performSegueWithIdentifier:@"PresentSegue" sender:nil]; }
  2. 我们平时写dismiss的时候,一般都会是在第二个控制器中直接给self发送dismissViewController的相关方法。在现在的SDK中,如果当前的VC是被显示的话,这个消息会被直接转发到显示它的VC去。但是这并不是一个好的实现,违反了程序设计的哲学,也很容易掉到坑里。所以我们用标准的delegate 方式实现 dismiss

首先我们在PresentViewController控制器中申明一个代理方法。

    #import <UIKit/UIKit.h>
    @class PresentViewController;
    @protocol PresentViewControllerDelegate <NSObject>
    - (void)dismissViewController:(PresentViewController *)viewController;
    @end
    @interface PresentViewController : UIViewController
    @property (nonatomic, weak) id<PresentViewControllerDelegate> delegate;
    @end

在button的点击事件中,让代理去完成关闭当前控制器的工作。

    #pragma mark - 点击关闭
    - (IBAction)closeBtnClick:(UIButton *)sender {
        if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) {
            [self.delegate dismissViewController:self];
        }
    }

与此同时,在ViewController中需要设置PresentViewController的代理,并且实现代理方法:

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        if ([segue.identifier isEqualToString:@"PresentSegue"]) {
            PresentViewController *presetVC = segue.destinationViewController;
            presetVC.delegate = self;
        }
    }
    #pragma mark - PresentViewControllerDelegate
    - (void)dismissViewController:(PresentViewController *)viewController {
        [self dismissViewControllerAnimated:YES completion:nil];
    }

OK,到这里,我们一个基本的转场就完成了(这也是系统自带的一个效果)。like this:

主要内容

接下来,要接触我们今天要讲的主要内容了,我们用iOS7中一个新的类UIViewControllerTransitioning来实现自定义转场。


UIViewControllerAnimatedTransitioning

首先我们需要一个实现了协议名为UIViewControllerAnimatedTransitioning的对象。创建一个类叫做PresentAnimation继承于NSObject并且实现了UIViewControllerAnimatedTransitioning协议。(注意:需要导入UIKit框架)

    @interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>

这个协议负责转场的具体内容。开发者在做自定义切换效果时大部门代码会是用来实现这个协议的,这个协议只有两个方法必须要实现的:

    // 返回动画的时间
    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
    // 在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

实现这两个方法

    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.8f;
    }
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
        // 1.我们需要得到参与切换的两个ViewController的信息,使用context的方法拿到它们的参照;
        UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];   
        // 2.对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置到屏幕下边缘;
        CGRect finaRect = [transitionContext finalFrameForViewController:toVC];
        toVC.view.frame = CGRectOffset(finaRect, 0, [UIScreen mainScreen].bounds.size.height);
        // 3.将view添加到containerView中;
        [[transitionContext containerView] addSubview:toVC.view];
        // 4.开始动画。这里的动画时间长度和切换时间长度一致。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线;
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
            toVC.view.frame = finaRect;
        } completion:^(BOOL finished) {
            // 5.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。
            [transitionContext completeTransition:YES];
        }];
    }

注意点

UITransitionContextToViewControllerKeyUITransitionContextFromViewControllerKey 比如从A present 出B,此时A是FromViewController,B是ToViewController 如果从B dismiss 到A,此时A是ToViewController,B是FromViewController

UIViewControllerTransitioningDelegate

这个接口的作用比较单一,在需要VC切换的时候系统会向实现了这个接口的对象询问是否需要使用自定义转场效果。 所以,一个比较好的地方是直接在主控制器ViewController中实现这个协议。

ViewController中完成如下代码:

    @interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate>
    @property (nonatomic, strong) PresentAnimation *presentAnimation;
    @end
    @implementation ViewController
    #pragma mark - 懒加载
    - (PresentAnimation *)presentAnimation {
        if (!_presentAnimation) {
            _presentAnimation = [[PresentAnimation alloc] init];
        }
        return _presentAnimation;
    }
    #pragma mark - UIViewControllerTransitioningDelegate
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return self.presentAnimation;
    }
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        if ([segue.identifier isEqualToString:@"PresentSegue"]) {
            PresentViewController *presetVC = segue.destinationViewController;
            presetVC.delegate = self;
            presetVC.transitioningDelegate = self;
        }
    }

现在看下我们的效果:

相对于上面系统自带的效果来说,我们在present出第二个控制器的时候,带有弹簧效果。

手势驱动百分比切换

现在我们增加一个功能,就是用手势滑动来dismiss,通俗的说,就是让present出来的那个控制器使用手势dismiss。

  1. 创建一个类,继承自UIPercentDrivenInteractiveTransition #import <UIKit/UIKit.h> @interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController; @end
    • 我们写一个方法提供给外部类调用。让外部类可以看到传入手势dismiss的VC的入口。
  2. 既然传入了这个需要手势dismiss的VC,我们就需要保存一下,方便当前类在其他地方使用,所以我们新建一个属性来保存这个传入的VC。 #import "PanInteractiveTransition.h" @interface PanInteractiveTransition () @property (nonatomic, strong) UIViewController *presentVC; @end @implementation PanInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController { self.presentVC = viewController; UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)]; [self.presentVC.view addGestureRecognizer:panGestR]; } #pragma mark - panGestureAction -(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:self.presentVC.view]; NSLog(@"%.2f",transition.y); switch (pan.state) { case UIGestureRecognizerStateBegan:{ [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = MIN(1.0, transition.y/300); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ if (pan.state == UIGestureRecognizerStateCancelled) { // 手势取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
  3. 和创建PresentAnimation一样,我们创建一个一个DismissAnimation类 @interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning> @end @implementation DismissAnimation -(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.4f; } -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } @end
  4. 最后,我们在主控制器中添加一个手势驱动的对象,一个dismiss转场的对象,然后懒加载。 -(PanInteractiveTransition *)paninterTransition { if (!_paninterTransition) { _paninterTransition = [[PanInteractiveTransition alloc] init]; } return _paninterTransition; } -(DismissAnimation *)dismissAnimation { if (!_dismissAnimation) { _dismissAnimation = [[DismissAnimation alloc] init]; } return _dismissAnimation; } #pragma mark - UIViewControllerTransitioningDelegate -(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissAnimation; } -(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator { return self.paninterTransition; } -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"PresentSegue"]) { // ... [self.paninterTransition panToDismiss:presetVC]; } }

完善

此时,我们运行程序,会发现以上代码尽管可以手势驱动了,但是点击按钮dismiss的功能无法使用了。这是因为如果只是返回self.paninterTransition,那么点击按钮dismiss的动画就会失效;如果只是返回nil,那么手势滑动的效果将会失效。综上所述,我们就得分情况考虑。 接下来我们就来完善一下。

  1. PanInteractiveTransition添加一个属性,表示是否处于切换过程中(用于判断使用的是点击按钮dismiss还是手势驱动来dismiss的) // 是否处于切换过程中 @property (nonatomic, assign, getter=isInteracting) BOOL interacting;
  2. PanInteractiveTransition添加一个属性,表示是否需要dismiss(用于当手势滑动到超过指定高度之后,就会dismiss,如果没有超过,就会还原) @property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
  3. 修改PanInteractiveTransition中的panGestureAction:方法: -(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:pan.view]; switch (pan.state) { case UIGestureRecognizerStateBegan:{ self.interacting = YES; [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0); self.shouldComplete = (percent > 0.5); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ self.interacting = NO; // 如果下移的距离小于300或者取消都当做取消 if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) { // 手势取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
  4. 另外还有一点,就是需要修改DismissAnimation中的一处代码: -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { // 此处做了修改,由之前的[transitionContext completeTransition:YES]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }

ok,到此为止,我们的一个自定义转场动画就算了完成了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Charlie's Road

Container ViewController自定义转场控制器。

最近接触到新公司的老项目改版。自从来了之后一直在忙另一个项目,也没有看老项目的实现逻辑。 看到设计稿的时候,并不是普通的树形标签导航的样子。大致效果如FaceU...

1251
来自专栏一“技”之长

iOS开发中的手势体系——UIGestureRecognizer分析及其子类的使用

        在iOS系统中,手势是进行用户交互的重要方式,通过UIGestureRecognizer类,我们可以轻松的创建出各种手势应用于app中。关于UI...

792
来自专栏Python疯子

3D立体相册,一个可旋转的立体相册

当然里面的很多元素都是可以改动的,根据自己需要自己改动即可 这里用的是Button,你也可改为其他的,数量也可改动

2891
来自专栏Guangdong Qi

小Q项目框架搭建及会动的Tabbar未完待续,持续更新中

1734
来自专栏移动开发

一个简单的ReactNative demo

本人非前端,请轻喷 ReactNative版本:0.31 github:https://github.com/X-FAN/reactnativelear...

3923
来自专栏進无尽的文章

实践-小细节Ⅵ

有时候,UITableView 的cell个数很少,可是UITableView的headView又是一个有颜色背景的View,当我们下拉的时候,拉扯出来的区域也...

742
来自专栏QQ音乐技术团队的专栏

一个循环动画引起的内存泄露问题总结

本文主要记录项目中遇到的一个内存泄露问题:由于一个循环动画引起的内存泄露,并且这个问题也是偶现的,在后面的隐藏问题里会说明。

4182
来自专栏岑志军的专栏

ReactNative-综合案例(02)

1717
来自专栏青玉伏案

iOS开发之自定义表情键盘(组件封装与自动布局)

  下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有MVC, iOS开发中的自动布局,自定义组件的封装与使用,Block回调,Cor...

25310
来自专栏跟着阿笨一起玩NET

总结DevExpress10个使用技巧

6202

扫码关注云+社区

领取腾讯云代金券