iOS 自定义 Present 动画
在 国寿i动APP 的跑步功能里面,点击左下角的地图按钮,present到跑步地图页面。不是常规的从底部弹出,而是自定义转场动画的方式:
国寿I动 跑步模块.gif1. 在目标控制器中重写构造方法
设置控制器的 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 范围的内容,其他部分会被遮盖。
所以步骤是:
- 实例化一个 CAShapeLayer 对象
- 设置容器试图的 mask 属性为这个 CAShapeLayer 对象
- 为 CAShapeLayer 对象的 path 属性赋值一个贝塞尔路径
- 新建一个 CABasicAnimation 动画
- 设置动画时长,开始和结束路径,填充模式
- 添加动画到 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谢谢大家!