3d翻转 ios_iOS自定义转场详解04——实现3D翻转效果
这是自定义转场系列的第四篇。由于具有一定的连续性,我会忽略一些基础,所以如果你是第一次看这个系列,可以先过目之前的几篇 ——— 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翻转效果相关推荐
- [Python图像识别] 五十.Keras构建AlexNet和CNN实现自定义数据集分类详解
该系列文章是讲解Python OpenCV图像处理知识,前期主要讲解图像入门.OpenCV基础用法,中期讲解图像处理的各种算法,包括图像锐化算子.图像增强技术.图像分割等,后期结合深度学习研究图像识别 ...
- object类中的equals与自定义equals方法详解
object类中的equals与自定义equal方法详解 1.this怎么理解?this == obj表示什么? this就是当前你new出来的对象,这里指谁调用equal方法this指的就是谁,ob ...
- android标尺自定义view,android尺子的自定义view——RulerView详解
项目中用到自定义尺子的样式: 原效果为 因为跟自己要使用的view稍有不同 所以做了一些修改,修改的注释都放在代码中了,特此记录一下. 首先是一个自定义View: public class RuleV ...
- mysql: union / union all / 自定义函数用法详解
mysql: union / union all http://www.cnblogs.com/wangyayun/p/6133540.html mysql:自定义函数用法详解 http://www. ...
- (119)System Verilog 父类与子类对象复制(自定义函数)详解
(119)System Verilog 父类与子类对象复制(自定义函数)详解 1.1 目录 1)目录 2)FPGA简介 3)System Verilog简介 4)System Verilog 父类与子 ...
- python权限管理系统_Django 自定义权限管理系统详解(通过中间件认证)
1. 创建工程文件, 修改setting.py文件 django-admin.py startproject project_name 特别是在 windows 上,如果报错,尝试用 django-a ...
- vue怎么实现右键二级菜单_vue中如何自定义右键菜单详解
在所编辑的页面,需要添加右键菜单的元素,绑定contextmenu事件,如下: v-for="item in resourceList" :key="item.id&qu ...
- openstack ice自定义调度算法项目详解(horizon、novaclient、api、scheduler、db、自定义数据库)
原文转自:openstack ice自定义调度算法项目详解(horizon.novaclient.api.scheduler.db.自定义数据库) 第一部分:页面层即horizon与novaclien ...
- layui单选框verify_layui lay-verify form表单自定义验证规则详解
虽然layui的官方文档已经是写的比较详细,但是初次使用的时候总会懵一下,这里纪录一下lay-verify自定义验证规则的时候到底放哪. html: 提交 js: form.verify({ //数组 ...
最新文章
- 【错误归纳】E: Sub-process /usr/bin/dpkg returned an error code (1)子进程 已安装 post-installation 脚本 返回了错误号 1
- MM模块部分名词解释
- 关闭软盘_乔布斯签名软盘驱动器即将开拍 成交价预计达7500美元
- python程序一定要有主函数_Python 没有main函数的原因
- 网络安全04_互联网发展史_网线+网卡+协议栈_中继器_集线器_网桥_路由器_AC/AP_防火墙_流控_家庭网络_小型创业公司网络_园区网_政务网络_数据中心网络拓扑_电信网/互联网_Mac地址
- 6174问题 --ACM解决方法
- php实现git服务器,如何搭建和配置Git服务器
- MySQL索引类型总结和使用技巧以及注意事项
- python coding style why_Python 编码规范(Style Guide)2
- 20200703:将有序数组转换为二叉搜索树(leetcode108)
- mysql外键约束_mysql 外键约束
- Mysql 按 create_time 排序导致的问题
- python基础函数应用_python基础之函数的应用
- 增强型MOSFET导通条件
- 网站内容收录不稳定/不收录的原因分析
- 电视dns服务器修复,电信电视dns遭到劫持的解决方法分享
- python画图像_使用python绘制SDSS图像
- 利用线性回归预测波士顿房价
- 通达信程序化交易接口使用步骤
- MapReduce论文解读
热门文章
- es6 --- 使用yield*命令遍历完全二叉树
- ES5-拓展 this指向的总结
- 巴黎市中心降下2019年第一场雪
- 程序员的国庆节如何安排,你想好了吗?
- 四大技巧轻松搞定云容器
- ebs r12 -- adadmin: error while loading shared libraries: libclntsh.so.10.1
- 演示: GTS流量×××和CAR流量监管的效果及相关实践计划
- 关于驰骋表单引擎中字段扩展设置对文本框Pop窗体返回值的升级说明 2012/11/13...
- SCCM 2007系列教程之六使用组策略实现SCCM客户端
- 我看windows mobile数据同步方案