项目概述

  • iOS中最常见的动画无疑是Push和Pop的转场动画了,其次是Present和Dismiss的转场动画。

    如果我们想自定义这些转场动画,苹果其实提供了相关的API,在自定义转场之前,我们需要了解转场原理和处理逻辑。下面是自定义转场的效果:
  • 项目地址:CustomPushAndPresent

    如果文章和项目对你有帮助,还请给个Star️,你的Star️是我持续输出的动力,谢谢啦

Push/Pop转场

Push/Pop转场原理

  • 在调用导航控制器的pushViewController:animated:之前,如果设置了导航控制器的delegate对象,就会调用delegate对象的回调方法navigationController:animationControllerForOperation:fromViewController:toViewController:,可在该回调方法中自定义转场,该回调方法需要返回一个遵守UIViewControllerAnimatedTransitioning协议的对象,定义一个类实现UIViewControllerAnimatedTransitioning协议的两个方法以便自定义Push/Pop转场,这两个必须实现的方法如下:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  • 用runtime给UIViewController提供一个属性hr_addTransitionFlag,用于标记是否添加自定义转场。代码如下:
@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:后同理。导航控制器的代码如下:

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
/*给导航控制器设置了delegate,调用pushViewController:animated:后,
会去执行navigationController:animationControllerForOperation:fromViewController:toViewController:
*/
self.delegate = (id)viewController;
[super pushViewController:viewController animated:animated];
} -(UIViewController *)popViewControllerAnimated:(BOOL)animated{
/*给导航控制器设置了delegate,调用popViewControllerAnimated:后,
会去执行navigationController:animationControllerForOperation:fromViewController:toViewController:
*/
self.delegate = self.viewControllers.lastObject;
return [super popViewControllerAnimated:animated];
}

自定义转场

  • 这里自定义一种Push时toView从屏幕顶部往下移动到屏幕中央的转场,Pop时toView从屏幕中央往下移出屏幕的转场。实现代码如下:
#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];
//Push/Pop的containerView默认有一个子视图fromView
UIView *containerView = transitionContext.containerView;
NSLog(@"Push/Pop containerView: %@", containerView.subviews);
//containerView本来有fromView,只需添加toView
[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对象:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
__weak typeof(self) weakself = self;
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
/*只要自定义navigationItem的leftBarButtonItem或navigationController,滑动手势会失效。
因此要重新设置系统自带的右滑返回手势的代理为self
*/
self.interactivePopGestureRecognizer.delegate = weakself;
}
}

以上设置后,rootViewController也会响应右滑返回,可能导致一些问题,因此需要禁止rootViewController的右滑返回功能。即导航控制器中的代码如下:

#pragma mark - UIGestureRecognizerDelegate
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if (gestureRecognizer == self.interactivePopGestureRecognizer) {
//屏蔽rootViewController的滑动返回手势,避免右滑返回手势引起死机问题
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对象后,就会执行以下代理方法:
-(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。代码如下:

//用于自定义Push or Pop的转场
//返回值非nil表示使用自定义的Push or Pop转场。nil表示使用系统默认的转场
-(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) {
// NSLog(@"_interactive:%@--%@", _interactive, self);
if (_interactive == nil) {
_interactive = [[HRPercentDrivenInteractiveTransition alloc] init];
}
[_interactive addGestureToViewController:self];
}
return obj;
} //使用自定义的Push or Pop转场才会回调该方法,用于自定义滑动手势的转场交互方式
//返回值非nil表示可交互处理转场进度。nil表示无法交互处理转场进度,直接完成转场
-(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(转场恢复到起点)。最终代码如下:
#import <UIKit/UIKit.h>
//UIPercentDrivenInteractiveTransition实现UIViewControllerInteractiveTransitioning协议
@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动画方法,即如下两个方法:
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

这两个方法需要返回一个遵守UIViewControllerAnimatedTransitioning协议的对象,定义一个类实现UIViewControllerAnimatedTransitioning协议的两个方法以便自定义Present/Dismiss转场。

控制器关键代码如下:

- (instancetype)init
{
self = [super init];
if (self) {
self.transitioningDelegate = self;
}
return self;
} //present过渡动画(非交互)
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionPresent];
return obj;
} //dismiss过渡动画(非交互)
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionDismiss];
return obj;
}

自定义转场

  • 这里自定义一种Present时toView从屏幕左边往右移动到屏幕中央的转场,dismiss时toView从屏幕中央往右移出屏幕的转场。实现代码如下:
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{
//present时,fromVC是导航控制器,toVC是HRDetailViewController。dismiss时,fromVC是HRDetailViewController,toVC是导航控制器
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
//Present/Dismiss的containerView默认没有子视图
UIView *containerView = transitionContext.containerView;
// NSLog(@"Present/Dismiss containerView:%@", containerView.subviews); CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC]; CGRect fromViewEndFrame = fromViewStartFrame;
CGRect toViewEndFrame = toViewStartFrame; if (_type == PictureTransitionPresent) {
[containerView addSubview:toView];
toViewStartFrame.origin.x -= toViewEndFrame.size.width;
}else if (_type == PictureTransitionDismiss) {
fromViewEndFrame.origin.x += fromViewStartFrame.size.width;
} 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

参考资料

自定义Push/Pop和Present/Dismiss转场的更多相关文章

  1. iOS 7 present/dismiss转场动画

    前言 iOS 7以后提供了自定义转场动画的功能,我们可以通过遵守协议完成自定义转场动画.本篇文章讲解如何实现自定义present.dismiss自定义动画. 效果图 本篇文章实现的动画切换效果图如下: ...

  2. 动画切换效果之push、pop、present、dismiss

    有时候页面跳转或视图切换的时候,需要做成特定的效果,常见的push.pop.present.dismiss效果如下,注意要添加代理 push默认动画效果 CATransition *transitio ...

  3. js 的数组怎么push一个对象. Js数组的操作push,pop,shift,unshift JavaScrip

    push()函数用于向当前数组的添加一个或多个元素,并返回新的数组长度.新的元素将会依次添加到数组的末尾. 该函数属于Array对象,所有主流浏览器均支持该函数. 语法 array.push( ite ...

  4. 数据结构---设计一个栈,push, pop, min 时间复杂度都是 O(1)

    普通的栈,push, pop 操作的复杂度是 O(1), 但是如果要找出其中的最小值,则需要 O(N)的时间. 题目要求 min 复杂度也是 O(1), 做法便是 空间换时间,每一步栈的最小值都用一个 ...

  5. js中常用数组方法concat join push pop slice splice shift

    javascript给我们很多常用的 数组方法,极大方便了我们做程序.下面我们来介绍下常用的集中数组方法. 比如 concat() join() push() pop() unshift() shif ...

  6. 解决 iOS View Controller Push/Pop 时的黑影

    那么如何解决这个问题呢? 实际上很简单,如果这个 ViewController 是在 TabBarViewController 的 NavigationController 上 Push/Pop 的, ...

  7. open-falcon自定义push数据无法在grafana显示

    使用open-falcon自定义push数据,在open-falcon中数据能正常显示,而在grafana中添加监控项时却无法显示. 由上述现象可判断可能是由于open-falcon的api组件有问题 ...

  8. 汇编 push ,pop指令

    知识点:  PUSH  POP  CALL堆栈平衡  RETN指令 一.PUSH入栈指令 (压栈指令): 格式: PUSH 操作数 //sub esp,4 ;mov [esp],EBP 操作数 ...

  9. js中push(),pop(),unshift(),shift()的用法

    js中push(),pop(),unshift(),shift()的用法小结   1.push().pop()和unshift().shift() 这两组同为对数组的操作,并且会改变数组的本身的长度及 ...

随机推荐

  1. Tomcat中的一些问题

    问题: 一闪而过,解决办法: 用记事本打开startup.bat文件,在最下面添加pause 再次运行,发现问题 CATALINA_HOME是TOMCAT安装路径的别名, 计算机>属性>环 ...

  2. Excel 快速跳转到工作表

    新建 vba 模块 Sub GotoSheet() tname = InputBox("input table name") If StrPtr(tname) = 0 Then E ...

  3. pip 源的问题

    pip install -i https://pypi.doubanio.com/simple/ --trusted-host pypi.douban.com some-package

  4. Spring Cloud Eureka 实践(二)

    接上一篇的内容,Eureka服务已经启动成功后,可以尝试开发服务的提供者与消费者,并注册到Eureka来实现服务的发现与调用. 首先,在父工程中继续创建服务提供者的Module,最新的目录结构如下图所 ...

  5. Linux学习笔记--快捷键

    桌面 ALT+空格 打开窗口菜单 ALT+F1 聚焦到桌面左侧任务导航栏,可按上下键导航 ALT+F2     运行命令 ALT+F4 关闭窗口 ALT+TAB 切换程序窗口 PRINT 桌面截图 S ...

  6. 【第十篇】- Git 远程仓库(Github)之Spring Cloud直播商城 b2b2c电子商务技术总结

    Git 远程仓库(Github) Git 并不像 SVN 那样有个中心服务器. 目前我们使用到的 Git 命令都是在本地执行,如果你想通过 Git 分享你的代码或者与其他开发人员合作. 你就需要将数据 ...

  7. 【第十七篇】- Maven Web 应用之Spring Cloud直播商城 b2b2c电子商务技术总结

    Maven Web 应用 本章节我们将学习如何使用版本控制系统 Maven 来管理一个基于 web 的项目,如何创建.构建.部署已经运行一个 web 应用. 创建 Web 应用 我们可以使用 mave ...

  8. 在excel中,应用公式到多行

    当一个单元格中输入公式后, 选中单元格 然后将鼠标放到右下角的控制手柄处,当鼠标变成"黑十字"标志 双击鼠标左键 即可

  9. kubectl工具的windows\linux安装方法

    kubectl 安装 下载kubectl二进制文件 curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.10.0 ...

  10. UVA 506 System Dependencies(模拟 烂题)

    https://vjudge.net/problem/UVA-506 题目是给出了五种指令,DEPEND.INSTALL.REMOVE.LIST.END,操作的格式及功能如下: DEPEND item ...