iOS - 自定义转场动画
前言
虽然在项目中自定义转场动画用的相对较少,但是相关的知识点还是需要掌握的,下面我们借助一个例子来实现下转场动画。直接看效果:
转场动画001
思路
实现相关协议
在协议中实现动画
代码
首先创建工程,给ViewController
添加代理UINavigationControllerDelegate
,在ViewController.h文件添加一个按钮,viewWillAppear方法里面设置代理:
//ViewController.h
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *blackBtn;
@end
//ViewController.m
@interface ViewController ()<UINavigationControllerDelegate>
@end
@implementation ViewController
- (void)viewWillAppear:(BOOL)animated{
//设置代理
self.navigationController.delegate = self;
}
接着实现相关协议,此处使用的UINavigationController,所对应的专场就是push和pop,
//告诉nav,我想自己自定义一个转场
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
/*
navigationController: 导航栏
operation: 判断是push/pop
UIViewControllerAnimatedTransitioning:转场动画返回!
*/
/*
UINavigationControllerOperationNone,
UINavigationControllerOperationPush,
UINavigationControllerOperationPop,
判断operation ,判断是push/pop界面
我们是从黑色跳转到红色,所以是push
*/
if (operation == UINavigationControllerOperationPush) {
//2.初始化自定义动画类
LcrTransition *ct = [LcrTransition new];
ct.isPush = YES;
return ct;
}
return nil;
}
同样的,另写一个控制器BlackViewController,添加一个红色按钮,并且添加UINavigationControllerDelegate代理,并实现响应的代理方法:
//告诉nav,我想自己自定义一个转场
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPop) {
//2.初始化自定义动画类
LcrTransition *ct = [LcrTransition new];
ct.isPush = NO;
return ct;
}
return nil;
}
此界面是pop回ViewController,所以需要判断当前是否是pop操作,然后再返回转场动画封装实例。
LcrTransition 类就是我们封装动画的类,@property (nonatomic, assign) BOOL isPush; 作为分辨push/pop,需要使其遵循UIViewControllerAnimatedTransitioning协议,并实现相关的协议方法:
NS_SWIFT_UI_ACTOR
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// This is used for percent driven interactive transitions, as well as for
// container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@optional
/// A conforming object implements this method if the transition it creates can
/// be interrupted. For example, it could return an instance of a
/// UIViewPropertyAnimator. It is expected that this method will return the same
/// instance for the life of a transition.
- (id <UIViewImplicitlyAnimating>) interruptibleAnimatorForTransition:(id <UIViewControllerContextTransitioning>)transitionContext API_AVAILABLE(ios(10.0));
// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;
@end
#import "LcrTransition.h"
#import "BlackViewController.h"
#import "ViewController.h"
@interface LcrTransition () <CAAnimationDelegate>
@property(nonatomic,strong)id<UIViewControllerContextTransitioning> context;
@end
@implementation LcrTransition
//1.定义转场动画时长
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return .8;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//1.动画是基于上下文的,首先自定义一个上下文
_context = transitionContext;
//2.获取View的容器(通过上下文获取)containerView在其中进行动画转换的视图。
UIView *containerView = [transitionContext containerView];
//3.获取跳转到的那个界面ViewController,跳转的下一个控制器
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//4.将toVC.view 添加到containerView 容器来.
//实质上自定义转场动画,我们实现的其实就是2个View之间切换的动画逻辑
[containerView addSubview:toVC.view];
//5.添加动画
/*
拆分动画:
1.画出2个圆(大小圆的中心点是一致的,可参考PPT)
2.贝塞尔画圆
3.门板
*/
//获取两个界面的button
UIButton *btn;
ViewController *vc1;
BlackViewController *vc2;
if(self.isPush){
vc1 = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
vc2 = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
btn = vc1.blackBtn;
}else{
vc1 = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
vc2 = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
btn = vc2.redBtn;
}
[containerView addSubview:vc1.view];
[containerView addSubview:vc2.view];
//5.画小圆
// UIButton *btn = fromVC.blackBtn;
//bezierPathWithOvalInRect 根据矩形块画内切圆
UIBezierPath *smallPath = [UIBezierPath bezierPathWithOvalInRect:btn.frame];
CGPoint centerP = btn.center;
CGFloat radius;
CGFloat y = CGRectGetHeight(toVC.view.bounds) - CGRectGetMaxY(btn.frame) + CGRectGetHeight(btn.bounds)/2;
CGFloat x = CGRectGetWidth(toVC.view.bounds) - CGRectGetMaxX(btn.frame) + CGRectGetWidth(btn.bounds)/2;
if(btn.frame.origin.x > CGRectGetWidth(toVC.view.bounds)/2){
if(CGRectGetMaxY(btn.frame) < CGRectGetHeight(toVC.view.bounds)/2){
//第一象限
radius = sqrtf(btn.center.x * btn.center.x + y * y);
}else{
//第四象限
radius = sqrtf(btn.center.x * btn.center.x + btn.center.y * btn.center.y);
}
}else{
if (CGRectGetMidY(btn.frame) < CGRectGetHeight(toVC.view.frame)) {
//第二象限
radius = sqrtf(x*x + y*y);
}else{
//第三象限
radius = sqrtf(x*x + btn.center.y*btn.center.y);
}
}
//7.画大圆
UIBezierPath *bigPath = [UIBezierPath bezierPathWithArcCenter:centerP radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
CAShapeLayer *shapelayer = [[CAShapeLayer alloc]init];
if(self.isPush){
shapelayer.path = bigPath.CGPath;
}else{
shapelayer.path = smallPath.CGPath;
}
//9.添加蒙板
//不能直接ADD,因为我们在切换过程中是有轮廓;
//[toVC.view.layer addSublayer:shaperLayer];
//toVC.view.layer.mask = shaperLayer;
UIViewController *VC;
if(self.isPush){
VC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
}else{
VC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
}
VC.view.layer.mask = shapelayer;
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"path"];
if(self.isPush){
anim.fromValue = (id)smallPath.CGPath;
}else{
anim.fromValue = (id)bigPath.CGPath;
}
//这一句可以不写,它会以shaperLayer.path = bigPath.CGPath; 作为结束path
//anim.toValue = (id)bigPath.CGPath;
//动画时长要和转场时长保持一致
anim.duration = [self transitionDuration:transitionContext];
//11.在动画结束时,做处理.在代理方法中来实现
anim.delegate = self;
[shapelayer addAnimation:anim forKey:nil];
}
#pragma mark -- animationDelegate
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[_context completeTransition:YES];
if (_isPush) {
//取消蒙板
UIViewController *toVC = [_context viewControllerForKey:UITransitionContextToViewControllerKey];
toVC.view.layer.mask = nil;
}else{
//取消蒙板
UIViewController *toVC = [_context viewControllerForKey:UITransitionContextFromViewControllerKey];
toVC.view.layer.mask = nil;
}
}
@end
LcrTransition 遵循CAAnimationDelegate协议是为了动画结束后将蒙层撤去。
LcrTransition实现transitionDuration方法,设置动画时长,真正的动画操作实现在animateTransition方法中完成,根据isPush来判断当前是push还是pop,此处难点在于怎么计算出大圆的半径。
分析
这个转场效果,就是push时候小圆放大到大圆,pop时候从大圆缩小到小圆,大小圆圆心都是同一点,两个按钮中心点,那么最大需要画多大的圆就是我们需要求的半径。
注意
:按钮不一定只在右下角,四个角都有可能,所以我们可以根据按钮所在位置,将屏幕划分成四个象限。
利用勾股定理将最长边长度也就是大圆最大半径求出(图中以按钮在第四象限为例), r 半径为 btn.x^2 * btn.y^2 开根号的值。
将屏幕作为坐标轴的话,中心点作为点(0,0),屏幕划分为四个象限,那么勾股定理相对应地直角边x、y计算方式依次为:
1.
x = btn.center.x; y = CGRectGetHight(toVC.view.bounds) - CGRectGetMaxY(btn.frame) + CGRectGetHight(btn.bounds)/2;
2.
x = CGRectGetWidth(toVC.view.bounds) - CGRectGetMaxX(btn.frame) + CGRectGetWidth(btn.bounds)/2 ; y = CGRectGetHight(toVC.view.bounds) - CGRectGetMaxY(btn.frame) + CGRectGetHight(btn.bounds)/2;
3.
x = CGRectGetWidth(toVC.view.bounds) - CGRectGetMaxX(btn.frame) + CGRectGetWidth(btn.bounds)/2; y = btn.center.y;
4.
x = btn.center.x; y = btn.center.y;
最后计算半径r为勾股定理斜边长度。