内容简介:在由图中可以看出要完成自定义转场动画,必须遵从先来看看网易严选App的转场效果,可以看出当前页面想要
在 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];
}
复制代码
Push和Pop动画
UIViewControllerAnimatedTransitioning协议
这里新建一个 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;
}
}
复制代码
Push实现
- (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];
}
}];
}
复制代码
Pop实现
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;
}
}];
}
复制代码
UINavigationControllerDelegate代理方法
完成 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;
}
复制代码
看看实现效果
Present动画和Dismiss动画
Present实现
- (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];
}
}];
}
复制代码
Dismiss实现
- (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];
}
}];
}
复制代码
UIViewControllerTransitioningDelegate代理方法
回到 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];
}
复制代码
手势动画
UIPercentDrivenInteractiveTransition创建手势类
新建一个手势类 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;
}
}
复制代码
UIViewControllerTransitioningDelegate代理方法
回到 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的转场效果,第一次看的时候,觉得很酷炫,现在了解了转场的核心后,觉得不那么难了,有时间再把它的效果写出来吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。