iOS转场动画(Push)

一如既往的在看文章之前我们先来看一下Demo的效果:


要实现这样的效果需要哪些技术呢?CAShaperLayer?UIBezierPath?CABasicAnimation?mask?OK,这些你都需要进行了解。此时你或许会有疑问,文章的标题是转场,这些能搞定转场?再加上UINavigationControllerDelegate和UIViewControllerTransitioningDelegate便能文题一致了。当控制器与控制器之间进行Push操作时,要实现自定义的转场动画需要遵循UINavigationControllerDelegate,并实现相关方法即可。

项目涉及技术简单介绍

在笔者的上一篇文章中已经大致讲解了mask的使用,所以在这就不赘述。主要简单介绍其它几个类的大致用法。

  • CAShaperLayer && UIBezierPath
    CAShaperLayer是一个通过矢量图形而非bitmap来绘制的图层子类。可以通过该图层去指定相应的线宽、线与线连接的样式、颜色等属性,通过path属性可以绘制出你想实现的任意图形。
    UIBezierPath是Core Graphics对path的一个封装,可以画圆、矩形或者曲线等形状。那这些都该如何使用?下面我们通过简单绘制一个火柴人去掌握这两个类的一些用法。



    我们首先将火柴人进行相应的拆分可以看出火柴人由一个圆以及多条直线组成。

// 创建bezierPath
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
/**
 *  设置path的起点为(200, 100)
 */
[bezierPath moveToPoint:CGPointMake(200, 100)];

/**
 *  绘制一个半径为50的圆
 *
 *  @param addArcWithCenter 圆点坐标
 *  @param radius 圆的半径
 *  @param startAngle 开始绘图的弧度
 *  @param endAngle 结束绘图的弧度
 *  @param clockwise 是否是顺时针
 */
[bezierPath addArcWithCenter:CGPointMake(150, 100) radius:50.f startAngle:0 endAngle:2 * M_PI clockwise:YES];

此时,你可能会想我想看看我画出的东西是否与自己预期的一致。然后command + R会发现并没有什么效果。此时CAShaperLayer便有了用武之地。

// 创建shapeLayer图层
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
// 指定shapeLayer的path
shapeLayer.path = bezierPath.CGPath;
// 设置图层绘画的线宽
shapeLayer.lineWidth = 5.0f;
// 设置图层画笔的颜色
shapeLayer.strokeColor = [UIColor redColor].CGColor;
// 设置图层的填充色
shapeLayer.fillColor = [UIColor clearColor].CGColor;
// 添加shapeLayer至父图层
[self.view.layer addSublayer:shapeLayer];

加上这段代码之后,你再次运行应用便可以看到你所绘制的图形是一个红色笔线、线宽为5的圆了。简单的绘画我们就实现了。那么继续完成火柴人的其余部分

// 设置起始点
[bezierPath moveToPoint:CGPointMake(150, 150)];
// 绘制一条长度为130的直线
[bezierPath addLineToPoint:CGPointMake(150, 280)];
// 绘制火柴人的左腿 如果不手动设置起始点,会默认上一次绘画的终点为下一次绘画的起点故而因此此处不设置起始点
[bezierPath addLineToPoint:CGPointMake(100, 350)];
// 绘制火柴人的右腿 因为此时的起始点为上一次的终点即(100,350),所以此时需要指定起始点
[bezierPath moveToPoint:CGPointMake(150, 280)];
[bezierPath addLineToPoint:CGPointMake(200, 350)];
// 最后绘制火柴人的手臂
[bezierPath moveToPoint:CGPointMake(100, 180)];
[bezierPath addLineToPoint:CGPointMake(200, 180)];

填充这段代码后继续运行。我们已经绘制完这个简单的火柴人了。
CAShaperLayer与UIBezierPath的简单结合使用就介绍到这里。

  • CABasicAnimation
    CABasicAnimation,顾名思义基本动画。可以通过其完成缩放、平移、旋转等基本的动画效果。在这里就不做详细介绍。只介绍与文前项目相关的一些属性用法。从项目中学习一些用法也不失一个好的方法。
@property(nullable, strong) id fromValue;
@property(nullable, strong) id toValue;
@property(nullable, strong) id byValue;

可以看到这三个属性的类型都是id,所以你给他们一个path也是完全可以的。

  • fromeValue:动画的起始位置
  • toValue:动画的结束位置
  • byValue:动画从当前值开始,加上byValue的值然后结束

其他的一些关于动画时间duration,beginTime等属性在上文也有相关的介绍便不赘述。
项目中可能会应用的相关技术已经讲解完成,那么我们开始我们的项目吧。

项目实践

  • 项目初始化
    此篇文章只讲解控制器之间Push以及Pop时的转场动画。所以给我们新建的项目Embed In一个导航控制器则是必不可少的,除此之外,一个Push进去的控制器也是不可获取的,那么我们在storyboard中完成这些初始化操作,并且生成相应控制器的.h和.m文件。为了解释在转场动画中涉及的一些东西,我们把根控制器的名称命名为FromViewController,将Push之后的控制器命名为ToViewController.
    接着,在根控制器以及子控制器中都添加一个UIImageView并添加约束与父View重合。并设置相应的图片。在图片的看似按钮的位置添加按钮,删除相应的title,设置约束。在FomeViewController控制器中的按钮拖线并设置show。在ToViewController控制器中的按钮连线至相应的.m文件并实现控制器的出栈操作。为了进一步美观,我们设置启动图片与根控制器的图片一致。这样我们项目的初始化操作就完成了。
  • 设置代理实现自定义转场动画(UINavigationControllerDelegate、UIViewControllerAnimatedTransitioning)
    在FromViewController中遵循协议、设置代理、实现代理方法

1.设置代理

self.navigationController.delegate = self;

2.实现代理方法

  - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                               animationControllerForOperation:(UINavigationControllerOperation)operation
                                            fromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC

在此代理方法中进行相应的判断并返回相应的实例:

 if (operation == UINavigationControllerOperationPush) {
    PushTransition *pushTransition = [[PushTransition alloc] init];
    return pushTransition;
} else {
    return nil;
}

从代理方法的返回值可以看出是一个遵循了UIViewControllerAnimatedTransitioning协议的id类型,那么我们就创建一个继承自NSObject的类并遵循UIViewControllerAnimatedTransitioning协议,命名为PushTransition。
点击跳入UIViewControllerAnimatedTransitioning。可以看到该协议有两个必须实现的方法

// 返回转场动画的时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 通过上下文实现转场动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@optional
// 动画结束
- (void)animationEnded:(BOOL) transitionCompleted;
  • 动画思路
    这类动画的思路我归结于两个圆,从一个圆到另一个圆并加上mask。或许此时你会有一些困惑,看完下张图便会一目了然。



    在整个动画的过程中,我们主要通过圆形形成遮罩去实现转场的效果,当Push进去的时候我们给ToViewController的view的layer增加一个mask,使其只显示mask的范围的内容,知道最终完全显示屏幕上的内容才结束,同理在pop时,我们由最大的圆变为最小的圆,并同样为ToViewController的view的layer增加mask。最终的路径是以按钮的frame所形成的矩形绘制一个动画结束时的圆。

  • 代码实现
    在前文创建的PushTransition类中实现代理方法。
    1.设置动画的时间为1秒钟

    - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
      return 1.0f;
    }
    

2.实现动画
在贴出代码之前,笔者想先介绍两个概念。我们可以通过上下文获得根控制器以及Push进入的子控制器,那么此时根控制器也叫做From,Push后的子控制器叫做To。当然,当我们pop时这个概念需要进行调转,即执行pop操作的控制器属于From,pop之后返回的控制器为To。这也是前面我们在命名设置控制器的名称为FromViewController以及ToViewController的原因,但这只是针对于Push,Pop时需要注意概念的实际含义,以免混淆。那么下面我们通过上下文的viewControllerForKey获取FromVC以及ToVC。

FromViewController *fromVC = (FromViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
ToViewController *toVC = (ToViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

当然也可以通过viewForKey并通过UITransitionContextToViewKey和UITransitionContextFromViewKey分别获取ToVC的View和FromVC的View。
此外,在上下文中还有一个比较重要的方法:

- (UIView *)containerView;

我们暂且理解为容器View。可以添加转场动画的子view作为它的子view。
添加子view至容器View。

[contentView addSubview:fromVC.view];
[contentView addSubview:toVC.view];

在动画实现思路那一段中我们已经讲过,要实现该动画我们需要两个圆的路径,一个是初始圆的路径,需要通过Push按钮去画一个初始的圆。所以可以通过拖线的方式将Push的按钮暴露出来。从而获取该按钮并绘制初始的圆。

UIButton *pushBtn = fromVC.pushBtn;
UIBezierPath *maskLayerStartPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(pushBtn.center.x, pushBtn.center.y, 0, 0)];

由前文的图我们可以很容易地知道,Push动画的结束点为屏幕的左下角的点,而起点则是Push按钮的中心点。因此可以根据简单的数学知识得出最大圆的半径。

CGPoint startPoint = pushBtn.center;
CGPoint finalPoint = CGPointMake(0, [UIScreen mainScreen].bounds.size.height);
double radius = sqrt(pow(startPoint.x, 2) + pow((finalPoint.y - startPoint.y), 2));

然后同样适用UIBezierPath去绘制一个最大的圆:

UIBezierPath *maskLayerFinalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(pushBtn.frame, -radius, -radius)];

再通过CAShaperLayer去实现遮罩图层。

CAShapeLayer *maskLayer = [CAShapeLayer layer];
// 使动画结束后不会回到初始动画
maskLayer.path = maskLayerFinalPath.CGPath;
toVC.view.layer.mask = maskLayer;

最后利用CABasicAnimation实现整个动画:

CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge id)(maskLayerStartPath.CGPath);
maskLayerAnimation.toValue = (__bridge id)(maskLayerFinalPath.CGPath);
maskLayerAnimation.duration = 1.0f;
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[maskLayer addAnimation:maskLayerAnimation forKey:@"pushPath"];

这个时候你运行应用,点击Push操作的按钮便可以得到Push时的动画效果。但这个时候你去点击Pop按钮进行控制器出栈时你会发现会没有任何反应。因为此时动画结束后你并未告诉系统转场动画结束了。所以下一步就是设置代理告诉系统动画结束并移除动画的layer,因为如果将已完成的动画保持在 layer 上时,会造成额外的开销(渲染器会去进行额外的绘画工作).

  • 设置代理
maskLayerAnimation.delegate = self;
  • 实现代理方法
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    [_transitionContext completeTransition:![_transitionContext transitionWasCancelled]];
    [_transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
    [_transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view.layer.mask = nil;
}

这样整个的Push过程的动画就完成了,Pop时的转场动画原理与其相同就不在此赘述。

本项目GitHub地址

https://github.com/RookieAngry/Demo_PushTransition.git

项目来源于KITTEN大神的《A GUIDE TO IOS ANIMATION》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • 前言的前言 唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各项指标...
    VincentHK阅读 5,331评论 3 44
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,368评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 反复琢磨,总不能想到一个很好的字眼,绘出衣服安稳暖心的画,解释我心里那份温暖给你的晚安。 握你小手,睡吧,夜还很长...
    半日清闲阅读 519评论 0 0
  • https 问题的解决 这里时候后台自己生成的免费证书 1.找后台要一个生成的cer文件 2.直接拖进你的工程里...
    liulianjianshu阅读 674评论 0 0