iOS开发 ----- UIViewController转场动画
UIViewController的转场动画
1. 自从开始看UI,就一直有个问题,就是类似微信的那个页面切换的效果怎么实现,也就是这个效果 【iOS开发 ----- UIViewController转场动画】
想过ScrollerView,但这样的话,会增大VC之间的耦合度,导致管理起来很混乱,在加上tableView的话,分分钟不想写的节奏啊,于是乎开始爬各种博客,终于,找到了目前认为最完美的解决方案,这样写的话,可以完美的实现这样的效果,且VC之间的耦合度时最低的 Demo下载地址
2. 关于这些转场动画是通过iOS7中新的API来实现的,极大的减轻了程序猿的琐碎工作,通过相关代理和协议来实现转场动画,不管是Nav的push 或者pop,或者是推出模态视图的present和dismiss,还是TabBar的点击切换的动画,都是可以实现,iOS的整齐对仗的API接口,理解了一个,也就不难理解另外两个,这里以推出推出模态视图的present 和dissmiss为例,进行演示讲解,另外两个,雷同
3. 关于动画,分为可交互与不可交互
1. 可交互,也就是转场的时候,view跟着手指移动,就像上边的图片一样
2. 不可交互,就是点击之后,就会切换过来,view不会跟着手指移动
4. 下边讨论实现过程(A –> B)
4.1 首先先说动画,然后再说交互
4.2 要想实现转场动画,得知道在哪里动画,Apple给了我们很方便的API来完成这个工作,平时我们在用的时候就是直接present,或者dissmiss,这样的话系统有几个默认的动画,想要自定制的话,就要实现一个动画协议,然后实现这个方法,以present为例,我们遵守这个协议
UIViewControllerTransitioningDelegate
而在这个协议里边,我们有两个方法来控制视图的动画 1.present时的动画
-(id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
2.dismiss时候的动画
-(id)animationControllerForDismissedController:(UIViewController *)dismissed
4.3 我们可以看出,这两个方法的返回值是遵守了UIViewControllerAnimatedTransitioning协议的id类型的对象,而这个正是这个协议的方法,可以让程序猿高度自定制动画,为了让增加代码的重用性,我们为此新建了一个Animation类,遵守这个协议 在这个协议里边,我们一般要实现两个方法 1.动画时长,该方法返回动画的时长
-(NSTimeInterval)transitionDuration:(id)transitionContext
2.动画的内容
-(void)animateTransition:(id)transitionContext
4.3.1 这样的话,我们就可以定义动画了,在上述第二个方法中的transitionContext,就是转场的上下文,有了这个上下文,我们就可以很方便的做动画了.而这个上下文,有一个属性,就是containerView,通过
UIView * view = [transitionContext containerView];
我们就可以拿到上下文的view,这个view就相当与一个舞台,然后,怎样动画,就取决于你自己了 例如从A –> B 转场,你要做就是,获取A,获取B,然后放在上下文的View上,进行动画,就OK了. 然而如果直接实例化A的对象,实例化B的对象的话,就不能很好的做到复用,但如此机智的Apple怎么会没有办法呢,我们可以动过简单的KVC的方式,获取A和B 从A –> B 那么fromeView就是A ,toView就是B,和舞台的转场一样,由场景A–>B 也就是 fromA –> toB
UIView * fromeView = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView * toView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
4.3.2 然而对于模态视图,一般有present和dissmiss,所以,在该类中我们应该定义一个枚举类型来区分这两种情况
typedef NS_ENUM(NSInteger, AnimationType){present,
dismiss};
4.3.3 在present的动画中,我们实现类似上图的效果,就是从滑动的时候跟所手指而移动,在这里我们先不说如何交互,直说动画,上边我们已经拿到了A 和 B,所以动画的过程应该是这样的,B在屏幕外,A在屏幕里边,然后随着手指的移动,B过来,A离开,大概是这个样子的
文章图片
所以代码应该是这样的
//B放在外边
toView.transform = CGAffineTransformMakeTranslation(375, 0);
//添加到舞台上
[view addSubview:fromeView];
[view addSubview:toView];
//开始动画
[UIView animateWithDuration:1.0f animations:^{
//让将要出现的view的最终位置就是屏幕的位置
//另一个view从左边退出
toView.transform = CGAffineTransformMakeTranslation(0, 0);
fromeView.transform = CGAffineTransformMakeTranslation(-375, 0);
} completion:^(BOOL finished) {
//重置fromeView
//自己remove的话,系统就会remove
fromeView.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
结束之后一定要调用这个方法,不然的话,会出现怪异的情况
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
4.4 上边就是动画的过程,下边说如何交互,也就是动画跟随者手指动,要想实现交互,就要实现 UIViewControllerTransitioningDelegate 协议的另一方法
-(id)interactionControllerForPresentation:(id)animator
4.4.1 我们可以看出,这个方法返回一个遵守了UIViewControllerInteractiveTransitioning协议的对象,然而系统里已经帮我们写了一个类,并且遵守了这个协议,那就是UIPercentDrivenInteractiveTransition,所以我们只需要在fromeView里边,声明一个这个类型的属性即可
@property(nonatomic,strong)UIPercentDrivenInteractiveTransition * interactive;
4.4.2 要想交互,肯定是要添加手势的,所以我们在View上添加一个手势
//添加滑动手势
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(next:)];
[self.view addGestureRecognizer:pan];
4.4.3 然后就是发生手势的方法,要想交互,其实每那么复杂,后台很多机制,Apple已经帮我们实现,我们只要调用一个方法即可
[self.interactive updateInteractiveTransition:-progress];
4.4.4 这个方法需要一个参数,也就是滑动的距离和屏幕的百分比,0.0 – 1.0之间的一个数,通过他,我们就可以实现交互了,但这个什么时候调用呢?手势都有状态,开始,移动,结束,在开始的时候,我们应该初始化我们的interactive,然后present视图.上边说了,实现动画要遵循一个代理,也就是toView要遵循UIViewControllerTransitioningDelegate协议,然后设置代理,这样才能出现动画,
//开始触摸的时候
if (recognizer.state == UIGestureRecognizerStateBegan) {//初始化交互对象
self.interactive = [[UIPercentDrivenInteractiveTransition alloc]init];
//推出的视图
SecendViewController * sec = [[SecendViewController alloc]init];
//设置推出的视图的代理为自己,这样的话,就能调用UIViewControllerTransitioningDelegate的代理方法
sec.transitioningDelegate = self;
//推出视图
[self presentViewController:sec animated:YES completion:nil];
}
4.4.5 手指移动的时候我们应该计算一下手指滑动位置相对屏幕宽度的比例,然后在手指移动的时候,更新过场动画,因为我们这里是从右向做滑动才推出视图,所以process是一个负数,我们取反然后更新
//获得滑动的比例 0.0 - 1.0之间的数值
//从右向左滑动的时候为负数
//从左向右滑动的时候为整数
NSLog(@"%@",NSStringFromCGPoint([recognizer locationInView:self.view]));
CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width;
NSLog(@"%f",progress);
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
//当状态改变的时候,更新交互界面,也就是界面随着手指运动,从右向左滑动的时候,时progress是负数,取反
[self.interactive updateInteractiveTransition:-progress];
}
4.4.6 然后就是结束的时候的处理了,当手指拉过屏幕一半的时候,我们完成动画,完全展示toView,否则的话,取消动画,然后把interactive设为nil即可
else if (recognizer.state == UIGestureRecognizerStateEnded)
{
//当滑动大于一半的时候,完成转场,否则,取消转场
if (progress < -0.5) {
[self.interactive finishInteractiveTransition];
}else
{
[self.interactive cancelInteractiveTransition];
}
self.interactive = nil;
}
4.4.7 所以整体拖动手指的时候处理的方法应该是这样的
-(void)next:(UIPanGestureRecognizer *)recognizer
{//获得滑动的比例 0.0 - 1.0之间的数值
//从右向左滑动的时候为负数
//从左向右滑动的时候为整数
NSLog(@"%@",NSStringFromCGPoint([recognizer locationInView:self.view]));
CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width;
NSLog(@"%f",progress);
//当滑动开始的时候
if (recognizer.state == UIGestureRecognizerStateBegan) {//初始化交互对象
self.interactive = [[UIPercentDrivenInteractiveTransition alloc]init];
//推出的视图
SecendViewController * sec = [[SecendViewController alloc]init];
//设置推出的视图的代理为自己,这样的话,就能调用UIViewControllerTransitioningDelegate的代理方法
sec.transitioningDelegate = self;
//推出视图
[self presentViewController:sec animated:YES completion:nil];
}else if (recognizer.state == UIGestureRecognizerStateChanged)
{
//当状态改变的时候,更新交互界面,也就是界面随着手指运动,从右向左滑动的时候,时progress是负数,取反
[self.interactive updateInteractiveTransition:-progress];
}else if (recognizer.state == UIGestureRecognizerStateEnded)
{
//当滑动大于一半的时候,完成转场,否则,取消转场
if (progress < -0.5) {
[self.interactive finishInteractiveTransition];
}else
{
[self.interactive cancelInteractiveTransition];
}
self.interactive = nil;
}}
这样的话,我们就基本完成了动画,而且是交互式的 4.5 上边提到的返回动画的那个协议方法,应该是这样的,因为我们要推出一个视图,通过我们定义的枚举值,使逻辑更加清晰
-(id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
Animation * animation = [[Animation alloc]init];
//设置动画的类型为present
animation.type = present;
return animation;
}
4.6 交互的协议方法返回应该是这样的
-(id)interactionControllerForPresentation:(id)animator
{
return self.interactive;
}
文章图片
5 这里说 B –> A 的转场动画 5.1 我们都知道,iOS7里边Apple引入了一个侧滑返回的机制,这样的话,更加有利于单手操作,这里我们的B dismiss的时候我们就模仿一下这个动画,而且我们要做的是在屏幕任何位置侧滑都可以返回,这样交互起来更加的友好
5.2 同样我们和上边一样,也要遵守代理UIViewControllerTransitioningDelegate
5.3 同样我们要添加手势
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(dismiss:)];
[self.view addGestureRecognizer:pan];
5.4 同样我们要处理手势,和上边类似,只不过计算的方式不一样,前边的progress是负数,这里边是正数,因为是从左往右滑动
-(void)dismiss:(UIPanGestureRecognizer *)recognizer
{
self.transitioningDelegate = self;
CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width;
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.interactive = [[UIPercentDrivenInteractiveTransition alloc]init];
[self dismissViewControllerAnimated:YES completion:nil];
}else if (recognizer.state == UIGestureRecognizerStateChanged)
{
[self.interactive updateInteractiveTransition:progress];
}else if(recognizer.state == UIGestureRecognizerStateEnded)
{
if (progress > 0.5) {
[self.interactive finishInteractiveTransition];
}else
{
[self.interactive cancelInteractiveTransition];
}
self.interactive = nil;
}
}
5.5 同样我们要返回动画
-(id)animationControllerForDismissedController:(UIViewController *)dismissed
{
Animation * animation = [[Animation alloc]init];
animation.type = dismiss;
return animation;
}
5.6 同样我们要返回交互对象
-(id)interactionControllerForDismissal:(id)animator
{
return self.interactive;
}
5.7 这里我们的动画就是这样的一种形式了,这里我们应该这么放两个视图,B –> A A在下边, B在上边,手指移动的时候,A不动,然后B跟着手指,往右走,直到走出一个屏幕,大致应该是这样的
文章图片
5.8 那么,这个动画应该是这样的
//拿到上下文的view,相当于一个舞台
UIView * view = [transitionContext containerView];
//则 fromeView 就是B,
//则 toView就是A
UIView * fromeView = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView * toView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
//根据动画的样式不同,两个view摆放的位置不同,这里的动画时模仿navController pop时候的滑动返回效果
//所以,把将要出现的view放在自己下边
[view insertSubview:toView belowSubview:fromeView];
//开始动画
[UIView animateWithDuration:1.0 animations:^{
//让自己走出一个屏幕
fromeView.transform = CGAffineTransformMakeTranslation(375, 0);
} completion:^(BOOL finished) {
//完成时候一定要调用这个方法,不然会发生意想不到的情况
//这个方法标志者动画的完成
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
文章图片
另外,Nav的push和pop的实现效果类似,只要遵守UINavigationControllerDelegate 如果是tabBar点击的话,就是UITabBarControllerDelegate 相关的API在协议里边都可以找到,实现原理是一样的 以上就是全部内容了,虽然比较丑,但原理就是这样的,不足之处还望多多指教
推荐阅读
- 深入理解Go之generate
- 2020-04-07vue中Axios的封装和API接口的管理
- 标签、语法规范、内联框架、超链接、CSS的编写位置、CSS语法、开发工具、块和内联、常用选择器、后代元素选择器、伪类、伪元素。
- iOS中的Block
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- iOS面试题--基础
- 《将来的你,一定会感谢现在战胜烦恼的自己-------第四章/第十一节/用逆向思维解除烦恼》
- 我的软件测试开发工程师书单