iOS利用Runtime自定义控制器POP手势动画
前言
苹果在iOS 7以后给导航控制器增加了一个Pop的手势,只要手指在屏幕边缘滑动,当前的控制器的视图就会跟随你的手指移动,当用户松手后,系统会判断手指拖动出来的大小来决定是否要执行控制器的Pop操作。
nav_pop_origin.gif
这个操作的想法非常好,但是系统给我们规定的范围必须是屏幕左侧边缘才可以触发,这样实际使用过程中对于有些产品会产生不便,于是有些app就采取整个屏幕都响应这个手势并且pop动画还是用系统原生的,这样操作起来确实方便好多。
nav_pop_custom.gif
开始大家一定会有疑问,给控制器的View加个手势然后拖动控制器的View时改变它的frame不就可以了吗?没错,加手势这个想法是正确的。但是,由我们自己来改变控制器视图的位置是比较麻烦的,细心的朋友一定发现了,我们自定义pop手势上面的导航栏也是在随着你的手势拖拽而变动的,所以这样做还需要负责导航栏的动画,而且有一个重点问题,如果单独拖动view,这个view下面会是黑黑的一片,因为控制器的push和pop层级是由系统管理的。
nav_pop_failed.gif
所以走这条路虽然可以,但实现起来会比较艰辛。那么,如何实现这个效果呢?今天就给大家提供两套实现方案。
方案一:自定义UIViewControllerInteractiveTransitioning对象,实现导航控制器代理方法。
这个是苹果官方推荐的做法,在WWDC 2013 218 - Custom Transitions Using View Controllers中有说明。
这套方案虽然实现比较麻烦,但是动画相对灵活,你可以实现这样的效果,
nav_pop_cube.gif
也可以有这种效果。
其实这个拖动过程属于导航控制器的动画,所以我们需要重写UINavigationController的两个代理方法,navigationController:animationControllerForOperation:fromViewController:toViewController:(名字很长下面就称为方法1)和
navigationController:interactionControllerForAnimationController:(方法2)。
解释一下他们的作用,方法1是苹果提供给我们用来重写控制器之间转场动画的(pop或者push)。方法2你可以这样理解,苹果让我们返回一个交互的对象,用来实时管理控制器之间转场动画的完成度,通过它我们可以让控制器的转场动画与用户交互(注意一点,如果方法1返回是nil,方法2是不会调用的,也就是说,只有我们自定义的动画才可以与控制器交互)。
下面我们来看一下实现过程。为了便于大家理解,我会尽量在Demo中的注释写的最清晰明了。
同时,我们先用最简单的代码实现,在这篇文章的最后我会对本例中的Demo提供一个相对合理的写法。
首先在方法1中,我们返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象,它就是自定义的动画对象,我们给它起名PopAnimation,在这个类中实现两个方法来自定义转场动画。
屏幕快照 2015-03-28 下午6.49.05.png
再来看方法2,我们需要返回一个遵守了UIViewControllerInteractiveTransitioning协议的对象(提示一下,这两个协议容易混淆,要注意区分,一个是负责动画,一个是负责交互过程),苹果已经有一个类专门处理这个功能,它叫UIPercentDrivenInteractiveTransition,当然你也可以自定义一个这样的类。我们可以这样理解它的作用:前面在方法1中返回的动画,会在执行的过程中被系统分解以用于用户交互,这个交互过程的动画完成度就由它来调控。下面我们来看一下如何使用它。(为了让控制器视图拖动,我们给控制器的视图加了一个拖动手势,在拖动方法里我们对这个对象进行操作)
屏幕快照 2015-03-29 下午12.33.59.png
最后在视图控制器里重写导航栏的两个方法。
屏幕快照 2015-03-29 下午12.37.51.png
有两点不要忘记:
设置导航控制器的代理为当前控制器。
给控制器加手势。
OK,这样我们就完成了这个过程。
nav_pop_own.gif
方案二:Runtime+KVC
要了解这样的做法,需要有Runtime的一些知识,会涉及到私有变量、私有方法的获取,但是这样做比较简单也比较有趣,如果你感兴趣就继续看下去吧。关于Runtime的知识,今后我会分享到博客里,朋友们敬请期待。
为了方便大家阅读下面的代码,我们需要先了解系统的这个手势。
前面我们了解到,这个手势属于UINavigationController,我们就跳到它的头文件里看看能不能找到线索。这个思路是正确的,确实有一个手势叫做interactivePopGestureRecognizer。属性为readonly,就是说我们不能给他换成自定义的手势,但是可以设置enable=NO。ok,既然找到了它,就打印一下看看它到底是一个什么手势。
屏幕快照 2015-03-26 下午5.17.35.png
通过log,我们看到他属于UIScreenEdgePanGestureRecognizer这个类(之前我是没有用到过),它继承自UIPanGestureRecognizer,出现在IOS7以后,是专门处理在屏幕边缘触发的手势类型,并且只有一个属性叫edges,用来设置它的触发边缘(上、下、左、右、全部)。看到这里一些朋友会想,直接改它的edges为全部可不可以?经过试验了解到,改这个属性是没用的,它只能用来触发边缘,设为全部的意思是四个方向的边缘会触发,而且用来做控制器POP手势的只有左边缘。
我们继续看它的log。控制台除了打印了它的类,还打印了它的触发target:_UINavigationInteractiveTransition(这是一个私有类,看来是专门用来做导航控制器交互动画的),和action:handleNavigationTransition(这是它的一个私有方法),我们要做的就是新建一个UIPanGestureRecognizer,让它的触发和系统的这个手势相同,这就需要利用runtime获取系统手势的target和action。
那么如何获取这个target呢?一开始我用kvc想直接获取这个手势的target,程序崩溃了,原来它根本没有这样一个属性。所以我能想到的是,先利用runtime遍历它的所有成员变量,看看系统是怎么存储这个属性的,
通过log我们可以看到,UIGestureRecognizer有一个叫_targets的属性,它的类型为NSMutableArray。
它是用数组来存储每一个target-action,所以可以动态的增加手势触发对象。那么又是什么存储每一个target-action呢?为了了解这个我们拿到这个属性的名字"_targets"通过kvc获取它,接着打印出来。
可以看到,由于系统重写了它的description方法,所以我们没办法通过打印获取这个对象是什么类型。既然不能打印,那么我们就用断点调试,来看它的真实类型,
我们看到,原来每一个target-action是用UIGestureRecognizerTarget这样一个类来存储的,它也是一个私有类。
苹果把许多的类做私有化也是有原因所在,其实在平时我们拿到这个类也是没有用的,他们的目的之一是避免对开发者公开无用的类,影响了封装性。所以在类的设计上,还是要向苹果学习。
下面直接看代码。
我们在控制器的ViewDidLoad加上这段代码,并且它只需要执行一次。
优化
这个demo我会提供给大家,下面简单说下程序的优化思路。
优化点一:对于方案一,其实不应该把导航控制器的代理方法以及手势处理的方法交给视图控制器,因为这段代码不是属于某一个视图控制器,而是全局的导航控制器,所以我们应该参考苹果的设计思想:新建一个专门管理交互过程的对象,这个类我们叫做NavigationInteractiveTransition。
优化点二:再来看之前的ViewDidLoad中只执行一次的代码,其实写在这里也不够妥当,同样的,这段代码也不属于某一个Controller,优化方案是新建一个导航控制器,在这个导航控制器的viewDidLoad中写上这些代码,这样也并不需要dispatch once。
优化点三:由于我们自定义的手势是加在一个私有view上,这个view是一个全局的,所以当这个控制器为根控制器时,我们的手势还是在起作用,这就相当于对根控制器做了pop操作,这会出现一个错误nested pop animation can result in corrupted navigation bar。导致这个错误的原因还有一个,如果我们pop的动画正在执行,再去触发一次手势,会导致导航控制器和导航条的动画混乱。为了避免问题出现我们需要成为手势的代理,判断当前控制器是否为根控制器并且pop或者push动画是否在执行(这个变量是私有的,需要用kvc来获取)。
经过最后的优化,视图控制器可以什么都不写,想使用这个效果,只要使用我们自定义的导航控制器就可以了,这样的好处是手势动画与控制器完全解耦,并且不用给每一个控制器都addGesture。
给大家推荐一个仓库https://github.com/nst/iOS-Runtime-Headers,这个仓库可以调取苹果的所有私有方法头文件,相当强大。
最后放上这个demo的地址:https://github.com/zys456465111/CustomPopAnimation(使用时,切换工程的scheme就能切换不同方案。对于方案二,只需要导航控制器的类就可以了。)
豆电雨 20160121
iOS利用Runtime自定义控制器POP手势动画的更多相关文章
- ios 利用runtime任性跳转
在开发项目中,会有这样变态的需求: 推送:根据服务端推送过来的数据规则,跳转到对应的控制器 feeds列表:不同类似的cell,可能跳转不同的控制器(嘘!产品经理是这样要求:我也不确定会跳转哪个界面哦 ...
- iOS - 利用runtime加深对基础知识的理解
利用runtime加深对基础知识的理解 如果对runtime需要学习,可以看这篇,以下仅作为学习笔记,相互交流. runtime的头文件: #import <objc/runtime.h> ...
- Yii2 利用controllerMap自定义控制器类
版权声明:本文为博主原创文章,未经博主允许不得转载. Yii2框架为我们自定义好的 controllers,Models,views,标准的MVC结构框架,但是有些时候我们写接口希望结构更加清晰而不 ...
- iOS开发之自定义控制器切换
iOS8以后, 苹果公司推出了UIPresentationController, 通过其(presentedController 和 presentingController)来控制modal控制器操 ...
- 利用Theme自定义Activity进入退出动画
有没有觉得Activity的默认动画太快了或者太难看了.. 我原来使用Activity.overridePendingTransition来自定义Activity的进入动画,却发现没法定义退出的动画. ...
- iOS-右滑返回,利用Runtime添加全屏Pop手势
项目中经常会遇到类似需求,需要在某控制器增加全屏右滑返回功能. 在我们不隐藏 NavigationBar 的前提下,系统会自动替我增加此功能,只是它作用的范围仅仅在屏幕左边有限区域. 我们需要在整个界 ...
- iOS 视图控制器转场动画/页面切换效果/跳转动画 学习
一 学习 在 UINavigationController 中 push 和 pop 的转场效果 (基于iOS7 以上的转场方式) 经过学习了解到,重点分三块: (1)pushAnimation: ...
- iOS:探究视图控制器的转场动画
一.介绍 在iOS开发中,转场动画的使用无处不见,不只是我们自己更多的使用UIViewblock动画实现一个转场动画,其实,在我们实现VC控制器跳转的时候都是转场动画的实现,例如标签栏控制器的切换.模 ...
- iOS 动画学习之视图控制器转场动画
一.概述 1.系统会创建一个转场相关的上下文对象,传递到动画执行器的animateTransition:和transitionDuration:方法,同样,也会传递到交互Controller的star ...
随机推荐
- 使用ssh对服务器进行登录
一.什么是SSH? 简单说,SSH是一种网络协议,用于计算机之间的加密登录. 如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会 ...
- 循序渐近学docker---笔记
1.安装docker 环境:ubuntu 16.04 sudo apt-get install docker.io root@ld-Lenovo-G470:~# docker -vDocker ver ...
- codevs愚蠢的矿工(树形DP)
/* 树形DP 根节点一定有人 然后 剩下的人没到每个孩子去 因为孩子数可能很多 不好枚举 所以转二叉树 分两部分 O(sum)就可以了 当然 转二叉树候必须顾及原来树的一些性质 如不能只选左孩子 转 ...
- Canvas transform浅析
没有前奏,直接进入主题 transform调用方法: ctx.transform(a,b,c,d,e,f);如下 var ctx = document.getElementById("myC ...
- JavaScript ArrayBuffer浅析
时隔一年半,再次来到博客园.回首刚接触前端时所写的两篇随笔,无法直视啊~ --------------------------------------------------------------- ...
- 开发solr功能问题点
Criteria criteria = new Criteria(); public Criteria and(Criteria criteria) { this.criteriaChain.add( ...
- (转)Android调用系统自带的文件管理器进行文件选择并获得路径
Android区别于iOS的沙盒模式,可以通过文件浏览器浏览本地的存储器.Android API也提供了相应的接口. 基本思路,先通过Android API调用系统自带的文件浏览器选取文件获得URI, ...
- Sqlite 错误码
#define SQLITE_OK 0 /* 成功 | Successful result */ /* 错误码开始 */ #define SQLITE_ERROR 1 /* SQL错误 或 丢失数据库 ...
- eclipse慢 优化(转)
1.打开 eclipse.ini -showsplash com.genuitec.myeclipse.product --launcher.XXMaxPermSize 256M -vmargs -D ...
- 002.AngularJs调用Restful实现CRUD
本节我们主要给大家介绍AngularJs如何调用Restful,实现数据的CRUD. 主要用到的技术: 后端:ASP.NET WebApi + SQLServer2008 前端:AngularJs,B ...