自定義切換頁(yè)面的Present Transitions動(dòng)畫

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

工程教學(xué)代碼: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

背后的機(jī)制

UIKIts框架允許我們通過(guò)代理來(lái)自定義controllers之間的present動(dòng)畫,可以通過(guò)遵守UIViewControllerTransitioningDelegate來(lái)實(shí)現(xiàn)。

每次我們present一個(gè)新的controller的時(shí)候,UIKits會(huì)詢問(wèn)他的代理是否應(yīng)該使用自定義的轉(zhuǎn)場(chǎng)動(dòng)畫,這里是自定義轉(zhuǎn)場(chǎng)動(dòng)畫實(shí)現(xiàn)的第一步,如下圖:

圖片發(fā)自簡(jiǎn)書App


UIKit調(diào)用animationController(forPresented:presenting:source:)代理方法來(lái)查看是否返回了一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象,

如果這個(gè)方法返回的是空,UIKit就會(huì)使用默認(rèn)的present動(dòng)畫,如果UIKit接收到了一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象,那么UIKit就會(huì)使用那個(gè)對(duì)象作為present動(dòng)畫的控制器。

下面是UIKit使用自定義動(dòng)畫控制器前的幾步其他操作:

圖片發(fā)自簡(jiǎn)書App

UIKit首次詢問(wèn)他的動(dòng)畫控制器來(lái)獲得動(dòng)畫的持續(xù)時(shí)間(以秒為單位)然后調(diào)用animateTransition(using:)方法,這是我們自定義動(dòng)畫開(kāi)始真正發(fā)生的地方。

在animateTransition(using:)方法中,我們既可以訪問(wèn)到當(dāng)前正展示在screen上的viewController,同時(shí)也可以訪問(wèn)到即將被present出來(lái)的viewController,你可以按照自己想要的對(duì)當(dāng)前view和即將出現(xiàn)的新view進(jìn)行漸隱漸現(xiàn)、縮放、旋轉(zhuǎn)等等操作。

現(xiàn)在已經(jīng)學(xué)習(xí)到了一點(diǎn)自定義present動(dòng)畫是如何工作的,那么就可以開(kāi)始自己的了。

實(shí)現(xiàn)transitioning動(dòng)畫代理

既然代理的任務(wù)是控制產(chǎn)生真正動(dòng)畫的動(dòng)畫發(fā)生器,那么在寫代理里面的方法前應(yīng)該先為這個(gè)動(dòng)畫類創(chuàng)建一個(gè)存根(存根的意思是????、)。在項(xiàng)目里新建一個(gè)類,繼承與NSObject,可以取名為PopAnimator,原文中使用swift語(yǔ)言寫的,這里用OC,然后打開(kāi)新建的類,讓它遵守UIViewControllerAnimatedTransitioning協(xié)議、可以看到xcode給出警告,因?yàn)槲覀冞€沒(méi)有實(shí)現(xiàn)它required的代理方法。

然后打開(kāi).m文件,向其中添加如下方法,

```

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

? ? return:0;

}

Swift:

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

?return 0

}

```

以上返回的0值只是起到一個(gè)占位的作用,可以在之后用真正的值來(lái)代替它。


現(xiàn)在向PopAnimator里繼續(xù)添加方法:

```

oc:

-(void)animateTransition:(id )transitionContext{

}

swift:

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

}

以上的方法將會(huì)持有我們的動(dòng)畫代碼,加上這個(gè)方法后就可以去掉Xcode提示的警告了。

現(xiàn)在既然已經(jīng)已經(jīng)擁有了基礎(chǔ)的動(dòng)畫類,那么可以轉(zhuǎn)移到viewController這邊來(lái)實(shí)現(xiàn)代理方法了。

打開(kāi)viewcontroller.h(或者viewcontroller.swift),然后然他遵守UIViewControllerTransitioningDelegate

這意味著我們的viewController遵守了專場(chǎng)協(xié)議。你可以將它的代理的方法添加到你的.m文件里

找到disTapImageView:(_:)這個(gè)方法,在方法的底部可以看到present出詳情頁(yè)的代碼,herbDetails是一個(gè)新的viewCotroller實(shí)例,你可以將他的專場(chǎng)代理設(shè)置為當(dāng)前的controller。

herbDetails.transitioningDelegate = self;

現(xiàn)在你每次present出這個(gè)新的詳情頁(yè)的時(shí)候,UIKit都會(huì)詢問(wèn)當(dāng)前viewController是否擁有一個(gè)動(dòng)畫控制器。然而到現(xiàn)在我們viewController里還沒(méi)有實(shí)現(xiàn)任何關(guān)于UIViewControllerTransitioningDelegate代理的方法。因此UIKit依然會(huì)使用系統(tǒng)默認(rèn)的動(dòng)畫。

下一步就是創(chuàng)建我們的動(dòng)畫控制器了,然后在UIKit每次詢問(wèn)的時(shí)候返回給它用來(lái)執(zhí)行動(dòng)畫。

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

Swift: let ?transition = PopAnimator();

(不得不說(shuō),Swift確實(shí)夠簡(jiǎn)潔,正像它的名字的含義一樣)

這個(gè)PopAnimator的實(shí)例transition將會(huì)執(zhí)行我們的轉(zhuǎn)場(chǎng)動(dòng)畫了,我們只需要?jiǎng)?chuàng)建一個(gè)這個(gè)對(duì)象,因?yàn)槲覀兠看蔚膭?dòng)畫都是一樣的,所以每次present一個(gè)controller的時(shí)候都可以用這個(gè)實(shí)例。

現(xiàn)在打開(kāi)我們的ViewController,添加以下的代理方法。

OC:

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

//返回一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象。

}

Swift:

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

?return transition

}

在這個(gè)方法里攜帶了幾個(gè)參數(shù)以便于讓你決定要不要返回一個(gè)自定義的動(dòng)畫。在這里我們只需要返回一個(gè)PopAnimator的實(shí)例即可。

現(xiàn)在我們已經(jīng)為present出controller添加了代理的方法,但是我們?cè)撛鯓觗ismiss掉呢


可以繼續(xù)添加代理方法如下:

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

Swift:

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

?return nil

}

上面的方法本質(zhì)上做了和上一個(gè)同樣的事情:你檢查哪一個(gè)viewController被dismiss掉了,然后決定是否應(yīng)該返回nil繼續(xù)使用系統(tǒng)默認(rèn)的動(dòng)畫,或者是返回一個(gè)自定義的動(dòng)畫來(lái)代替。在這里我們返回nil即可。

現(xiàn)在我們已經(jīng)擁有了一個(gè)自定義的動(dòng)畫控制器來(lái)控制我們的自定義的轉(zhuǎn)場(chǎng)動(dòng)畫。但是它怎樣工作呢?如果這時(shí)候運(yùn)行工程然后run,然后會(huì)發(fā)現(xiàn)沒(méi)有發(fā)生任何事情,為什么?因?yàn)槲覀兊淖远x動(dòng)畫控制器里還沒(méi)有任何代碼呢,哈哈!

那么現(xiàn)在我們寫PopAnimator里的代碼

首先給類設(shè)置幾個(gè)成員變量如下

OC:

{

? ? CGFloat ? duration;(默認(rèn)為1)

? ? BOOL ?presenting;(默認(rèn)為true)

? ? CGRect originFrame;(默認(rèn)為CGRectZero)

}

Swift:

let duration = 1.0

var presenting = true

var originFrame = CGRect.zero


此處我們的duration可以用在幾個(gè)不同的地方,比如當(dāng)你想告訴UIKit我們的動(dòng)畫將會(huì)持續(xù)多久還有當(dāng)你創(chuàng)建自己的動(dòng)畫時(shí)也會(huì)用到。

我們也定義了presenting 布爾值,來(lái)告訴動(dòng)畫控制器我們是在present還是在dismiss。我們之所以想記錄這個(gè)值,因?yàn)槲覀冋梢杂眠@個(gè)來(lái)present,反過(guò)來(lái)就可以用來(lái)dismiss了。

最后我們還使用了originFrame來(lái)保存圖片的原始frame,因?yàn)槲覀儠?huì)用到這個(gè)值給圖片做一個(gè)動(dòng)畫使圖片充滿屏幕。所以應(yīng)該在點(diǎn)擊圖片的時(shí)候應(yīng)該記得保存圖片的原始frame,然后把它傳遞給動(dòng)畫控制器。


現(xiàn)在我們可以將注意力移到UIViewControllerAnimatedTransitioning的代理方法中。將返回時(shí)間的默認(rèn)0改為 return duration.

重復(fù)使用duration這個(gè)變量會(huì)讓我們更加方便的體驗(yàn)我們的轉(zhuǎn)場(chǎng)動(dòng)畫,我們可以通過(guò)簡(jiǎn)單的修改這個(gè)屬性的值來(lái)使得動(dòng)畫運(yùn)行的更快或者更慢。

設(shè)置我們的轉(zhuǎn)場(chǎng)動(dòng)畫上下文

現(xiàn)在是時(shí)候往我們的animateTransition方法里添加點(diǎn)魔法了。這個(gè)方法擁有一個(gè)UIViewControllerConextTransitioning類型的參數(shù),可以讓我們獲取到專場(chǎng)動(dòng)畫所需要的參數(shù)和viewControllers

在我們開(kāi)始代碼前,有必要知道什么是動(dòng)畫上下文(animation context)

當(dāng)兩個(gè)viewControllers之間的轉(zhuǎn)場(chǎng)動(dòng)畫開(kāi)始時(shí),當(dāng)前顯示的view會(huì)被添加到一個(gè)transition container view上,新的試圖控制器的view被創(chuàng)建了但是還沒(méi)有被看到,就像下面的視圖所展示的一樣


圖片發(fā)自簡(jiǎn)書App

因此我們的任務(wù)就是用animateTransitin()方法將新的view添加到transition ?container 中

默認(rèn)狀態(tài)下,當(dāng)發(fā)生專場(chǎng)動(dòng)畫時(shí)舊視圖會(huì)從transition container中移除。如下圖:


圖片發(fā)自簡(jiǎn)書App

我們需要先創(chuàng)建一個(gè)簡(jiǎn)單的專場(chǎng)動(dòng)畫來(lái)看看他是如何工作的,然后我們?cè)賹?shí)現(xiàn)更加炫酷、更加復(fù)雜的動(dòng)畫

先添加一個(gè)帶帶漸隱漸現(xiàn)效果的transition

我們先以一個(gè)簡(jiǎn)單的漸隱動(dòng)畫開(kāi)始,初步感受一下自定義的轉(zhuǎn)場(chǎng)動(dòng)畫。

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

OC:{

? ?UIView *containerView = transitionContext.containerView;

? ?UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];

}

Swift{

let containerView = transitionContext.containerView

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

}

首先我們獲取到了container view,我們的動(dòng)畫將會(huì)在它里面發(fā)生,然后我們還獲取到了新的view,用toView來(lái)代表


這個(gè)轉(zhuǎn)場(chǎng)上下文對(duì)象擁有兩個(gè)非常好用的方法,我們可以用這些方法獲取到一些有用的值


1、 - (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key方法。通過(guò)這個(gè)方法我們可以獲取到新舊view,比如通過(guò) UITransitionContextToViewKey可以獲取到新的view,通過(guò) UITransitionContextFromViewKey可以獲取到舊的view.

2、 - (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;通過(guò)這個(gè)方法可以獲取到新舊的視圖控制器,通過(guò) UITransitionContextFromViewControllerKey還有 UITransitionContextToViewControllerKey來(lái)獲取

此時(shí),我們同時(shí)擁有了container view和我們將要展示的view,接下來(lái)就是要將這個(gè)將要presented的view當(dāng)做containerViewde 子視圖添加到其上,并且以某種方式使其運(yùn)動(dòng)。

可以在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)

}

)

}

需要注意的是我們要在動(dòng)畫結(jié)束的時(shí)候用上下文調(diào)用completeTranstion方法,這會(huì)告訴我們的UIKit我們的動(dòng)畫完成了。


現(xiàn)在運(yùn)行我們的工程,點(diǎn)擊其中的一張圖片,會(huì)發(fā)現(xiàn)出現(xiàn)了漸顯式的動(dòng)畫。

圖片發(fā)自簡(jiǎn)書App


* 現(xiàn)在我們已經(jīng)看到可以在animateTransition里做什么操作了,我們接下來(lái)添加點(diǎn)更有意思的東西。

添加一個(gè)帶Pop效果的transition

現(xiàn)在我們要重構(gòu)我們的代碼來(lái)獲得一個(gè)完全不一樣的新的轉(zhuǎn)場(chǎng)效果,先用如下的代碼替換掉原來(lái)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是我們動(dòng)畫發(fā)生的地方,toView是將要present出來(lái)的頁(yè)面。如果我們正在present新頁(yè)面,那么herbView就恰恰是toView,否則的話,就從上下文中獲取,無(wú)論是present還是dismiss,要確保herbView是我們要給添加動(dòng)畫的view。


當(dāng)我們present一個(gè)詳情頁(yè)時(shí),它會(huì)慢慢充滿整個(gè)屏幕,當(dāng)dismissed的時(shí)候,它會(huì)慢慢縮小回圖片的原有尺寸。


接下來(lái)向其中繼續(xù)添加如下代碼:

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

在上面的代碼中,我們獲取到動(dòng)畫的初始frame信息以及最終的frame信息,然后計(jì)算出我們?cè)诿總€(gè)view上做動(dòng)畫時(shí)候的每條坐標(biāo)軸的縮放比例。

現(xiàn)在我們需要仔細(xì)的計(jì)算新的view的位置,使得它恰巧從我們所點(diǎn)擊的照片的上方出現(xiàn),這會(huì)使得這個(gè)動(dòng)畫看起來(lái)是我們點(diǎn)擊的這張照片變大了,最終填滿了整個(gè)屏幕。

繼續(xù)在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

}


當(dāng)我們present這個(gè)新的view的時(shí)候,我們?cè)O(shè)置了它的scale還有position。


現(xiàn)在我們?cè)诜椒ɡ锾砑幼詈髱仔写a

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)

?}

)

以上代碼首先會(huì)將toView添加到container上,然后下一步,我們需要確保herbView是在屏幕的頂部的,因?yàn)檫@是我們唯一讓之運(yùn)動(dòng)的view,應(yīng)該記住的是當(dāng)我們dismiss的時(shí)候,toView是我們的初始view,所以我們要把它加載其他任何東西的上面,如果不把它放在最上面的話我們的動(dòng)畫將會(huì)被隱藏掉。

然后我們可以開(kāi)始我們的動(dòng)畫了,使用帶spring 效果的動(dòng)畫會(huì)給它加上一點(diǎn)彈簧的效果。


在我們的動(dòng)畫表述中,我們改變了herbView的transform和position,當(dāng)present的時(shí)候,我們使其從底部的一個(gè)小尺寸變大到充滿整個(gè)屏幕。


此刻我們已經(jīng)將新的viewController搬到屏幕上來(lái)了,并且是在所點(diǎn)擊的圖片的上方,我們?cè)诔跏糵rame和最終的frame之間做動(dòng)畫,然后動(dòng)畫結(jié)束后,我們調(diào)用completeTransition方法向UIKit提交反饋。


然而,此時(shí)并不完美,但是只要我們稍加留意一下我們動(dòng)畫的幾個(gè)粗糙的地方,然后就會(huì)變成我們想要的樣子了。


現(xiàn)在我們的動(dòng)畫是從屏幕的左上角開(kāi)始的,因?yàn)閛riginFrame的默認(rèn)尺寸是(0,0),我們還沒(méi)有給它設(shè)置什么值。


打開(kāi)ViewController.Swift,向animationController(forPresented:)方法上面添加如下代碼

Swift


transition.originFrame =

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


transition.presenting = true

selectedImage!.isHidden = true

注意這里主要就是設(shè)置動(dòng)畫的初始位置,也就是我們所點(diǎn)擊的圖片的位置,OC代碼的話保持邏輯一致即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容