博客:TransitionAnimation 自定义转场动画
在iOS 7
之后,苹果就开放了自定义转场的相关api
,现在都快iOS 12
了,一直都没有好好研究转场动画,一个是之前没有重视,觉得花里胡哨的,另外一个是所做的项目中没有这样的转场动画需求。这里说的转场动画和上一篇CAAnimation 系统动画中CATransition
动画不是一个概念,上一篇指的是单个View的转场特效,这里指的是整个控制器的转场特效。其实写上篇文章的目前也是为今天打下铺垫,复杂的转场效果也是由单个动画来组成的。
由图中可以看出要完成自定义转场动画,必须遵从UIViewControllerAnimatedTransitioning
协议,协议中有两个必须实现的方法一个是返回转场时间,一个是具体转场的实现。文章会结合5个最常用的动画场景来说明转场动画。
先来看看网易严选App的转场效果,可以看出当前页面想要Push
其他的页面的时候,当前页面会下沉同时其他页面从右边平移至左边。Present
页面的时候,当前页面也会下沉,目标视图从底部弹出。
来看代码,在ViewController
里面有两个按钮,分别是Push
出SecondVC
和Present
出ThirdVC
。
- (IBAction)pushBtnClick:(id)sender
{
SecondViewController * vc = [[SecondViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
[self presentViewController:vc animated:YES completion:nil];
}
这里新建一个AnimatedTransitioningObject
类,然后要遵循UIViewControllerAnimatedTransitioning
协议。这个为了方便,把Push、Pop、Present、Dismiss
这四个效果写在一起,用枚举来区分,当然也可以把每种动画效果单独用一个AnimatedTransitioningObject
类来实现。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger,TransitionAnimationObjectType) {
TransitionAnimationObjectType_Push,
TransitionAnimationObjectType_Pop,
TransitionAnimationObjectType_present,
TransitionAnimationObjectType_Dismiss
};
@interface TransitionAnimationObject : NSObject <UIViewControllerAnimatedTransitioning>
@property (nonatomic,assign) TransitionAnimationObjectType type;
- (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;
+ (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;
@end
来看看两个必须实现的方法,在返回转场时间里也可以根据type
来返回不同的动画时间,这里统一返回0.5秒。pushAnimateTransition
里面实现Push
效果转场。
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
switch (_type) {
case TransitionAnimationObjectType_Push:
[self pushAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_Pop:
[self popAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_present:
[self presentAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_Dismiss:
[self dismissAnimateTransition:transitionContext];
break;
default:
break;
}
}
- (void)pushAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//获取目标View(secondVC.view) 和 来源View(ViewController.view)
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//这里截图做动画 隐藏来源View
UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
fromView.hidden = YES;
//将需要做转场的View按照顺序添加到转场容器中
UIView * containerView = [transitionContext containerView];
[containerView addSubview:tempView];
[containerView addSubview:toView];
CGFloat width = containerView.frame.size.width;
CGFloat height = containerView.frame.size.height;
//设置目标View的初始位置
toView.frame = CGRectMake(width, 0, width, height);
//开始做动画
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
toView.transform = CGAffineTransformMakeTranslation(-width, 0);
} completion:^(BOOL finished) {
//这里要标记转场成功 假如不标记 系统会认为还在转场中 无法交互
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
//转场失败 也要做相应的处理
if ([transitionContext transitionWasCancelled])
{
fromView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
Push
和Pop
是相对的关系,所以在Pop
动画中,目标视图和来源视图互换身份,实现也是用CGAffineTransformIdentity
来还原Push
动画即可。
- (void)popAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//注意这里是还原 所以toView和fromView 身份互换了 toView是ViewController.view
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//获取相应的视图
UIView * containerView = [transitionContext containerView];
UIView * tempView = [[containerView subviews] firstObject];
//在fromView 下面插入toView 不然回来的时候回黑屏
[containerView insertSubview:toView belowSubview:fromView];
//将动画直接还原即可
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
tempView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
//标记转场
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
//转场成功的处理
if (![transitionContext transitionWasCancelled])
{
[tempView removeFromSuperview];
toView.hidden = NO;
}
}];
}
完成AnimatedTransitioningObject
类后,再返回ViewController
中,ViewController
要遵循UINavigationBarDelegate
和UIViewControllerTransitioningDelegate
,把SecondVC
的transitioningDelegate
设置为自己。然后根据不同的operation
,来返回不同的动画实现。
@interface ViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate>
- (IBAction)pushBtnClick:(id)sender
{
SecondViewController * vc = [[SecondViewController alloc] init];
vc.transitioningDelegate = self;
[self.navigationController pushViewController:vc animated:YES];
}
#pragma mark - Push && Pop
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush)
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Push];
}
else if (operation == UINavigationControllerOperationPop)
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Pop];
}
return nil;
}
- (void)presentAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//获取目标View(ThirdVC.view) 和 来源View(ViewController.view)
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//截图做动画
UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
tempView.frame = fromView.frame;
fromView.hidden = YES;
//按照顺序假如转场动画容器中
UIView * containerView = [transitionContext containerView];
[containerView addSubview:tempView];
[containerView addSubview:toView];
CGFloat width = containerView.frame.size.width;
CGFloat height = containerView.frame.size.height;
//设置toView的初始化位置 在屏幕底部
toView.frame = CGRectMake(0, height, width, 400);
//做转场动画
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
toView.transform = CGAffineTransformMakeTranslation(0, -400);
} completion:^(BOOL finished) {
//转场结束后一定要标记 否则会认为还在转场 无法交互
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
if ([transitionContext transitionWasCancelled])
{
//转场失败
fromView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
- (void)dismissAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//dismiss的时候 fromVC和toVC身份倒过来了
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//containerView里面的顺序也倒过来了 截图在最上面
UIView * containerView = [transitionContext containerView];
UIView * tempView = [[containerView subviews] firstObject];
//做还原动画就可以了
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
tempView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
//转场结束后一定要标记 否则会认为还在转场 无法交互
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
if (![transitionContext transitionWasCancelled])
{
//转场成功
toView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
回到ViewController
,把ThirdVC
的transitioningDelegate
设置为自己,然后在代理方法中自定类型。
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
vc.transitioningDelegate = self;
[self presentViewController:vc animated:YES completion:nil];
}
#pragma mark - Present && Dismiss
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_present];
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Dismiss];
}
新建一个手势类GestureObject
继承自UIPercentDrivenInteractiveTransition
,addGestureToViewController
是给目标控制器添加手势。
#import <UIKit/UIKit.h>
@interface GestureObject : UIPercentDrivenInteractiveTransition
//判断是交互的手势
@property (nonatomic,assign) BOOL interacting;
- (void)addGestureToViewController:(UIViewController *)viewController;
@end
然后再手势的状态之间来判断是否执行动画,这里是判断手势偏移量超过屏幕一半的高度就生效,执行相关动画,否则还原动画。
- (void)handleGesture:(UIPanGestureRecognizer *)ges
{
CGPoint point = [ges translationInView:ges.view];
switch (ges.state) {
case UIGestureRecognizerStateBegan:
{
self.interacting = YES;
[self.targetVC dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat fraction = point.y / ges.view.frame.size.height;
//限制在0和1之间
fraction = MAX(0.0, MIN(fraction, 1.0));
self.shouldComplete = fraction > 0.5;
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
self.interacting = NO;
if (!self.shouldComplete || ges.state == UIGestureRecognizerStateCancelled)
{
//还原动画
[self cancelInteractiveTransition];
}
else
{
//完成动画
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
回到ViewController
中,在Present
出ThirdVC
的时候添加手势,在代理方法interactionControllerForDismissal
中指定手势。
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
vc.transitioningDelegate = self;
[self.gestureObject addGestureToViewController:vc];
[self presentViewController:vc animated:YES completion:nil];
}
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
return self.gestureObject.interacting ? self.gestureObject : nil;
}
Push
、Pop
、Present
、Dismiss
、手势动画都讲解完了,可以看出,自定义转场大致的步骤是
- 根据
viewForKey
来获取转场上下文 - 将要转场的视图加入转场容器中
- 做出转场动画
- 标记转场成功的状态,根据状态做相应的处理
理解了这些,再复杂的转场动画都能一步步分解出来,下面是格瓦拉App的转场效果,第一次看的时候,觉得很酷炫,现在了解了转场的核心后,觉得不那么难了,有时间再把它的效果写出来吧。