自定义切换页面的Present Transitions动画

————译自iOS Animation Tutorial: Custom View Controller Presentation Transitions,英文链接:https://www.raywenderlich.com/146692/ios-animation-tutorial-custom-view-controller-presentation-transitions-2

工程教学代码:https://koenig-media.raywenderlich.com/uploads/2016/10/Custom_View_Controller_starter.zip

工程完整代码:https://koenig-media.raywenderlich.com/uploads/2016/10/Custom_View_Controller_final.zip

背后的机制

UIKIts框架允许我们通过代理来自定义controllers之间的present动画,可以通过遵守UIViewControllerTransitioningDelegate来实现。

每次我们present一个新的controller的时候,UIKits会询问他的代理是否应该使用自定义的转场动画,这里是自定义转场动画实现的第一步,如下图:

图片发自简书App


UIKit调用animationController(forPresented:presenting:source:)代理方法来查看是否返回了一个遵守了UIViewControllerAnimatedTransitioning协议的对象,

如果这个方法返回的是空,UIKit就会使用默认的present动画,如果UIKit接收到了一个遵守了UIViewControllerAnimatedTransitioning协议的对象,那么UIKit就会使用那个对象作为present动画的控制器。

下面是UIKit使用自定义动画控制器前的几步其他操作:

图片发自简书App

UIKit首次询问他的动画控制器来获得动画的持续时间(以秒为单位)然后调用animateTransition(using:)方法,这是我们自定义动画开始真正发生的地方。

在animateTransition(using:)方法中,我们既可以访问到当前正展示在screen上的viewController,同时也可以访问到即将被present出来的viewController,你可以按照自己想要的对当前view和即将出现的新view进行渐隐渐现、缩放、旋转等等操作。

现在已经学习到了一点自定义present动画是如何工作的,那么就可以开始自己的了。

实现transitioning动画代理

既然代理的任务是控制产生真正动画的动画发生器,那么在写代理里面的方法前应该先为这个动画类创建一个存根(存根的意思是????、)。在项目里新建一个类,继承与NSObject,可以取名为PopAnimator,原文中使用swift语言写的,这里用OC,然后打开新建的类,让它遵守UIViewControllerAnimatedTransitioning协议、可以看到xcode给出警告,因为我们还没有实现它required的代理方法。

然后打开.m文件,向其中添加如下方法,

```

OC:- (NSTimeInterval)transitionDuration:(nullable id )transitionContext{

    return:0;

}

Swift:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {

 return 0

}

```

以上返回的0值只是起到一个占位的作用,可以在之后用真正的值来代替它。


现在向PopAnimator里继续添加方法:

```

oc:

-(void)animateTransition:(id )transitionContext{

}

swift:

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

}

以上的方法将会持有我们的动画代码,加上这个方法后就可以去掉Xcode提示的警告了。

现在既然已经已经拥有了基础的动画类,那么可以转移到viewController这边来实现代理方法了。

打开viewcontroller.h(或者viewcontroller.swift),然后然他遵守UIViewControllerTransitioningDelegate

这意味着我们的viewController遵守了专场协议。你可以将它的代理的方法添加到你的.m文件里

找到disTapImageView:(_:)这个方法,在方法的底部可以看到present出详情页的代码,herbDetails是一个新的viewCotroller实例,你可以将他的专场代理设置为当前的controller。

herbDetails.transitioningDelegate = self;

现在你每次present出这个新的详情页的时候,UIKit都会询问当前viewController是否拥有一个动画控制器。然而到现在我们viewController里还没有实现任何关于UIViewControllerTransitioningDelegate代理的方法。因此UIKit依然会使用系统默认的动画。

下一步就是创建我们的动画控制器了,然后在UIKit每次询问的时候返回给它用来执行动画。

OC:PopAnimator *transition = [[PopAnimator alloc]init];

Swift: let  transition = PopAnimator();

(不得不说,Swift确实够简洁,正像它的名字的含义一样)

这个PopAnimator的实例transition将会执行我们的转场动画了,我们只需要创建一个这个对象,因为我们每次的动画都是一样的,所以每次present一个controller的时候都可以用这个实例。

现在打开我们的ViewController,添加以下的代理方法。

OC:

- (nullableid )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{

//返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象。

}

Swift:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

 return transition

}

在这个方法里携带了几个参数以便于让你决定要不要返回一个自定义的动画。在这里我们只需要返回一个PopAnimator的实例即可。

现在我们已经为present出controller添加了代理的方法,但是我们该怎样dismiss掉呢


可以继续添加代理方法如下:

OC: - (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed;

Swift:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {

 return nil

}

上面的方法本质上做了和上一个同样的事情:你检查哪一个viewController被dismiss掉了,然后决定是否应该返回nil继续使用系统默认的动画,或者是返回一个自定义的动画来代替。在这里我们返回nil即可。

现在我们已经拥有了一个自定义的动画控制器来控制我们的自定义的转场动画。但是它怎样工作呢?如果这时候运行工程然后run,然后会发现没有发生任何事情,为什么?因为我们的自定义动画控制器里还没有任何代码呢,哈哈!

那么现在我们写PopAnimator里的代码

首先给类设置几个成员变量如下

OC:

{

    CGFloat   duration;(默认为1)

    BOOL  presenting;(默认为true)

    CGRect originFrame;(默认为CGRectZero)

}

Swift:

let duration = 1.0

var presenting = true

var originFrame = CGRect.zero


此处我们的duration可以用在几个不同的地方,比如当你想告诉UIKit我们的动画将会持续多久还有当你创建自己的动画时也会用到。

我们也定义了presenting 布尔值,来告诉动画控制器我们是在present还是在dismiss。我们之所以想记录这个值,因为我们正着可以用这个来present,反过来就可以用来dismiss了。

最后我们还使用了originFrame来保存图片的原始frame,因为我们会用到这个值给图片做一个动画使图片充满屏幕。所以应该在点击图片的时候应该记得保存图片的原始frame,然后把它传递给动画控制器。


现在我们可以将注意力移到UIViewControllerAnimatedTransitioning的代理方法中。将返回时间的默认0改为 return duration.

重复使用duration这个变量会让我们更加方便的体验我们的转场动画,我们可以通过简单的修改这个属性的值来使得动画运行的更快或者更慢。

设置我们的转场动画上下文

现在是时候往我们的animateTransition方法里添加点魔法了。这个方法拥有一个UIViewControllerConextTransitioning类型的参数,可以让我们获取到专场动画所需要的参数和viewControllers

在我们开始代码前,有必要知道什么是动画上下文(animation context)

当两个viewControllers之间的转场动画开始时,当前显示的view会被添加到一个transition container view上,新的试图控制器的view被创建了但是还没有被看到,就像下面的视图所展示的一样


图片发自简书App

因此我们的任务就是用animateTransitin()方法将新的view添加到transition  container 中

默认状态下,当发生专场动画时旧视图会从transition container中移除。如下图:


图片发自简书App

我们需要先创建一个简单的专场动画来看看他是如何工作的,然后我们再实现更加炫酷、更加复杂的动画

先添加一个带带渐隐渐现效果的transition

我们先以一个简单的渐隐动画开始,初步感受一下自定义的转场动画。

在animateTransition:方法中添加以下代码

OC:{

   UIView *containerView = transitionContext.containerView;

   UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];

}

Swift{

let containerView = transitionContext.containerView

let toView = transitionContext.view(forKey: .to)!

}

首先我们获取到了container view,我们的动画将会在它里面发生,然后我们还获取到了新的view,用toView来代表


这个转场上下文对象拥有两个非常好用的方法,我们可以用这些方法获取到一些有用的值


1、 - (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key方法。通过这个方法我们可以获取到新旧view,比如通过 UITransitionContextToViewKey可以获取到新的view,通过 UITransitionContextFromViewKey可以获取到旧的view.

2、 - (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;通过这个方法可以获取到新旧的视图控制器,通过 UITransitionContextFromViewControllerKey还有 UITransitionContextToViewControllerKey来获取

此时,我们同时拥有了container view和我们将要展示的view,接下来就是要将这个将要presented的view当做containerViewde 子视图添加到其上,并且以某种方式使其运动。

可以在animateTransition:方法里添加如下代码

OC:{

   toView.alpha = 0;

   [UIView animateWithDuration:1.0 animations:^{

       toView.alpha = 1.0;

   }completion:^(BOOL finished) {

       [transitionContext completeTransition:YES];

   }];

}

Swift:{

containerView.addSubview(toView)

toView.alpha = 0.0

UIView.animate(withDuration: duration,

animations: {

toView.alpha = 1.0

},

completion: { _ in

transitionContext.completeTransition(true)

}

)

}

需要注意的是我们要在动画结束的时候用上下文调用completeTranstion方法,这会告诉我们的UIKit我们的动画完成了。


现在运行我们的工程,点击其中的一张图片,会发现出现了渐显式的动画。

图片发自简书App


* 现在我们已经看到可以在animateTransition里做什么操作了,我们接下来添加点更有意思的东西。

添加一个带Pop效果的transition

现在我们要重构我们的代码来获得一个完全不一样的新的转场效果,先用如下的代码替换掉原来animateTransition()中的代码

OC代码

UIView *containerView = transitionContext.containerView;

UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];

UIView *herbView = presenting?toView:[transitionContext viewForKey:UITransitionContextFromViewControllerKey];

Swift代码


let containerView = transitionContext.containerView

let toView = transitionContext.view(forKey: .to)!

let herbView = presenting ? toView :

 transitionContext.view(forKey: .from)!

containerView是我们动画发生的地方,toView是将要present出来的页面。如果我们正在present新页面,那么herbView就恰恰是toView,否则的话,就从上下文中获取,无论是present还是dismiss,要确保herbView是我们要给添加动画的view。


当我们present一个详情页时,它会慢慢充满整个屏幕,当dismissed的时候,它会慢慢缩小回图片的原有尺寸。


接下来向其中继续添加如下代码:

OC

   CGRect initialFrame = presenting?originrame:herbView.frame;

   CGRect finalFrame = presenting?herbView.frame :originrame;

   CGFloat xScaleFactor = presenting?initialFrame.size.width/finalFrame.size.width:finalFrame.size.width/initialFrame.size.width;

   CGFloat yScaleFactor = presenting?initialFrame.size.height/finalFrame.size.height:finalFrame.size.height/originrame.size.height;

Swift


let initialFrame = presenting ? originFrame : herbView.frame

let finalFrame = presenting ? herbView.frame : originFrame


let xScaleFactor = presenting ?


 initialFrame.width / finalFrame.width :

 finalFrame.width / initialFrame.width


let yScaleFactor = presenting ?


 initialFrame.height / finalFrame.height :

 finalFrame.height / initialFrame.height

在上面的代码中,我们获取到动画的初始frame信息以及最终的frame信息,然后计算出我们在每个view上做动画时候的每条坐标轴的缩放比例。

现在我们需要仔细的计算新的view的位置,使得它恰巧从我们所点击的照片的上方出现,这会使得这个动画看起来是我们点击的这张照片变大了,最终填满了整个屏幕。

继续在animateTransition()中添加代码

OC

   CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScaleFactor, yScaleFactor);

   if (presenting) {

       herbView.transform = scaleTransform;

       herbView.center = CGPointMake(initialFrame.origin.x, initialFrame.origin.y);

       herbView.clipsToBounds = YES;

   }

Swift


let scaleTransform = CGAffineTransform(scaleX: xScaleFactor,

                                           y: yScaleFactor)


if presenting {

 herbView.transform = scaleTransform

 herbView.center = CGPoint(

   x: initialFrame.midX,

   y: initialFrame.midY)

 herbView.clipsToBounds = true

}


当我们present这个新的view的时候,我们设置了它的scale还有position。


现在我们在方法里添加最后几行代码

OC

   [containerView addSubview:toView];

   [containerView bringSubviewToFront:herbView];

   [UIView animateWithDuration:1.0f delay:0 usingSpringWithDamping:0.4 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{

       herbView.transform = presenting?CGAffineTransformIdentity:scaleTransform;

       herbView.center = CGPointMake(finalFrame.origin.x, finalFrame.origin.y);

   } completion:^(BOOL finished) {

       [transitionContext completeTransition:YES];

   }];


Swift


containerView.addSubview(toView)

containerView.bringSubview(toFront: herbView)


UIView.animate(withDuration: duration, delay:0.0,

 usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,

 animations: {

   herbView.transform = self.presenting ?

     CGAffineTransform.identity : scaleTransform

   herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)

 },

 completion:{_ in

   transitionContext.completeTransition(true)

 }

)

以上代码首先会将toView添加到container上,然后下一步,我们需要确保herbView是在屏幕的顶部的,因为这是我们唯一让之运动的view,应该记住的是当我们dismiss的时候,toView是我们的初始view,所以我们要把它加载其他任何东西的上面,如果不把它放在最上面的话我们的动画将会被隐藏掉。

然后我们可以开始我们的动画了,使用带spring 效果的动画会给它加上一点弹簧的效果。


在我们的动画表述中,我们改变了herbView的transform和position,当present的时候,我们使其从底部的一个小尺寸变大到充满整个屏幕。


此刻我们已经将新的viewController搬到屏幕上来了,并且是在所点击的图片的上方,我们在初始frame和最终的frame之间做动画,然后动画结束后,我们调用completeTransition方法向UIKit提交反馈。


然而,此时并不完美,但是只要我们稍加留意一下我们动画的几个粗糙的地方,然后就会变成我们想要的样子了。


现在我们的动画是从屏幕的左上角开始的,因为originFrame的默认尺寸是(0,0),我们还没有给它设置什么值。


打开ViewController.Swift,向animationController(forPresented:)方法上面添加如下代码

Swift


transition.originFrame =

selectedImage!.superview!.convert(selectedImage!.frame, to: nil)


transition.presenting = true

selectedImage!.isHidden = true

注意这里主要就是设置动画的初始位置,也就是我们所点击的图片的位置,OC代码的话保持逻辑一致即可。

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

推荐阅读更多精彩内容