这是自定义转场系列的第四篇。由于具有一定的连续性,我会忽略一些基础,所以如果你是第一次看这个系列,可以先过目之前的几篇 ——— UIViewControllerTransitioning的用法 、实现Keynote中的神奇移动效果、实现通过圆圈放大缩小的转场动画。

老规矩,先端上GIF。

How to work

首先在StoryBoard上拖两个UIViewController。并且在第一个VC上放一个button,使用Action Segue连接到第二个VC。

然后回到代码界面。和以往一样,我们需要创建两个文件:一个用于从第一个VC过渡到第二个VC的动画(如push),另一个这是第二个过渡到第一个VC的动画(如pop)。这里不得不说iOS7中引入的这种解耦合的方式,它的意义在于无论在哪儿需要用到转场动画的地方,直接把这两个文件扔过去就行了。

我们创建两个文件:KYPushTransition 和 KYPopTransition 。从名字可以看出,后一个是前一个的反转动画。其实,我们完全可以把两个文件写在一起:KYTransition 。因为两个文件的代码结构几乎别无二致,不同的地方也只要用布尔值区分一下就行了。但这里为了让介绍思路清晰,我们把两个动画分开来实现。

首先是KYPushTransition。

先继承 UIViewControllerAnimatedTransitioning 协议。实现下面两个方法:

- (NSTimeInterval)transitionDuration:(id )transitionContext{

//动画的时间

return 0.6f;

}

- (void)animateTransition:(id )transitionContext{

//动画的逻辑

...

}

下面具体介绍动画的逻辑。

- (void)animateTransition:(id )transitionContext{

//1

FirstViewController *fromVC = (FirstViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

SecondViewController *toVC = (SecondViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *fromView = fromVC.view;

UIView *toView = toVC.view;

UIView *containerView = [transitionContext containerView];

[containerView addSubview:toView];

[containerView sendSubviewToBack:toView];

//2

CATransform3D transform = CATransform3DIdentity;

transform.m34 = -0.002;

containerView.layer.sublayerTransform = transform;

//3

CGRect initialFrame = [transitionContext initialFrameForViewController:fromVC];

fromView.frame = initialFrame;

toView.frame = initialFrame;

//4

[self updateAnchorPointAndOffset:CGPointMake(0.0, 0.5) view:fromView];

//5

CAGradientLayer *gradient = [CAGradientLayer layer];

gradient.frame = fromView.bounds;

gradient.colors = @[(id)[UIColor colorWithWhite:0.0 alpha:0.5].CGColor,

(id)[UIColor colorWithWhite:0.0 alpha:0.0].CGColor];

gradient.startPoint = CGPointMake(0.0, 0.5);

gradient.endPoint = CGPointMake(0.8, 0.5);

UIView *shadow = [[UIView alloc]initWithFrame:fromView.bounds];

shadow.backgroundColor = [UIColor clearColor];

[shadow.layer insertSublayer:gradient atIndex:1];

shadow.alpha = 0.0;

[fromView addSubview:shadow];

//6

[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{

//旋转fromView 90度

fromView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1.0, 0);

shadow.alpha = 1.0;

} completion:^(BOOL finished) {

//7

fromView.layer.anchorPoint = CGPointMake(0.5, 0.5);

fromView.layer.position = CGPointMake(CGRectGetMidX([UIScreen mainScreen].bounds), CGRectGetMidY([UIScreen mainScreen].bounds));

fromView.layer.transform = CATransform3DIdentity;

[shadow removeFromSuperview];

[transitionContext completeTransition:YES];

}];

}

解释一下。

1)通过上下文transitionContext获得前后两个UIView,这也是发生动画的具体对象。同时还需要获得containerView,这也是动画发生的地方。我们需要把后一个视图添加上去。为了保证后一个视图加上去之后不遮住前一个视图的动画,我们还要把后一个视图放到最后:[containerView sendSubviewToBack:toView];

2)为了保证视图产生3D的效果,我们需要设置layer的仿射变换。关于仿射变化和m34的概念,推荐一篇博客:iOS的三维透视投影。

3)为fromView、toView设置初始frame。

4)重置锚点。锚点就是视图旋转时候的中心,就是那个不动的点。关于锚点以及position的关系,你可以参考这一篇解释:这将是你最后一次纠结position与anchorPoint!。所以我们在设置了锚点的之后,还需要把layer的position也设置到相应位置:

-(void)updateAnchorPointAndOffset:(CGPoint)anchorPoint view:(UIView *)view{

view.layer.anchorPoint = anchorPoint;

view.layer.position = CGPointMake(0, CGRectGetMidY([UIScreen mainScreen].bounds));

}

方便记忆,你可以理解锚点会吸附到position上。所以光改变锚点不改变position,那么结果就是锚点确实改了,但是position还是在默认的(0.5,0.5),也就是视图中心。就像这样:

5)给fromView增加左深右浅的阴影。并且一开始的透明度为0,随着翻转的角度变大过渡到1。

6)开始动画。这这里,我们让fromView翻转90度 fromView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1.0, 0);。这里要注意向外90度是-M_PI_2,y为1.0表示绕着y轴旋转。

7)动画结束,我们需要还原锚点的位置、恢复position的位置、恢复layer的transform为CATransform3DIdentity,并且把阴影层移除。由于一开始我没有没有恢复锚点和position的位置,而且一直没找到原因。知道我查看了视图的层级结构才恍然大悟:

可见,使用控制台的”Debug View Hierarchy“

是多么有用!

当然,还有最重要最关键的一步:[transitionContext completeTransition:YES];。告诉上下文,动画已经完成。如果你这里不这么做,你将无法从后一个视图返回前一个视图。

好了,至此已经完成所有push动画逻辑。实现pop的逻辑基本无二。

KYPopTransition

1、首先就是删除[containerView sendSubviewToBack:toView];,这时我们可不想让动画躲在后面。

2、第二个不同点,需要加上

//让toView的截图旋转90度

toView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0.0, 1.0, 0.0);

因为这时动画的起始应该先保持在90度的位置,然后慢慢过渡到0度。

3、阴影层的需要加在toView上,并且起始透明度应该为1,终止时为0。

How to Use

1、如果你是一个VC present 到另一个VC,那么FirstViewController和SecondViewController都需要继承协议是UIViewControllerTransitioningDelegate。

然后在FirstViewController中设置代理。

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

SecondViewController *secVC = (SecondViewController *)segue.destinationViewController;

secVC.transitioningDelegate = self;

[super prepareForSegue:segue sender:sender];

}

并实现协议的两个方法:分别对应present和dismiss。

- (id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{

KYPushTransition *flip = [KYPushTransition new];

return flip;

}

- (id )animationControllerForDismissedController:(UIViewController *)dismissed{

KYPopTransition *flip = [KYPopTransition new];

return flip;

}

2、如果你用UINavigationController去控制两个VC,此时如果你什么都不做,segue会自动变成标准的导航栏推进的push、pop。要使用我们自定义的动画,需要让FirstViewController继承协议UINavigationControllerDelegate,然后设置代理为自己self.navigationController.delegate = self;。

实现UINavigationControllerDelegate中的相关协议方法,只需要一个:

- (id )navigationController:(UINavigationController *)navigationController

animationControllerForOperation:(UINavigationControllerOperation)operation

fromViewController:(UIViewController *)fromVC

toViewController:(UIViewController *)toVC{

if (operation == UINavigationControllerOperationPush) {

KYPushTransition *flip = [KYPushTransition new];

return flip;

}else if (operation == UINavigationControllerOperationPop){

KYPopTransition *flip = [KYPopTransition new];

return flip;

}else{

return nil;

}

}

搞定,现在就运行了。

Where to go —— 如何为转场增加手势交互

如果是一个VC present 到另一个VC,那么就要实现UIViewControllerTransitioningDelegate中的两个方法,和UIViewControllerAnimatedTransitioning一样,分别对应 present 和 dismiss的动画。

- (id )interactionControllerForPresentation:(id )animator;

- (id )interactionControllerForDismissal:(id )animator;

这时,你可能已经被各种协议代理名字搞晕了,是的,我一开始也晕了,但只要多练几次还是能就熟悉的。

既然我们之前是创建了两个文件继承UIViewControllerAnimatedTransitioning来实现过渡动画,那么是不是也应该继承UIViewControllerInteractiveTransitioning来实现百分比交互动画呢?因为在图中,两者的关系是并列的。是的,你可以这么做。但是苹果提供了一个更好的类 ———— UIPercentDrivenInteractiveTransition。

顾名思义,我们就可以猜到这个类就是专门用来实现手势百分比交互的。怎么使用呢?

为了遵循解耦合,我们新建一个UIPercentDrivenInteractiveTransition的子类 —— KYPopInteractiveTransition。

创建一个方法-(void)addPopGesture:(UIViewController *)viewController;,用来给目标视图控制器添加一个边缘滑动手势:

-(void)addPopGesture:(UIViewController *)viewController{

presentedVC = viewController;

UIScreenEdgePanGestureRecognizer *edgeGes = [[UIScreenEdgePanGestureRecognizer alloc]initWithTarget:self action:@selector(edgeGesPan:)];

edgeGes.edges = UIRectEdgeLeft;

[viewController.view addGestureRecognizer:edgeGes];

}

实现相应的手势方法:

-(void)edgeGesPan:(UIScreenEdgePanGestureRecognizer *)edgeGes{

//1

CGFloat translation =[edgeGes translationInView:presentedVC.view].x;

CGFloat percent = translation / (presentedVC.view.bounds.size.width);

percent = MIN(1.0, MAX(0.0, percent));

NSLog(@"%f",percent);

switch (edgeGes.state) {

case UIGestureRecognizerStateBegan:{

//2

self.interacting = YES;

[presentedVC dismissViewControllerAnimated:YES completion:nil];

//如果是navigationController控制,这里应该是[presentedVC.navigationController popViewControllerAnimated:YES];

break;

}

case UIGestureRecognizerStateChanged:{

//3

[self updateInteractiveTransition:percent];

break;

}

case UIGestureRecognizerStateEnded:{

//4

self.interacting = NO;

if (percent > 0.5) {

[self finishInteractiveTransition];

}else{

[self cancelInteractiveTransition];

}

break;

}

default:

break;

}

}

1)计算手指在X轴方向上的偏移距离,与屏幕的宽度的之比保存为一个百分比。也就是说,当手指划过屏幕的距离超过屏幕宽度的1/2,那么剩下的动画就自动完成;否则,取消动画。这里用了MIN和MAX把百分比始终控制在了0~1之间。

2)滑动开始,指定要执行的操作。这里因为没有使用UINavigation控制两个VC,所以是dismissViewControllerAnimated:。如果是用UINavigation去控制的,那么这里相应的应该是navigationController popViewControllerAnimated:。 self.interacting的作用稍后揭晓。

3)在UIGestureRecognizerStateChanged 调用 [self updateInteractiveTransition:percent]。这里我们把刚才的百分比传了过去,系统就可以通过这个0~1的竖数值实时改变动画的进度。

4)当UIGestureRecognizerStateEnded的时候,我们需要判断此时手指是否划过屏幕大于一半的距离。如果大于一半,告诉系统完成:[self finishInteractiveTransition]; 反之,告诉系统取消操作:[self cancelInteractiveTransition],这时动画也将返回初始位置。

特别注意,当我们使用了手势百分比交互,在相应的动画逻辑KYPopTransition中,把原来的[transitionContext completeTransition:YES] 改成 [transitionContext completeTransition:![transitionContext transitionWasCancelled]]。如果一直是YES的话,当我们手指划过小于屏幕一半,即使系统知道是取消动画,但在上下文中依然是写死的YES。

使用手势百分比交互

在FirstViewController中的-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender里面,创建一个KYPopInteractiveTransition的实例并把SecondVC传过去:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

...

popInteractive = [KYPopInteractiveTransition new];

[popInteractive addPopGesture:secVC];

...

}

然后实现UIViewControllerTransitioningDelegate里的这个关于手势百分比交互的方法:

- (id )interactionControllerForDismissal:(id )animator{

return popInteractive.interacting ? popInteractive : nil;

}

好了,这里你发现我们使用了popInteractive.interacting来判断,还记得之前买的关子吗?self.interacting的作用就在这里。因为- (id )interactionControllerForDismissal:(id )animator 这个方法在 点击dismiss和滑动dismiss时候都会调用。然而如果只是return popInteractive;的话,当我们点击dismiss的时候,程序将不会做出反应。所以,我们需要区分,到底是点击dismiss还是滑动dismiss。因此,我们需要一个布尔值来判断,就是这样。

这个系列应该差不多到这里就结束了。本篇的源码你可以在这里获得。

有任何疑问,欢迎在下方评论区域留言:D

3d翻转 ios_iOS自定义转场详解04——实现3D翻转效果相关推荐

  1. [Python图像识别] 五十.Keras构建AlexNet和CNN实现自定义数据集分类详解

    该系列文章是讲解Python OpenCV图像处理知识,前期主要讲解图像入门.OpenCV基础用法,中期讲解图像处理的各种算法,包括图像锐化算子.图像增强技术.图像分割等,后期结合深度学习研究图像识别 ...

  2. object类中的equals与自定义equals方法详解

    object类中的equals与自定义equal方法详解 1.this怎么理解?this == obj表示什么? this就是当前你new出来的对象,这里指谁调用equal方法this指的就是谁,ob ...

  3. android标尺自定义view,android尺子的自定义view——RulerView详解

    项目中用到自定义尺子的样式: 原效果为 因为跟自己要使用的view稍有不同 所以做了一些修改,修改的注释都放在代码中了,特此记录一下. 首先是一个自定义View: public class RuleV ...

  4. mysql: union / union all / 自定义函数用法详解

    mysql: union / union all http://www.cnblogs.com/wangyayun/p/6133540.html mysql:自定义函数用法详解 http://www. ...

  5. (119)System Verilog 父类与子类对象复制(自定义函数)详解

    (119)System Verilog 父类与子类对象复制(自定义函数)详解 1.1 目录 1)目录 2)FPGA简介 3)System Verilog简介 4)System Verilog 父类与子 ...

  6. python权限管理系统_Django 自定义权限管理系统详解(通过中间件认证)

    1. 创建工程文件, 修改setting.py文件 django-admin.py startproject project_name 特别是在 windows 上,如果报错,尝试用 django-a ...

  7. vue怎么实现右键二级菜单_vue中如何自定义右键菜单详解

    在所编辑的页面,需要添加右键菜单的元素,绑定contextmenu事件,如下: v-for="item in resourceList" :key="item.id&qu ...

  8. openstack ice自定义调度算法项目详解(horizon、novaclient、api、scheduler、db、自定义数据库)

    原文转自:openstack ice自定义调度算法项目详解(horizon.novaclient.api.scheduler.db.自定义数据库) 第一部分:页面层即horizon与novaclien ...

  9. layui单选框verify_layui lay-verify form表单自定义验证规则详解

    虽然layui的官方文档已经是写的比较详细,但是初次使用的时候总会懵一下,这里纪录一下lay-verify自定义验证规则的时候到底放哪. html: 提交 js: form.verify({ //数组 ...

最新文章

  1. 【错误归纳】E: Sub-process /usr/bin/dpkg returned an error code (1)子进程 已安装 post-installation 脚本 返回了错误号 1
  2. MM模块部分名词解释
  3. 关闭软盘_乔布斯签名软盘驱动器即将开拍 成交价预计达7500美元
  4. python程序一定要有主函数_Python 没有main函数的原因
  5. 网络安全04_互联网发展史_网线+网卡+协议栈_中继器_集线器_网桥_路由器_AC/AP_防火墙_流控_家庭网络_小型创业公司网络_园区网_政务网络_数据中心网络拓扑_电信网/互联网_Mac地址
  6. 6174问题 --ACM解决方法
  7. php实现git服务器,如何搭建和配置Git服务器
  8. MySQL索引类型总结和使用技巧以及注意事项
  9. python coding style why_Python 编码规范(Style Guide)2
  10. 20200703:将有序数组转换为二叉搜索树(leetcode108)
  11. mysql外键约束_mysql 外键约束
  12. Mysql 按 create_time 排序导致的问题
  13. python基础函数应用_python基础之函数的应用
  14. 增强型MOSFET导通条件
  15. 网站内容收录不稳定/不收录的原因分析
  16. 电视dns服务器修复,电信电视dns遭到劫持的解决方法分享
  17. python画图像_使用python绘制SDSS图像
  18. 利用线性回归预测波士顿房价
  19. 通达信程序化交易接口使用步骤
  20. MapReduce论文解读

热门文章

  1. es6 --- 使用yield*命令遍历完全二叉树
  2. ES5-拓展 this指向的总结
  3. 巴黎市中心降下2019年第一场雪
  4. 程序员的国庆节如何安排,你想好了吗?
  5. 四大技巧轻松搞定云容器
  6. ebs r12 -- adadmin: error while loading shared libraries: libclntsh.so.10.1
  7. 演示: GTS流量×××和CAR流量监管的效果及相关实践计划
  8. 关于驰骋表单引擎中字段扩展设置对文本框Pop窗体返回值的升级说明 2012/11/13...
  9. SCCM 2007系列教程之六使用组策略实现SCCM客户端
  10. 我看windows mobile数据同步方案