项目概述
iOS中最常见的动画无疑是Push和Pop的转场动画了,其次是Present和Dismiss的转场动画。 如果我们想自定义这些转场动画,苹果其实提供了相关的API,在自定义转场之前,我们需要了解转场原理和处理逻辑。下面是自定义转场的效果:
Push/Pop转场 Push/Pop转场原理
在调用导航控制器的pushViewController:animated:之前,如果设置了导航控制器的delegate对象,就会调用delegate对象的回调方法navigationController:animationControllerForOperation:fromViewController:toViewController:
,可在该回调方法中自定义转场,该回调方法需要返回一个遵守UIViewControllerAnimatedTransitioning协议的对象,定义一个类实现UIViewControllerAnimatedTransitioning协议的两个方法以便自定义Push/Pop转场,这两个必须实现的方法如下:1 2 - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext; - (void )animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
用runtime给UIViewController提供一个属性hr_addTransitionFlag,用于标记是否添加自定义转场。代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @interface UIViewController (TransitionProperty) @property (nonatomic, assign) BOOL hr_addTransitionFlag; @end #import "UIViewController+TransitionProperty.h" #import <objc/runtime.h> static NSString *hr_addTransitionFlagKey = @"hr_addTransitionFlagKey" ;@implementation UIViewController (TransitionProperty) - (void ) setHr_addTransitionFlag:(BOOL) hr_addTransitionFlag { objc_setAssociatedObject(self, &hr_addTransitionFlagKey, @(hr_addTransitionFlag), OBJC_ASSOCIATION_ASSIGN); } - (BOOL)hr_addTransitionFlag { return [objc_getAssociatedObject(self, &hr_addTransitionFlagKey) integerValue] == 0 ? NO : YES; } @end
上面说过只要给导航控制器设置delegate,则调用pushViewController:animated:后,就会执行navigationController:animationControllerForOperation:fromViewController:toViewController:方法,从而展示自定义的Push/Pop转场,调用popViewControllerAnimated:后同理。导航控制器的代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -(void )pushViewController:(UIViewController *)viewController animated:(BOOL)animated{ self.delegate = (id)viewController; [super pushViewController:viewController animated:animated]; } -(UIViewController *)popViewControllerAnimated:(BOOL)animated{ self.delegate = self.viewControllers.lastObject; return [super popViewControllerAnimated:animated]; }
自定义转场
这里自定义一种Push时toView从屏幕顶部往下移动到屏幕中央的转场,Pop时toView从屏幕中央往下移出屏幕的转场。实现代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #import <UIKit/UIKit.h> @interface HRPushAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning,CAAnimationDelegate> @property(nonatomic, assign)UINavigationControllerOperation operation; @end @implementation HRPushAnimatedTransitioning -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{ return 0.4 ; } -(void )animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; UIView *containerView = transitionContext.containerView; [containerView addSubview:toView]; CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC]; CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC]; CGRect fromViewEndFrame = fromViewStartFrame; CGRect toViewEndFrame = toViewStartFrame; if (_operation == UINavigationControllerOperationPush) { toViewStartFrame.origin.y -= toViewEndFrame.size.height; }else if (_operation == UINavigationControllerOperationPop) { fromViewEndFrame.origin.y += fromViewStartFrame.size.height; [containerView sendSubviewToBack:toView]; } fromView.frame = fromViewStartFrame; toView.frame = toViewStartFrame; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromView.frame = fromViewEndFrame; toView.frame = toViewEndFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; }
处理系统的右滑返回手势
iOS7开始苹果提供了一个滑动返回上一界面的手势,由于我在pushViewController:animated:方法中设置了导航控制器的delegate,导致右滑返回手势失效,解决方式是重新设置右滑返回手势的delegate对象:1 2 3 4 5 6 7 8 9 10 11 - (void )viewDidLoad { [super viewDidLoad]; __weak typeof (self) weakself = self; if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) { self.interactivePopGestureRecognizer.delegate = weakself; } }
以上设置后,rootViewController也会响应右滑返回,可能导致一些问题,因此需要禁止rootViewController的右滑返回功能。即导航控制器中的代码如下:1 2 3 4 5 6 7 8 9 10 #pragma mark - UIGestureRecognizerDelegate -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ if (gestureRecognizer == self.interactivePopGestureRecognizer) { if (self.viewControllers.count <= 1 || self.visibleViewController == [self.viewControllers objectAtIndex:0 ]) { return NO; } } return YES; }
注意右滑返回手势默认是启用的,即self.interactivePopGestureRecognizer的enable默认是YES
处理右滑返回手势的转场
上面虽然实现了自定义Push/Pop转场,但是用系统自带滑动手势pop时并没有展示我们自定义的Push/Pop转场效果,展示的依然是系统默认的转场效果。 原因是当自定义了Push or Pop的转场,系统调用navigationController:animationControllerForOperation:fromViewController:toViewController:方法,该方法如果返回的是非nil对象后 ,就会执行以下代理方法:1 -(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
这是苹果提供给开发者自定义滑动手势交互转场的代理方法,返回一个遵守UIViewControllerInteractiveTransitioning协议的对象,该对象需要实现startInteractiveTransition:方法,为此苹果提供了一个实现该协议的UIPercentDrivenInteractiveTransition类,我们只需定义一个继承UIPercentDrivenInteractiveTransition类的类,就能满足返回对象的条件,而不需要是实现startInteractiveTransition:方法。 由于当navigationController:animationControllerForOperation:fromViewController:toViewController返回的对象非nil时,Push和Pop都会回调navigationController:interactionControllerForAnimationController:
代理方法,而我们重写该代理方法只是针对右滑返回手势的转场,其他情况返回nil,因此需要区分push还是pop。解决方式是在navigationController:animationControllerForOperation:fromViewController:toViewController中保存当前是push还是pop。代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 -(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ if (!self.hr_addTransitionFlag) { return nil; } HRPushAnimatedTransitioning *obj = [[HRPushAnimatedTransitioning alloc] init]; obj.operation = operation; _operation = operation; if (operation == UINavigationControllerOperationPush) { if (_interactive == nil) { _interactive = [[HRPercentDrivenInteractiveTransition alloc] init]; } [_interactive addGestureToViewController:self]; } return obj; } -(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{ if (_operation == UINavigationControllerOperationPush) { return nil; }else { if (_interactive.canInteractive) { return _interactive; }else { return nil; } } }
实现自定义右滑返回手势的转场
HRPercentDrivenInteractiveTransition类的逻辑是:给控制器view添加Pan手势,当右滑时,计算右滑占屏幕宽度的百分比percent(可认为是转场进度参数),然后在右滑开始时,调用导航控制器的popViewControllerAnimated:。滑动过程中调用updateInteractiveTransition:,传入转场进度参数percent。转场结束时根据转场进度,判断是调用finishInteractiveTransition(转场完成,即成功pop到上一界面)还是cancelInteractiveTransition(转场恢复到起点)。最终代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #import <UIKit/UIKit.h> @interface HRPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition @property (readonly, assign, nonatomic) BOOL canInteractive; -(void )addGestureToViewController:(UIViewController *)vc; @end @interface HRPercentDrivenInteractiveTransition () @property (nonatomic, weak) UINavigationController *nav; @property (nonatomic, strong) CADisplayLink *displayLink; @property (nonatomic, assign) CGFloat percent; @end @implementation HRPercentDrivenInteractiveTransition -(void )addGestureToViewController:(UIViewController *)vc{ UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)]; [vc.view addGestureRecognizer:pan]; self.nav = vc.navigationController; } -(void )panAction:(UIPanGestureRecognizer *)pan{ _percent = 0.0 ; CGFloat totalWidth = pan.view.bounds.size.width; CGFloat x = [pan translationInView:pan.view].x; _percent = x/totalWidth; switch (pan.state) { case UIGestureRecognizerStateBegan:{ _canInteractive = YES; [_nav popViewControllerAnimated:YES]; } break ; case UIGestureRecognizerStateChanged:{ [self updateInteractiveTransition:_percent]; } break ; case UIGestureRecognizerStateEnded:{ _canInteractive = NO; [self continueAction]; } break ; default : break ; } } -(BOOL)isCanInteractive{ return _canInteractive; } - (void )continueAction{ if (_displayLink) { return ; } _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIChange)]; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void )UIChange { CGFloat timeDistance = 1.5 /60 ; if (_percent > 0.4 ) { _percent += timeDistance; }else { _percent -= timeDistance; } [self updateInteractiveTransition:_percent]; if (_percent >= 1.0 ) { [self finishInteractiveTransition]; [_displayLink invalidate]; _displayLink = nil; } if (_percent <= 0.0 ) { [self cancelInteractiveTransition]; [_displayLink invalidate]; _displayLink = nil; } }
Present/Dismiss转场 Present/Dismiss转场原理
控制器设置transitioningDelegate为自身,遵守UIViewControllerTransitioningDelegate协议,实现协议的present动画方法和dismiss动画方法,即如下两个方法:1 2 -(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
这两个方法需要返回一个遵守UIViewControllerAnimatedTransitioning协议的对象,定义一个类实现UIViewControllerAnimatedTransitioning协议的两个方法以便自定义Present/Dismiss转场。 控制器关键代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - (instancetype)init { self = [super init]; if (self) { self.transitioningDelegate = self; } return self; } -(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionPresent]; return obj; } -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionDismiss]; return obj; }
自定义转场
这里自定义一种Present时toView从屏幕左边往右移动到屏幕中央的转场,dismiss时toView从屏幕中央往右移出屏幕的转场。实现代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 typedef NS_ENUM (NSInteger,PictureTransitionType) { PictureTransitionPresent = 0 , PictureTransitionDismiss }; @interface HRPresentAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning> - (instancetype)initType:(PictureTransitionType)type; @end #import "HRPresentAnimatedTransitioning.h" @interface HRPresentAnimatedTransitioning () @property (nonatomic, assign) PictureTransitionType type; @end @implementation HRPresentAnimatedTransitioning - (instancetype)initType:(PictureTransitionType)type{ self = [super init]; if (self) { _type = type; } return self; } -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{ return 0.4 ; } -(void )animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; UIView *containerView = transitionContext.containerView; [containerView addSubview:toView]; CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC]; CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC]; CGRect fromViewEndFrame = fromViewStartFrame; CGRect toViewEndFrame = toViewStartFrame; if (_type == PictureTransitionPresent) { toViewStartFrame.origin.x -= toViewEndFrame.size.width; }else if (_type == PictureTransitionDismiss) { fromViewEndFrame.origin.x += fromViewStartFrame.size.width; [containerView sendSubviewToBack:toView]; } fromView.frame = fromViewStartFrame; toView.frame = toViewStartFrame; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromView.frame = fromViewEndFrame; toView.frame = toViewEndFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; } @end
Next Post
两种方式实现轮播图
Previous Post
AVPlayer音频和视频播放