欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS 自定义转场入门

程序员文章站 2024-03-22 21:52:04
...

前言

hi,勇敢的小伙伴儿们大家好,写ARKit的第二篇文章偶然了解了一个叫自定义转场的东西,嗯,我之前一直不知道,像我这种技术渣只会用基本的push和pop等,自定义转场这么高级的词汇我真的是第一次听说,既然学艺不精,就查漏补缺,赶紧了解,于是我搜索了相关博客,其中最通俗易懂的就是来自喵神的博客:ViewController切换 2013年的文章,年代有点久远,但不影响学习。

不正经的插一句话:我已经被喵神的匠心精神虏获,嘻嘻(逃。

我自己根据博客将大部分解释写成了注释,有兴趣可以到我的github:自定义转场入门Demo下载查看,有问题烦请指出,多谢!

正文是我根据喵神博客自己重新排版组织的,可能没有喵神写的详细,但是我自认为有一个比较系统的整理,大家可以看我的然后再看喵神的会对知识理解更加透彻。大部分解释在代码部分的注释,希望多多在意哦~

正文(2018.2.26补充)

整体上,我们将转场动画分为两个部分学习,一部分是动画切换,一部分是交互切换

首先我们学习如何自定义ViewController的动画切换。

自定义ViewController动画切换

知识储备

在学习之前,我们先了解几个相关的协议。

@protocol UIViewControllerContextTransitioning

这个接口是用来提供切换上下文给开发者使用,Context是开发者需要关注的区别词汇。

这个接口包含了FromVC和ToVC等各类信息,一般不需要开发者自己实现。

其中比较重要的方法有:

  • -(UIView *)containerView; VC切换所发生的view容器,开发者应该将切出的view移除,将切入的view加入到该view容器中。
  • -(UIViewController *)viewControllerForKey:(NSString *)key; 提供一个key,返回对应的VC。现在的SDK中key的选择只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey两种,分别表示将要切出和切入的VC。
  • -(CGRect)initialFrameForViewController:(UIViewController *)vc; 某个VC的初始位置,可以用来做动画的计算。
  • -(CGRect)finalFrameForViewController:(UIViewController *)vc; 与上面的方法对应,得到切换结束时某个VC应在的frame。
  • -(void)completeTransition:(BOOL)didComplete; 向这个context报告切换已经完成。

@protocol UIViewControllerAnimatedTransitioning

这个接口负责切换的具体内容,在这个地方我们可以自定义一个基于NSObject的类,遵守UIViewControllerAnimatedTransitioning协议,实现它的代理方法,设定动画切换的时间,以及动画内容。

其中重要的方法有:

  • -(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext; 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间(一般就返回动画的时间就好了,SDK会用这个时间来在百分比驱动的切换中进行帧的计算,后面再详细展开)。

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

在第二个方法中,我们重写UIViewControllerContextTransitioning中的代理方法。

@protocol UIViewControllerTransitioningDelegate

这个接口的作用比较简单单一,在需要VC切换的时候系统会向遵守UIViewControllerTransitoningDelegate协议的对象询问是否使用自定义的切换效果。

其中重要的方法有:

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

  • -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

  • -(id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id< UIViewControllerAnimatedTransitioning >)animator;

  • -(id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id< UIViewControllerAnimatedTransitioning >)animator;

前两个方法,中间部分是animation开头的方法,是针对动画切换的,一个是present一个是dismiss,我们分别在呈现VC和解散VC的时候,return一个遵循UIViewControllerAnimatedTransitioning协议的对象。

iOS 自定义转场入门后两个方法,大家也可以看到中间部分是interaction开发的方法,针对的是交互式切换,如图,后面详细说明,我们先将动画切换做一个好的理解。

iOS 自定义转场入门

这样一来,我们来复习一下流程,首先我们需要创建一个被切换的ViewController,使其有拥有一个遵循UIViewControllerTransitioningDelegate的代理,如图,其中ModalViewControllerDelegate是为了执行dismiss的代理协议。

iOS 自定义转场入门

在进行动画切换的ViewController中需要将自己设定为被切换的VC的代理,如图。

iOS 自定义转场入门

并遵循同一个协议,实现其中的代理方法。

其中的present和dismiss方法需要返回一个遵循UIViewControllerAnimatedTransitioning的对象,那我们就创建一个遵循该协议的对象设定动画时间和动画内容。如图:

iOS 自定义转场入门

ok,搞定了~

是不是很容易理解? 如果觉得难理解的话,没错,我和你一样,但是总体的步骤我们清晰了,三个协议,有两个需要我们实现,一个是新建的类里实现,一个是要实现转场的ViewController里实现。

Demo Time

被自定义转场的ViewController

ModalViewController.h

#import <UIKit/UIKit.h>

@class ModalViewController;
@protocol ModalViewControllerDelegate <NSObject>

- (void) modalViewControllerDidClickedDismissButton:(ModalViewController *)viewController;

@end

@interface ModalViewController : UIViewController
@property (nonatomic,weak) id<ModalViewControllerDelegate> delegate;
@property (nonatomic,weak) id<UIViewControllerTransitioningDelegate> transitioningDelegate;
@end

ModalViewController.m

#import "ModalViewController.h"

@interface ModalViewController ()

@end

@implementation ModalViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [button setTitle:@"Dismiss me" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
}

- (void)buttonClicked:(UIButton *)sender {
    if (self.delegate && [self.delegate respondsToSelector:@selector(modalViewControllerDidClickedDismissButton:)]) {
        [self.delegate modalViewControllerDidClickedDismissButton:self];
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

遵循UIViewControllerAnimatedTransitioning协议的类

BouncePresentAnimation.h

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

@interface BouncePresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>

@end

BouncePresentAnimation.m

#import "BouncePresentAnimation.h"

@implementation BouncePresentAnimation

#pragma mark - UIViewControllerAnimatedTransitioning

//系统给出一个切换上下文transitionContext,我们根据上下文环境返回这个切换多需要的花费时间,一般就返回动画的时间就好了,SDK会用这个时间来在百分比驱动的切换中进行真的计算,后面在详细展开)
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.8f;
}

//在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    //1.得到参与切换的两个ViewController的信息,使用context拿到它们的参照,
    //ViewControllerForKey提供一个key返回对应的VC, UITransitionContextToViewControllerKey是将要切入的VC,UITransitionContextFromViewControllerKey是将要切出的VC。
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //2.对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置为屏幕的下边缘
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    //finalFrameForViewController是某个VC的最终位置,initialFrameForViewController是某个VC的初始位置
    CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
    toVC.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);
    
    //3.将view添加到containerView中
    //此处containerView是VC切换所发生的view容器,开发者应该将切出的view移除,将切入的view加入到该containerView中
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    
    //4.开始动画。这里的动画时间长度和切换时间长度一致都是0.8s。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线。(此处注意UIView动画在iOS7种新添加的Category:UIViewKeyframeAnimations)
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        toVC.view.frame = finalFrame;
    } completion:^(BOOL finished) {
        //5.在动画结束后我们必须向context报告VC切换完成,是否成功。(这里的动画切换种,没有失败的可能性,因此直接pass一个YES过去)
        [transitionContext completeTransition:YES];
    }];
}
@end

执行自定义转场的ViewController

MainViewController.h

#import <UIKit/UIKit.h>

@interface MainViewController : UIViewController

@end

MainViewController.m

#import "MainViewController.h"
#import "ModalViewController.h"
#import <UIKit/UIKit.h>
#import "BouncePresentAnimation.h"
#import "NormalDismissAnimation.h"
#import "SwipeUpInteractiveTransition.h"

@interface MainViewController ()<ModalViewControllerDelegate,UIViewControllerTransitioningDelegate>
@property (nonatomic,strong) BouncePresentAnimation *presentAnimation;
//@property (nonatomic,strong) NormalDismissAnimation *dismissAnimation;
//@property (nonatomic,strong) SwipeUpInteractiveTransition *transitionController;
@end

@implementation MainViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        _presentAnimation = [BouncePresentAnimation new];
//        _dismissAnimation = [NormalDismissAnimation new];
//        _transitionController = [SwipeUpInteractiveTransition new];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *buttton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttton.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [buttton setTitle:@"Click me" forState:UIControlStateNormal];
    [buttton addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttton];
}

- (void)buttonClicked:(UIButton *)sender {
    ModalViewController *mvc = [[ModalViewController alloc] init];
    mvc.delegate = self;
    mvc.transitioningDelegate = self;
//    [self.transitionController wireToViewController:mvc];
    [self presentViewController:mvc animated:YES completion:nil];
}

#pragma mark - UIViewControllerTransitioningDelegate

//针对动画切换,我们需要分别在呈现VC和解散VC给出一个实现了UIViewControllerAnimatedTransitioning接口的对象(包含切换时长和如何切换)
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return self.presentAnimation;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return self.dismissAnimation;
}

//涉及交互式切换,之后再说
//- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator {
//
//}

//- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
//    return self.transitionController.intracting ? self.transitionController : nil;
//}

#pragma mark - ModalViewControllerDelegate

- (void)modalViewControllerDidClickedDismissButton:(ModalViewController *)viewController {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end
实现效果如图

iOS 自定义转场入门

ok,动画切换VC到此结束了~接下来我们学习交互切换,此处交互切换包括很多种,我们用通常的手势做例子。


自定义ViewController交互切换

知识储备

UIPercentDrivenInteractiveTransition

这不是一个接口,这是一个类!这是一个类!这是一个遵循UIViewControllerInteractiveTransitioning协议的类!

其中重要的方法:

  • -(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通过手势识别的长度之类的来计算一个值,然后进行更新。之后的例子里会看到详细的用法
  • -(void)cancelInteractiveTransition 报告交互取消,返回切换前的状态
  • –(void)finishInteractiveTransition 报告交互完成,更新到切换后的状态

@protocol UIViewControllerInteractiveTransitioning

这个接口是为了实现交互式切换的功能的。大部分时候我们不需要自己来实现这个接口,所以不展开说明。

Demo Time

UIPercentDrivenInteractiveTransition的子类SwipeUpInteractiveTransition

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

@interface SwipeUpInteractiveTransition : UIPercentDrivenInteractiveTransition

//设定一个BOOL变量来表示是否处于切换过程中,这个布尔值将在监测到手势开始时呗设置,我们之后会在调用返回这个InteractiveTransition的时候用到
@property (nonatomic, assign) BOOL intracting;

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

@end

SwipeUpInteractiveTransition.m

#import "SwipeUpInteractiveTransition.h"

@interface SwipeUpInteractiveTransition ()
@property (nonatomic, assign) BOOL shouldComplete;
@property (nonatomic, strong) UIViewController *presentingVC;
@end

@implementation SwipeUpInteractiveTransition

- (void)wireToViewController:(UIViewController *)viewController {
    self.presentingVC = viewController;
    [self prepareGestureRecognizerInView:viewController.view];
}

- (void)prepareGestureRecognizerInView:(UIView *)view {
    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    [view addGestureRecognizer:gesture];
}

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

- (void)handleGesture:(UIPanGestureRecognizer *)sender {
    CGPoint translation = [sender translationInView:sender.view.superview];
    switch (sender.state) {
        case UIGestureRecognizerStateBegan:
            self.intracting = YES;
            [self.presentingVC dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged: {
            //向下滑动400px或以上为100%,每次手势状态变化时根据当前手势位置计算新的百分比
            CGFloat fraction = translation.y / 400.0;
            fraction = fminf(fmaxf(fraction, 0.0), 1.0);
            self.shouldComplete = (fraction > 0.5);
            
            [self updateInteractiveTransition:fraction];
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            //当手势结束时,把正在切换的标设置回NO,然后进行判断。在2种我们设定了手势超过一半就认为应该结束手势,否则就应该返回原来状态,在这里使用shouldComplete判断,已决定这次transition是否应该结束。
            self.intracting = NO;
            if (!self.shouldComplete || sender.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
            } else {
                [self finishInteractiveTransition];
            }
            break;
        }
        default:
            break;
    }
}

@end

然后我们调整MainViewController的内容。

将上面MainViewController.m代码注释部分打开即可~

效果如图:

iOS 自定义转场入门

到此,入门结束了~

因为时间原因,我将未来要抽时间学习的博客贴在这里:https://www.jianshu.com/p/45434f73019e

相关标签: 自定义转场 iOS