本文是我学习了onevcat的这篇转场入门作的一点笔记。ios
今天咱们来实现一个简单的自定义转场,咱们先来看看这篇文章将要实现的一个效果图吧:
框架
咱们先建立一个工程,首先用storyboard快速的建立两个控制器,一个做为主控制器,叫ViewController
,另一个做为present出来的控制器,叫PresentViewController
,而且用autoLayout快速搭建好界面。就像这样:
iview
咱们先作好点击ViewController
上面的按钮,present出 PresentViewController
,点击PresentViewController
上面的按钮,dismiss掉PresentViewController
的逻辑。这里有两个注意点:ide
由于此处我使用了segue
,因此在ViewController
按钮点击的时候,咱们只须要这样调用就行。学习
#pragma mark - 点我弹出 -(IBAction)presentBtnClick:(UIButton *)sender { [self performSegueWithIdentifier:@"PresentSegue" sender:nil]; }
咱们平时写dismiss的时候,通常都会是在第二个控制器中直接给self发送dismissViewController
的相关方法。在如今的SDK中,若是当前的VC是被显示的话,这个消息会被直接转发到显示它的VC去。可是这并非一个好的实现,违反了程序设计的哲学,也很容易掉到坑里。因此咱们用标准的delegate
方式实现 dismiss
。动画
首先咱们在PresentViewController
控制器中申明一个代理方法。ui
#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的点击事件中,让代理去完成关闭当前控制器的工做。this
#pragma mark - 点击关闭 - (IBAction)closeBtnClick:(UIButton *)sender { if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) { [self.delegate dismissViewController:self]; } }
与此同时,在ViewController
中须要设置PresentViewController
的代理,而且实现代理方法:atom
- (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
的对象。建立一个类叫作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]; }]; }
注意点
UITransitionContextToViewControllerKey
与UITransitionContextFromViewControllerKey
好比从A present 出B,此时A是FromViewController
,B是ToViewController
若是从B dismiss 到A,此时A是ToViewController
,B是FromViewController
这个接口的做用比较单一,在须要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。
建立一个类,继承自UIPercentDrivenInteractiveTransition
#import <UIKit/UIKit.h> @interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController; @end
既然传入了这个须要手势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; } }
和建立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
最后,咱们在主控制器中添加一个手势驱动的对象,一个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,那么手势滑动的效果将会失效。综上所述,咱们就得分状况考虑。
接下来咱们就来完善一下。
给PanInteractiveTransition
添加一个属性,表示是否处于切换过程当中(用于判断使用的是点击按钮dismiss仍是手势驱动来dismiss的)
// 是否处于切换过程当中 @property (nonatomic, assign, getter=isInteracting) BOOL interacting;
给PanInteractiveTransition
添加一个属性,表示是否须要dismiss(用于当手势滑动到超过指定高度以后,就会dismiss,若是没有超过,就会还原)
@property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
修改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; } }
另外还有一点,就是须要修改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,到此为止,咱们的一个自定义转场动画就算了完成了。