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