iOS 自定义 Present 动画

2019-02-26  本文已影响0人  蒋伟_iOS

在 国寿i动APP 的跑步功能里面,点击左下角的地图按钮,present到跑步地图页面。不是常规的从底部弹出,而是自定义转场动画的方式:

国寿I动 跑步模块.gif

1. 在目标控制器中重写构造方法

设置控制器的 modalPresentationStyle属性为UIModalPresentationCustom
实例化一个 动画代理类 JWAnimators,并赋值给 transitioningDelegate 属性

@implementation PresentViewController {
    
    JWAnimators *_circleAnimator;
    
}

- (instancetype)init {
    
    self = [super init];
    if (self) {
        self.modalPresentationStyle = UIModalPresentationCustom;
        _circleAnimator = [[JWAnimators alloc] init];;
        self.transitioningDelegate = _circleAnimator;
    }
    return self;
}

这里有个要注意的地方是 动画代理对象不能像下面的写法,这样init执行完成以后动画代理会被释放,动画不生效,必须跟控制器强引用。

  self.modalPresentationStyle = UIModalPresentationCustom;
  JWAnimators *animator = [[JWAnimators alloc] init];
  self.transitioningDelegate = animator;

还有个modalTransitionStyle 属性,也控制ViewController以模态方式展现时的动画方式,可选值如下,有兴趣也可以看看非默认值的效果

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
    UIModalTransitionStyleCoverVertical = 0,
    UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,
    UIModalTransitionStyleCrossDissolve,
    UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
};

2. 告诉动画代理谁来提供模态动画

控制器的transitioningDelegate属性需要遵守UIViewControllerTransitioningDelegate协议

@property (nullable, nonatomic, weak) id <UIViewControllerTransitioningDelegate> transitioningDelegate 
NS_AVAILABLE_IOS(7_0);

所以 JWAnimators 作为动画代理需要在 JWAnimators.h 中遵守UIViewControllerTransitioningDelegate协议

#import <UIKit/UIKit.h>

@interface JWAnimators : NSObject <UIViewControllerTransitioningDelegate>

@end

以下是UIViewControllerTransitioningDelegate协议内容:

@protocol UIViewControllerTransitioningDelegate <NSObject>

@optional
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

@end

我们在 JWAnimators.m 中实现协议中前两个方法, 并记录下标记animatorsType

typedef NS_ENUM(NSInteger, JWAnimatorsType) {
    JWAnimatorsPresent = 0,
    JWAnimatorsDismiss = 1
};

@implementation JWAnimators {
    
    //当前是展现还是消失
    JWAnimatorsType _animatorsType;
    
}
//告诉控制器谁来提供展现动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    _animatorsType = JWAnimatorsPresent;
    return self;
}

//告诉控制器谁来提供消失动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    _animatorsType = JWAnimatorsDismiss;
    return self;
}

3. 处理动画过渡管理上下文

遵守UIViewControllerAnimatedTransitioning协议

@interface JWAnimators() <UIViewControllerAnimatedTransitioning>

@end

实现协议中的动画时长方法

//动画时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 5.0;
}

这一步是核心!!!
实现协议中的animateTransition:方法,方法的参数为动画过渡管理上下文transitionContext
可以拿到容器试图,在上面可以发挥你的想象,实现各种动画方法。
分步讲解见代码哪注释

//实现动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    // 获取容器试图容器视图
    UIView *containerView = [transitionContext containerView];

    // 根据之前记录的_animatorsType属性,来确定执行动画的到底是 toView 还是 fromView
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *view;

    if (_animatorsType == JWAnimatorsPresent) {
        view = toView;
    } else {
        view = fromView;
    }

    if (_animatorsType == JWAnimatorsPresent) {
        [containerView addSubview:view];
    }

    // 这个就是在视图上进行自定义动画,在下节中讲解
    [self circleAnimWithView:view];

    //自定义全局变量,见下文
    _transitionContext = transitionContext;
    
}

自定义一个全局的动画上下文去接受这个上下文,在动画中和动画结束时会使用到

@implementation JWAnimators {
    
    JWAnimatorsType _animatorsType;
    __weak id <UIViewControllerContextTransitioning> _transitionContext;
    
}

4. 为容器视图添加动画

这里要说一下 正常动画都是目标视图进行动画,但是看这个效果 是目标视图从无到有一点点展示,这时候就需要你了解一个叫 CAShapeLayer 的东西。
你把一个 CAShapeLayer 的对象设置成 CALayer 对象的 mask 属性,就会产生这种遮盖效果。也就是 CALayer 只会显示 CAShapeLayer 范围的内容,其他部分会被遮盖。
所以步骤是:

- (void)circleAnimWithView:(UIView *)view {

    //1. 实例化一个 CAShapeLayer 对象
    CAShapeLayer *layer = [CAShapeLayer layer];

    //2.设置容器试图的 mask 属性为这个 CAShapeLayer 对象
    view.layer.mask = layer;

     //3.为 CAShapeLayer 对象的 path 属性赋值一个贝塞尔路径
    CGFloat radius = 50;
    CGFloat viewWidth = view.bounds.size.width;
    CGFloat viewHeight = view.bounds.size.height;
    CGRect rect = CGRectMake(60, viewHeight - 60 - 50, radius, radius);

    UIBezierPath *beginPath = [UIBezierPath bezierPathWithOvalInRect:rect];
    layer.path = beginPath.CGPath;

    //4.新建一个 CABasicAnimation 动画
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"path"];

    //5.设置动画时长,开始和结束路径,填充模式
    CGFloat maxRadius = sqrt((viewWidth - 85) * (viewWidth - 85) + (viewHeight - 85) * (viewHeight - 85));

    CGRect endRect = CGRectInset(rect, -maxRadius, -maxRadius);
    UIBezierPath *endPath = [UIBezierPath bezierPathWithOvalInRect:endRect];

    anim.duration = [self transitionDuration:_transitionContext];
    if (_animatorsType == JWAnimatorsPresent) {
        anim.fromValue = (__bridge id _Nullable)(beginPath.CGPath);
        anim.toValue = (__bridge id _Nullable)(endPath.CGPath);
    } else {
        anim.fromValue = (__bridge id _Nullable)(endPath.CGPath);
        anim.toValue = (__bridge id _Nullable)(beginPath.CGPath);
    }

    anim.fillMode = kCAFillModeForwards;
    anim.removedOnCompletion = NO;
    anim.delegate = self;

    //6.添加动画到 CAShapeLayer 层
    [layer addAnimation:anim forKey:nil];
    
}

5. 通知上下文完成转场动画

这个通知的时机应该是动画结束的时候,所以我们设置动画的代理为self,实现animationDidStop: finished:代理方法,然后用刚才定义的全局变量_transitionContext完成转场。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    [_transitionContext completeTransition:YES];
}

Demo地址:https://github.com/muyan091115/JWAnimators.git

Demo效果如下:

Demo.gif

谢谢大家!

上一篇 下一篇

猜你喜欢

热点阅读