自定义Push/Pop和Present/Dismiss转场
项目概述
- 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转场的更多相关文章
- iOS 7 present/dismiss转场动画
前言 iOS 7以后提供了自定义转场动画的功能,我们可以通过遵守协议完成自定义转场动画.本篇文章讲解如何实现自定义present.dismiss自定义动画. 效果图 本篇文章实现的动画切换效果图如下: ...
- 动画切换效果之push、pop、present、dismiss
有时候页面跳转或视图切换的时候,需要做成特定的效果,常见的push.pop.present.dismiss效果如下,注意要添加代理 push默认动画效果 CATransition *transitio ...
- js 的数组怎么push一个对象. Js数组的操作push,pop,shift,unshift JavaScrip
push()函数用于向当前数组的添加一个或多个元素,并返回新的数组长度.新的元素将会依次添加到数组的末尾. 该函数属于Array对象,所有主流浏览器均支持该函数. 语法 array.push( ite ...
- 数据结构---设计一个栈,push, pop, min 时间复杂度都是 O(1)
普通的栈,push, pop 操作的复杂度是 O(1), 但是如果要找出其中的最小值,则需要 O(N)的时间. 题目要求 min 复杂度也是 O(1), 做法便是 空间换时间,每一步栈的最小值都用一个 ...
- js中常用数组方法concat join push pop slice splice shift
javascript给我们很多常用的 数组方法,极大方便了我们做程序.下面我们来介绍下常用的集中数组方法. 比如 concat() join() push() pop() unshift() shif ...
- 解决 iOS View Controller Push/Pop 时的黑影
那么如何解决这个问题呢? 实际上很简单,如果这个 ViewController 是在 TabBarViewController 的 NavigationController 上 Push/Pop 的, ...
- open-falcon自定义push数据无法在grafana显示
使用open-falcon自定义push数据,在open-falcon中数据能正常显示,而在grafana中添加监控项时却无法显示. 由上述现象可判断可能是由于open-falcon的api组件有问题 ...
- 汇编 push ,pop指令
知识点: PUSH POP CALL堆栈平衡 RETN指令 一.PUSH入栈指令 (压栈指令): 格式: PUSH 操作数 //sub esp,4 ;mov [esp],EBP 操作数 ...
- js中push(),pop(),unshift(),shift()的用法
js中push(),pop(),unshift(),shift()的用法小结 1.push().pop()和unshift().shift() 这两组同为对数组的操作,并且会改变数组的本身的长度及 ...
随机推荐
- JS 之 每日一题 之 算法 ( 划分字母区间 )
题目详解: 字符串 S 由小写字母组成.我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段.返回一个表示每个字符串片段的长度的列表. 例子: 示例 1: 输入:S = &quo ...
- 理解ASP.NET Core - [03] Dependency Injection
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...
- Python3实现打格点算法的GPU加速
技术背景 在数学和物理学领域,总是充满了各种连续的函数模型.而当我们用现代计算机的技术去处理这些问题的时候,事实上是无法直接处理连续模型的,绝大多数的情况下都要转化成一个离散的模型再进行数值的计算.比 ...
- Linux新加磁盘并挂载到目录
步骤:1.分区 ----> 2.格式化 ----> 3.挂载 一.查看当前情况 1. 2. 二.磁盘分区 fdisk /dev/sdb 1.输入n,表示添加一个新的分区 2. e ex ...
- 【linux】 linux超实用命令整理
linux实用命令整理 由于开发过程中经常接触linux系统进行各种情况下的操作,故开此博客整理生产环境下操作命令集,温故而知新. 系统命令 快捷键操作命令 1.tab //命令或路径等的补全键,li ...
- Spring-图解
- 6步快速配置Tomcat环境变量(Win10)
一.配置 tomcat环境变量之前先安装jdk和配置jdk的环境变量 1.首先右击我的电脑(此电脑),点击属性,或者也可以从控制面板上打开,如下图,找到系统点击高级系统设置: 2.然后进入系统属性界面 ...
- Spring Boot中有多个@Async异步任务时,记得做好线程池的隔离!
通过上一篇:配置@Async异步任务的线程池的介绍,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务.为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资 ...
- 分组密码(三)DES 算法— 密码学复习(六)
在介绍完Feistel结构之后,接下来进入到著名的DES算法. 6.1 DES算法的意义 在正式介绍DES之前,首先介绍几个重要的历史时间节点. ① 1973年,美国国家标准局(NBS)向社会公开征集 ...
- Elasticsearch(ES)分词器的那些事儿
1. 概述 分词器是Elasticsearch中很重要的一个组件,用来将一段文本分析成一个一个的词,Elasticsearch再根据这些词去做倒排索引. 今天我们就来聊聊分词器的相关知识. 2. 内置 ...