iOS开发 -- 知识点杂记

iOS - 自定义转场动画

2022-11-18  本文已影响0人  Lcr111

前言

虽然在项目中自定义转场动画用的相对较少,但是相关的知识点还是需要掌握的,下面我们借助一个例子来实现下转场动画。直接看效果:


转场动画001

思路

  1. 实现相关协议
  2. 在协议中实现动画

代码

首先创建工程,给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时候从大圆缩小到小圆,大小圆圆心都是同一点,两个按钮中心点,那么最大需要画多大的圆就是我们需要求的半径。
注意:按钮不一定只在右下角,四个角都有可能,所以我们可以根据按钮所在位置,将屏幕划分成四个象限。

转场动画002
利用勾股定理将最长边长度也就是大圆最大半径求出(图中以按钮在第四象限为例), 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为勾股定理斜边长度。

上一篇 下一篇

猜你喜欢

热点阅读