iOS自定义转场动画-1

像我们平常用的最多的就是presentViewController:animated:completion:和对应的dismissViewControllerAnimated:completion:来实现展示视图和隐藏视图。最近看了官方的Demo,对于我这种小菜鸟着实下了一番功夫才搞明白。详细代码可以在这里下载,如果觉得有用,可以随手star一下。

动画效果 只实现了其中的一部分

Untitled.gif

第一个效果(Cross Dissolve)

. 1 首先当前的控制器需要遵守UIViewControllerTransitioningDelegate点击command进去可以看到有5个代理方法,由于当前的动画没有涉及到用户的交互情况,所以只实现了前两个方法,它返回的是一个遵守了UIViewControllerAnimatedTransitioningid对象,UIViewControllerAnimatedTransitioning它是用来控制动画的持续时间和展示逻辑。

#import <UIKit/UIKit.h>
@interface CrossDissolveAnimator : NSObject<UIViewControllerAnimatedTransitioning>
@end
#import "CrossDissolveAnimator.h"
@implementation CrossDissolveAnimator
//动画持续的时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
    return 0.35;
}
//动画相关的参数
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    //动画相关联的两个控制器
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //containerView:本人理解是如果没有动画,则为fromView,如果在动画的过程中,则为toView
    UIView *containerView = transitionContext.containerView;
    UIView *fromView;
    UIView *toView;
    
    //iOS8引入了viewForKey方法,系统会优先访问这个方法,另外viewForKey这个方法有可能会返回nil,尽量不直接访问controller的view属性,就比如第三个demo从底部弹出的动画,给presentedViewController的view添加了圆角阴影等效果,viewForKey访问到的是所有的view 而presentedViewController的view只能访问到其中的一个子视图
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    }else{
        fromView = fromViewController.view;
        toView = toViewController.view;
    }
    
    fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
    toView.frame = [transitionContext finalFrameForViewController:toViewController];
    
    fromView.alpha = 1.0f;
    toView.alpha = 0.0f;
    
    //在present和,dismiss时,必须将toview添加到视图层次中
    [containerView addSubview:toView];
    NSTimeInterval transitionDurating = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:transitionDurating animations:^{
        fromView.alpha = 0.0f;
        toView.alpha = 1.0f;
    } completion:^(BOOL finished) {
        BOOL wasCancelled = [transitionContext transitionWasCancelled];
        [transitionContext completeTransition:!wasCancelled];
    }];
}
@end

以上为核心的代码,这里借用下别人的图片来注明一下用到的fromViewtoView


另外需要注意,modalPresentationStyle需要是UIModalPresentationFullScreen,fullCustom的本质区别是full会移除fromView,但是Custom却不会。

第二个效果(Swipe)

首先先分析,它既可以点击自定义跳转,其次又可以添加手势来滑动,所以不仅要实现UIViewControllerTransitioningDelegate中的方法而且我们还要计算在手势拖动中百分比的控制,官方已经封装好了我们只需要实现UIPercentDrivenInteractiveTransition协议就好了。并且当前的转场动画是交互式的动画,判断的依据就是可以手势滑动。
.1 自定义一个类遵守UIViewControllerTransitioningDelegate通过动画我们可以看出,当present的时候是从屏幕的右边开始,dismiss从左边开始消失,所以我们要计算两个toView的frame值
首先实现点击Button来转场

#import <UIKit/UIKit.h>
@interface SwipeTransitionAnimatar : NSObject<UIViewControllerAnimatedTransitioning>
@property(nonatomic,assign) UIRectEdge edge;//用来判断是左滑还是右滑
- (instancetype)initWithTargetEdge:(UIRectEdge)dege;
@end
#import "SwipeTransitionAnimatar.h"

@implementation SwipeTransitionAnimatar
- (instancetype)initWithTargetEdge:(UIRectEdge)dege{
    if (self = [super init]) {
        _edge = dege;
    }
    return self;
}
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
{
    return 0.35f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = transitionContext.containerView;
    UIView *fromView;
    UIView *toView;
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    }else{
        fromView = fromViewController.view;
        toView = toViewController.view;
    }
    // isPresenting用于判断当前是present还是dismiss
    BOOL isPresenting = (toViewController.presentingViewController == fromViewController);
    
    CGRect fromFrame = [transitionContext initialFrameForViewController:fromViewController];
    CGRect toFrame = [transitionContext finalFrameForViewController:toViewController];
    
   __block  CGVector offset;//用于计算toView的位置
    //定义一个二维矢量运动的方向,重力方向默认的是(0.f,1.f),dx为-1.0f时向左运动,dy为-1.0时向上运动
     if (self.edge == UIRectEdgeLeft){
        offset = CGVectorMake(1.f, 0.f);//从左边屏幕开始滑动表示dismiss时
    }else if (self.edge == UIRectEdgeRight){
        offset = CGVectorMake(-1.f, 0.f);//从右边开始滑动表示present时
    }else{
        NSAssert(NO, @"targetEdge must be one of UIRectEdgeLeft, or UIRectEdgeRight.");
    }
    
    //根据当前是present还是dismiss来计算好toView的初始位置以及结束位置
    if (isPresenting) {
        fromView.frame = fromFrame;
        toView.frame = CGRectOffset(toFrame, toFrame.size.width*offset.dx*-1, toFrame.size.height*offset.dy*-1);
    }else{
        fromView.frame = fromFrame;
        toView.frame = toFrame;
    }

    if (isPresenting) {
        [containerView addSubview:toView];
    }else{
        [containerView insertSubview:toView belowSubview:fromView];
    }
    NSTimeInterval transitionDurating = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:transitionDurating animations:^{
        if (isPresenting) {
            toView.frame = toFrame;
        }else{
            fromView.frame = CGRectOffset(toFrame, toFrame.size.width*offset.dx, toFrame.size.height*offset.dy);
        }
    } completion:^(BOOL finished) {
       BOOL wasCancelled = [transitionContext transitionWasCancelled];
        if (wasCancelled)
            [toView removeFromSuperview];
        [transitionContext completeTransition:!wasCancelled];
    }];
}
@end

.2 自定义动画百分比控制器来实现手势滑动的时候所占的百分比

#import "SwipePercentInteractionController.h"

@interface SwipePercentInteractionController ()
@property(nonatomic,strong)id<UIViewControllerContextTransitioning> transitionContext;//用来存储实时的transitionContext
@property(nonatomic,strong)UIScreenEdgePanGestureRecognizer *pan;
@property(nonatomic,assign) UIRectEdge edge;
@end

@implementation SwipePercentInteractionController
- (instancetype)initWithGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)pan edgeForDragging:(UIRectEdge)edge{
    if (self = [super init]) {
        _pan = pan;
        _edge = edge;
        [_pan addTarget:self action:@selector(pan:)];
    }
    return self;
}
- (instancetype)init{
    @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Use -initWithGestureRecognizer:edgeForDragging:" userInfo:nil];
}
- (void)dealloc{
    [self.pan removeTarget:self action:@selector(pan:)];
}
//不应该缓存transitionContext,而是动态的获取,这样保证拿到的始终是最新的,最正确的消息
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    self.transitionContext = transitionContext;
    [super startInteractiveTransition:transitionContext];
}
//根据当前手势触摸的点从而计算出偏移的百分比
- (CGFloat)percentForGuesture:(UIScreenEdgePanGestureRecognizer *)pan{
    UIView *transitionContainerView = self.transitionContext.containerView;
    CGPoint location = [pan locationInView:transitionContainerView];
    CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
    if (self.edge == UIRectEdgeLeft) {
        return location.x/width;
    }else if (self.edge == UIRectEdgeRight){
        return (width - location.x)/width;
    }else
        return 0.f;
}
//手势滑动触发方法
- (void)pan:(UIScreenEdgePanGestureRecognizer *)pan{
    switch (pan.state) {
        case UIGestureRecognizerStateBegan:
            break;
        case UIGestureRecognizerStateChanged://手势滑动,更新百分比
            [self updateInteractiveTransition:[self percentForGuesture:pan]];
            break;
        case UIGestureRecognizerStateEnded: // 滑动结束,判断是否超过一半,如果是则完成剩下的动画,否则取消动画
            if ([self percentForGuesture:pan] >= 0.5f) {
                [self finishInteractiveTransition];
            }else{
                [self cancelInteractiveTransition];
            }
            break;
            
        default:
            [self cancelInteractiveTransition];
            break;
    }
}
@end

.3 实现UIViewControllerTransitioningDelegate代理方法

@implementation SwipeTransitioningDelegate

//   === 点击Button的时候走下面这两个方法  ===
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
    return [[SwipeTransitionAnimatar alloc]initWithTargetEdge:self.edge];
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
    return [[SwipeTransitionAnimatar alloc]initWithTargetEdge:self.edge];
}

//=== 用手势滑动的时候走这两个方法 ====
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator{
    if (self.pan) {
        return [[SwipePercentInteractionController alloc]initWithGestureRecognizer:self.pan edgeForDragging:self.edge];
    }
    else
        return nil;
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{
    if (self.pan) {
        return [[SwipePercentInteractionController alloc]initWithGestureRecognizer:self.pan edgeForDragging:self.edge];
    }
    else
        return nil;
}

@end

第三个效果(Custom Presentation)

分析动画我们可以知道,presented出来的view自定义的有阴影,有圆角等而且frame并不是整个屏幕,这种比较彻底的自定义的presentedView我们可以使用转场协调器UIPresentationController来实现,它在转场动画的执行过程中一直存在。UIPresentationController具有以下的功能:
1.设置presentedViewController的视图大小
2.添加自定义视图来改变presentedView的外观
3.为任何自定义的视图提供转场动画效果
4.根据size class进行响应式布局
由于```UIPresentationController``是一个基类,所以我们需要自定义实现一个类来集成它。该类主要的四个方法,顾名思义,就是在转场动画将要开始(在这里定义视图层级结构和frame),开始,将要结束,结束时的操作。

- (void)presentationTransitionWillBegin;
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;
#import "CustomPresentatitionTransitioning.h"

@interface CustomPresentatitionTransitioning ()<UIViewControllerAnimatedTransitioning>
@property(nonatomic,strong)UIView *dimmingView;//后面的视图遮罩
@property(nonatomic,strong)UIView *presentationWrappingView;//添加动画效果的view
@end

@implementation CustomPresentatitionTransitioning
//  =============  UIPresentationController初始化 方法 ==================
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController{
    if (self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController]) {
        //Custom不会移除presentingView
        presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
    }
    return self;
}
- (UIView *)presentedView{
    return self.presentationWrappingView;
}
//转场将要开始的时候,布局视图的层级结构和frame
- (void)presentationTransitionWillBegin{
    //可以更改自身的视图层级,添加额外的效果(阴影,圆角)
    UIView *presentedViewControllerView = [super presentedView];
    presentedViewControllerView.autoresizingMask = UIViewAutoresizingFlexibleWidth |UIViewAutoresizingFlexibleHeight;
  
    /*运行的效果是由三个View叠加在一起形成的效果层级关系是
     presentationWapperView(负责阴影)
            presentationRoundedCornerView(负责圆角)
                   presentedViewControllerWrapperView
                         presentedViewControllerView
     
     */
    UIView *presentationWapperView = [[UIView alloc]initWithFrame:self.frameOfPresentedViewInContainerView];
    presentationWapperView.layer.shadowOpacity = 0.44f;//阴影不透明
    presentationWapperView.layer.shadowRadius = 13.f;
    presentationWapperView.layer.shadowOffset = CGSizeMake(0, -6.f);
    self.presentationWrappingView = presentationWapperView;
    
    UIView *presentationRoundedCornerView = [[UIView alloc]initWithFrame:UIEdgeInsetsInsetRect(presentationWapperView.bounds, UIEdgeInsetsMake(0, 0, -16, 0))];
    presentationRoundedCornerView.autoresizingMask =  UIViewAutoresizingFlexibleWidth |UIViewAutoresizingFlexibleHeight;
    presentationRoundedCornerView.layer.cornerRadius = 16;
    presentationRoundedCornerView.layer.masksToBounds = YES;
    
    UIView *presentedViewControllerWrapperView = [[UIView alloc]initWithFrame:UIEdgeInsetsInsetRect(presentationRoundedCornerView.bounds, UIEdgeInsetsMake(0, 0, 16, 0))];
    presentedViewControllerWrapperView.autoresizingMask = UIViewAutoresizingFlexibleWidth |UIViewAutoresizingFlexibleHeight;
    presentedViewControllerView.frame = presentedViewControllerWrapperView.bounds;
    //层级关系
    [presentationWapperView addSubview:presentationRoundedCornerView];
    [presentationRoundedCornerView addSubview:presentedViewControllerWrapperView];
    [presentedViewControllerWrapperView addSubview:presentedViewControllerView];
    
    //添加一个dimmingview(昏暗)在presentationWrapperView的后面,然后再添加self.presentedView 这样的话dimmingview都出现在presentedView的后面
    UIView *dimmingView = [[UIView alloc]initWithFrame:self.containerView.bounds];
    dimmingView.backgroundColor = [UIColor blackColor];
    dimmingView.opaque = NO;//透明
    dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [dimmingView addGestureRecognizer:[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)]];
    self.dimmingView = dimmingView;
    [self.containerView addSubview:dimmingView];
    
    
    id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    self.dimmingView.alpha = 0.f;
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
        self.dimmingView.alpha = 0.5;
    } completion:nil];
}
//转场完成时清除多余的视图
- (void)presentationTransitionDidEnd:(BOOL)completed{
    //如果present没有完成,把dimmingView和wrappingView都清空,这些临时视图用不到了
    if (completed == NO) {
        self.presentationWrappingView = nil;
        self.dimmingView = nil;
    }
}
- (void)dismissalTransitionWillBegin{
    id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
        self.dimmingView.alpha = 0.f;
    } completion:nil];
}
- (void)dismissalTransitionDidEnd:(BOOL)completed{
    if (completed) {
        self.presentationWrappingView = nil;
        self.dimmingView = nil;
    }
}
// =============  如果当前DidChange的视图是presentedViewController,UIContentContainer重新布局子视图 ==========
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container{
    [super preferredContentSizeDidChangeForChildContentContainer:container];
    if (container == self.presentedViewController) {
        [self.containerView setNeedsLayout];
    }
}
- (CGSize)sizeForChildContentContainer:(id<UIContentContainer>)container withParentContainerSize:(CGSize)parentSize{
    if (container == self.presentedViewController) {
        return ((UIViewController *)container).preferredContentSize;
    }else
        return [super sizeForChildContentContainer:container withParentContainerSize:parentSize];
    
}
//重写系统方法计算presentedViewController的大小
- (CGRect)frameOfPresentedViewInContainerView{
    CGRect containerViewBounds = self.containerView.bounds;
    CGSize presentedViewContentSize = [self sizeForChildContentContainer:self.presentedViewController withParentContainerSize:containerViewBounds.size];
    
    CGRect presentedViewControllerFrame = CGRectMake(containerViewBounds.origin.x, CGRectGetMaxY(containerViewBounds) - presentedViewContentSize.height, presentedViewContentSize.width, presentedViewContentSize.height);
    return presentedViewControllerFrame;
}

- (void)containerViewWillLayoutSubviews{
    [super containerViewWillLayoutSubviews];
    self.dimmingView.frame = self.containerView.bounds;
    self.presentationWrappingView.frame = self.frameOfPresentedViewInContainerView;
}

- (IBAction)tap:(UITapGestureRecognizer *)tap{
    
    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}

@end

总结:

其实实现自定义转场动画无非就是以下几步:
1.设置将要跳转的目的控制器presentedViewControllertransitationingDelegate
2.充当代理的对象可以是 self(比较简单的动画)也可以是自定义的一个实现了UIViewControllerTransitioningDelegate的类。
3.在2的该类中实现指定的代理方法返回对应的Animater,Animater是实现了UIViewControllerAnimatedTransitioning的对象,转场动画的所有的核心代码都是在该Animater中实现

自定义动画还有很多需要学习研究的地方,远远不止这些,以上仅为自己总结的一点点心得。还有几个转场动画有时间继续更新!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容