iOS 7 中最讓我激動(dòng)的特性之一就是提供了新的 API 來支持自定義 view contrioller 之間的轉(zhuǎn)場動(dòng)畫。iOS 7 發(fā)布之前,我自己寫過一些 view controller 之間的轉(zhuǎn)場動(dòng)畫,這是一個(gè)比較頭疼的過程,而且這種做法并不被蘋果完全地支持,尤其是如果你想讓這個(gè)轉(zhuǎn)場動(dòng)畫有交互式的效果就更難了。
在繼續(xù)閱讀之前,我需要先聲明一下:這個(gè) API 是新近才發(fā)布的,目前還沒有所謂的最佳實(shí)踐。通常來說,開發(fā)者需要探索幾個(gè)月才能得出關(guān)于新 API 的最佳實(shí)踐。因此請(qǐng)將本文看做對(duì)一個(gè)新 API 的探索,而非關(guān)于這個(gè)新 API 的最佳實(shí)踐介紹。如果您有更好的關(guān)于這個(gè) API 的實(shí)踐,請(qǐng)不吝賜教,我們會(huì)把您的實(shí)踐更新到這篇文章中。
在開始研究新的 API 之間,我們先來看看在 iOS 7 中 navigation controller 之間的默認(rèn)的行為發(fā)生了那些改變:在 navigation controller 中,切換兩個(gè) view controller 的動(dòng)畫變得更有交互性。比方說你想要 pop 一個(gè) view controller 出去,你可以用手指從屏幕的左邊緣開始拖動(dòng),慢慢地把當(dāng)前的 view controller 向右拖出屏幕去。
接下來,我們來看看這個(gè)新 API。很有趣的一個(gè)現(xiàn)象是,這部分 API 大量的使用了協(xié)議而不是具體的對(duì)象。這初看起來有點(diǎn)奇怪,但我個(gè)人更喜歡這樣的 API 設(shè)計(jì),因?yàn)檫@種設(shè)計(jì)給了我們這些開發(fā)者更大的靈活性。下面,讓我們來做件簡單的事情:在 Navigation Controller 中,實(shí)現(xiàn)一個(gè)自定義的 push 動(dòng)畫效果(本文中的示例代碼托管在 Github)。為了完成這個(gè)任務(wù),需要實(shí)現(xiàn) UINavigationControllerDelegate
中的新方法:
編者注 原文的作者在 Github 上面的示例代碼和文章中的代碼有一些出入(比如下面這里是 Push,但是在示例代碼中是 Pop)。如果需要,您也可以參考這個(gè)修正版示例代碼,和文章的代碼差異要小一點(diǎn)。
- (id<UIViewControllerAnimatedTransitioning>)
navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush) {
return self.animator;
}
return nil;
}
從上面的代碼可以看出,我們可以根據(jù)不同的 operation(Push 或 Pop)返回不同的 animator。我們可以把 animator 存到一個(gè)屬性中,從而在多個(gè) operation 之間實(shí)現(xiàn)共享,或者我們也可以為每個(gè) operation 都創(chuàng)建一個(gè)新的 animator 對(duì)象,這里的靈活性很大。
為了讓動(dòng)畫運(yùn)行起來,我們創(chuàng)建一個(gè)自定義類,并且實(shí)現(xiàn) UIViewControllerContextTransitioning
這個(gè)協(xié)議:
@interface Animator : NSObject <UIViewControllerAnimatedTransitioning>
@end
這個(gè)協(xié)議要求我們實(shí)現(xiàn)兩個(gè)方法,其中一個(gè)定義了動(dòng)畫的持續(xù)時(shí)間:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.25;
}
另一個(gè)方法描述整個(gè)動(dòng)畫的執(zhí)行效果:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1);
toViewController.view.alpha = 1;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
從上面的例子中,你可以看到如何運(yùn)用協(xié)議的:這個(gè)方法中通過接受一個(gè)類型為 id<UIViewControllerContextTransitioning>
的參數(shù),來獲取 transition context。值得注意的是,執(zhí)行完動(dòng)畫之后,我們需要調(diào)用 transitionContext 的 completeTransition:
這個(gè)方法來更新 view controller 的狀態(tài)。剩下的代碼和 iOS 7 之前的一樣了,我們從 transition context 中得到了需要做轉(zhuǎn)場的兩個(gè) view controller,然后使用最簡單的 UIView
animation 來實(shí)現(xiàn)了轉(zhuǎn)場動(dòng)畫。這就是全部代碼了,我們已經(jīng)實(shí)現(xiàn)了一個(gè)縮放效果的轉(zhuǎn)場動(dòng)畫。
注意,這里只是為 Push 操作實(shí)現(xiàn)了自定義效果的轉(zhuǎn)場動(dòng)畫,對(duì)于 Pop 操作,還是會(huì)使用默認(rèn)的滑動(dòng)效果,另外,上面我們實(shí)現(xiàn)的轉(zhuǎn)場動(dòng)畫無法交互,下面我們就來看看解決這個(gè)問題。
想要?jiǎng)赢嬜兊乜梢越换シ浅:唵?,我們只需要覆蓋另一個(gè) UINavigationControllerDelegate
的方法:
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
return self.interactionController;
}
注意,在非交互式動(dòng)畫效果中,該方法返回 nil。
這里返回的 interaction controller 是 UIPercentDrivenInteractionTransition
類的一個(gè)實(shí)例,開發(fā)者不需要任何配置就可工作。我們創(chuàng)建了一個(gè)拖動(dòng)手勢(Pan Recognizer),下面是處理該手勢的代碼:
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
if (location.x > CGRectGetMidX(view.bounds)) {
navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
[self performSegueWithIdentifier:PushSegueIdentifier sender:self];
}
}
編者注 這里的代碼有一點(diǎn)示意的意思,和實(shí)際代碼有些出入,為了尊重原作者,我們沒有進(jìn)行修改,您可以參考原文在 Github 上的示例代碼進(jìn)行對(duì)比,也可以參考這個(gè)修正版示例代碼。
只有當(dāng)用戶從屏幕右半部分開始觸摸的時(shí)候,我們才把下一次動(dòng)畫效果設(shè)置為交互式的(通過設(shè)置 interactionController
這個(gè)屬性來實(shí)現(xiàn)),然后執(zhí)行方法 performSegueWithIdentifier:
(如果你不是使用的 storyboards,那么就直接調(diào)用 pushViewController...
這類方法)。為了讓轉(zhuǎn)場動(dòng)畫持續(xù)進(jìn)行,我們需要調(diào)用 interaction controller 的一個(gè)方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1;
[interactionController updateInteractiveTransition:d];
}
該方法會(huì)根據(jù)用戶手指拖動(dòng)的距離計(jì)算一個(gè)百分比,切換的動(dòng)畫效果也隨著這個(gè)百分比來走。最酷的是,interaction controller 會(huì)和 animation controller 一起協(xié)作,我們只使用了簡單的 UIView
animation 的動(dòng)畫效果,但是interaction controller 卻控制了動(dòng)畫的執(zhí)行進(jìn)度,我們并不需要把 interaction controller 和 animation controller 關(guān)聯(lián)起來,因?yàn)樗羞@些系統(tǒng)都以一種解耦的方式自動(dòng)地替我們完成了。
最后,我們需要根據(jù)用戶手勢的停止?fàn)顟B(tài)來判斷該操作是結(jié)束還是取消,并調(diào)用 interaction controller 中對(duì)應(yīng)的方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
if ([panGestureRecognizer velocityInView:view].x < 0) {
[interactionController finishInteractiveTransition];
} else {
[interactionController cancelInteractiveTransition];
}
navigationControllerDelegate.interactionController = nil;
}
注意,當(dāng)切換完成或者取消的時(shí)候,記得把 interaction controller 設(shè)置為 nil。因?yàn)槿绻乱淮蔚霓D(zhuǎn)場是非交互的, 我們不應(yīng)該返回這個(gè)舊的 interaction controller。
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)完全自定義的可交互的轉(zhuǎn)場動(dòng)畫了。通過簡單的手勢識(shí)別和 UIKit 提供的一個(gè)類,用幾行代碼就達(dá)到完成了。對(duì)于大部分的應(yīng)用場景,你讀到這兒就夠用了,使用上面提到的方法就可以達(dá)到你想要的動(dòng)畫效果了。但如果你想更對(duì)轉(zhuǎn)場動(dòng)畫或者交互效果進(jìn)行深度定制,請(qǐng)繼續(xù)閱讀下面一節(jié)。
下面我們就來看看如何真正的,徹底的定制動(dòng)畫效果。這一次我們不使用 UIView animation,甚至連 Core Animation 也不用,完全自己來實(shí)現(xiàn)所有的動(dòng)畫效果。在 Letterpress-style 這個(gè)項(xiàng)目中,剛開始我嘗試使用 Core Image 來做動(dòng)畫效果,但是在我的 iPhone 4 上,動(dòng)畫的渲染最高只能達(dá)到 9 幀/秒,離我想要的 60 幀/秒差得很遠(yuǎn)。
但是當(dāng)我使用了 GPUImage 之后,實(shí)現(xiàn)一個(gè)非常漂亮的動(dòng)畫變的異常簡單。這里我們要實(shí)現(xiàn)的轉(zhuǎn)場效果是:兩個(gè) view controller 像素化,然后相互消融在一起。實(shí)現(xiàn)方法是先對(duì)兩個(gè) view controller 進(jìn)行截屏,然后再用 GPUImage 的圖片濾鏡(filter)處理這兩張截圖。
首先,我們先創(chuàng)建一個(gè)自定義類,這個(gè)類實(shí)現(xiàn)了 UIViewControllerAnimatedTransitioning
和 UIViewControllerInteractiveTransitioning
這兩個(gè)協(xié)議:
@interface GPUImageAnimator : NSObject
<UIViewControllerAnimatedTransitioning,
UIViewControllerInteractiveTransitioning>
@property (nonatomic) BOOL interactive;
@property (nonatomic) CGFloat progress;
- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;
@end
為了加速動(dòng)畫的運(yùn)行,我們可以把圖片一次加載到 GPU 中,然后所有的處理和繪圖都直接在 GPU 上執(zhí)行,不需要再傳送到 CPU 處理(這種數(shù)據(jù)傳輸非常慢)。通過使用 GPUImageView,我們就可以直接使用 OpenGL 畫圖(我們不需要手寫 OpenGL 這種底層的代碼,只要繼續(xù)使用 GPUImage 封裝好的接口就可以)。
創(chuàng)建濾鏡鏈(filter chain)也非常的直觀,我們可以直接在樣例代碼的 setup
方法中看到如何構(gòu)造它。比較有挑戰(zhàn)的是如何讓濾鏡也“動(dòng)”起來。GPUImage 沒有直接提供給我們動(dòng)畫效果,因此我們需要每渲染一幀就更新一下濾鏡來實(shí)現(xiàn)動(dòng)態(tài)的濾鏡效果。使用 CADisplayLink
可以完成這個(gè)工作:
編者注 原文中的示例代碼中缺少了這一章的內(nèi)容,我在原作者的 Github Gist 上找到了相關(guān)的源碼,整理之后放到了 Github 上,您可以在這里找到它。
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
在 frame
方法中,我們可以根據(jù)時(shí)間來更新動(dòng)畫進(jìn)度,并相應(yīng)地更新濾鏡:
- (void)frame:(CADisplayLink*)link
{
self.progress = MAX(0, MIN((link.timestamp - self.startTime) / duration, 1));
self.blend.mix = self.progress;
self.sourcePixellateFilter.fractionalWidthOfAPixel = self.progress *0.1;
self.targetPixellateFilter.fractionalWidthOfAPixel = (1- self.progress)*0.1;
[self triggerRenderOfNextFrame];
}
好了,基本上這樣就完成了。如果你想要實(shí)現(xiàn)交互式的轉(zhuǎn)場效果,那么在這里,就不能使用時(shí)間,而是要根據(jù)手勢來更新動(dòng)畫進(jìn)度,其他的代碼基本差不多。
這個(gè)功能非常強(qiáng)大,你可以使用 GPUImage 中任何已有的濾鏡,或者寫一個(gè)自己的 OpenGL 著色器(shader)來達(dá)到你想要的效果。
本文只探討了在 navigation controller 中的兩個(gè) view controller 之間的轉(zhuǎn)場動(dòng)畫,但是這些做法在 tab bar controller 或者任何你自己定義的 view controller 容器中也是通用的。另外,在 iOS 7 中,UICollectionViewController
也進(jìn)行了擴(kuò)展,現(xiàn)在你可以在布局之間進(jìn)行自動(dòng)以及交互的動(dòng)畫切換,背后使用的也是同樣的機(jī)制。這真是太強(qiáng)大了。
在和 Orta 討論這個(gè) API 的時(shí)候,他提到他已經(jīng)在大量地使用這些機(jī)制以創(chuàng)建更輕量的 view controller。與其在一個(gè) view controller 中維護(hù)各種狀態(tài),不如再創(chuàng)建一個(gè)新的 view controller,使用自定義的轉(zhuǎn)場動(dòng)畫,然后在這個(gè)轉(zhuǎn)場動(dòng)畫中來移動(dòng)你的各種 view。