一如既往的在看文章之前我们先来看一下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》