自定義動畫
iOS 7 對我來說最激動人心的特性就是新的檢視控制器切換API(View Controller Transitioning API)。 iOS 7 之前,View Controller之間切換,我需要建立自定義的transitions。 而且這些方法都支援不完整,讓人頭疼。在transitions中增加互動功能就更難了。
在開始這篇文章之前,我要提醒一下:這是一個新的API,我們盡最大努力讓它可以實用,但是並不能保證是最佳。可能需要至少一個月後才能確定,這篇文章不是最佳的實用方案,這裡只是一個對新功能的探索。如果有更好的使用這個API的方法,請聯絡我們,這樣就可以修正這篇文章。
在開始介紹這個API之前,我們需要知道導航控制器的預設行為在iOS7下已經改變了:導航控制器下,切換2個view controller的動畫有一點細微的改變,變得更有互動性。例如,當你希望彈出一個view controller時,可以從螢幕左邊開始拖動,把整個內容拖動到螢幕右邊。
讓我們仔細看一下這個API,我發現這個被重度使用的介面是協議並不是一個實體。雖然一上來看上去有一點怪,但是我喜歡這個API,它給了我們更多的靈活性。我們從簡單開始:用自定義動畫代替原有的view controller的push動畫(這裡是sample project 在github)。我們首先需要實現這個新的 UINavigationControllerDelegate 方法:
1 2 3 4 5 6 7 8 9 10 11 |
- (id<UIViewControllerAnimatedTransitioning>) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController*)fromVC toViewController:(UIViewController*)toVC { if (operation == UINavigationControllerOperationPush) { return self.animator; } return nil; } |
我們可以觀察一下這種型別的操作(push 和 pop)返回一個不同的 animator。如果我們分享程式碼的話,這個可能是一個物件。我們可能需要把這個變數通過property儲存下來。我們也可以為不同的操作建立不同的物件,這裡有很高的靈活性。
讓這個動畫執行起來,我們建立一個自定義物件實現 UIViewControllerContextTransitioning 協議。
1 2 3 |
@interface Animator : NSObject <UIViewControllerAnimatedTransitioning> @end |
這個協議要求我們實現2個方法,其中一個是描述動畫的執行時間
1 2 3 4 |
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext { return 0.25; } |
另一個是描述動畫的執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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]]; }]; } |
這裡你可以看到這個協議是怎麼用的:沒有提供實體的物件引數,而是通過這個型別 id 得到transitionContext 唯一的最重要的東西就是在完成動畫之後要呼叫 completeTransition 這個告訴 transitionContext 我們已經完成動畫並且相應的更新了 view controller的狀態。其他程式碼是標準的,我們通過transitionContext得到2個UIViewController,然後使用簡單的 UIView 動畫,這裡我們很簡單的做了一個zooming的動畫
注意,我們只是寫了push的自定義動畫,當view controller pop時,iOS系統還是會使用預設的滑動動畫。而且,實現這個方法後。導航欄也不能互動了(就是從左到右拖動實現pop view controller)。下面完善它
互動動畫
讓之前的動畫變得能夠互動起來非常簡單。我們需要實現另一個UINavigationControllerDelegate
1 2 3 4 5 |
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController { return self.interactionController; } |
注意,如果在一個不能互動的動畫中,這裡會返回nil。(譯註:當不能互動時 self.interactionController 為 nil)
interactionController是UIPercentDrivenInteractionTransition的例項,沒有必要更多的設定。我們通過建立拖動手勢(UIPanGestureRecognizer)來實現:
1 2 3 4 5 6 |
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) { if (location.x > CGRectGetMidX(view.bounds)) { navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init]; [self performSegueWithIdentifier:PushSegueIdentifier sender:self]; } } |
只有當使用者在螢幕右邊操作時,我們才設定動畫是可以互動的(通過設定interactionController 屬性)。然後我們呼叫performSegueWithIdentifier(或是不用storyboards,直接push view controller) 在這個手勢變化中,我們呼叫interactionController 的一個方法 updateInteractiveTransition:
1 2 3 4 |
else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) { CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1; [interactionController updateInteractiveTransition:d]; } |
這裡根據拖動的距離設定百分比,非常cool的事情是互動控制器(interactionController)和 動畫控制器(animation controller)相互協作。而且因為是普通的 UIView 動畫,它控制著動畫的程式。我們不需要處理他們之前的事情, 所有的事情都在背後默默的自動搞定了。
最後,當手勢停止或是取消掉,我們需要呼叫interaction controller相應的方法
1 2 3 4 5 6 7 8 |
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) { if ([panGestureRecognizer velocityInView:view].x < 0) { [interactionController finishInteractiveTransition]; } else { [interactionController cancelInteractiveTransition]; } navigationControllerDelegate.interactionController = nil; } |
當切換動畫完畢時,設定interactionController為nil非常重要。如果下一個動畫是非互動的,我們不希望得到一個奇怪的 interactionController
現在我們已經有一個完整的自定義的可互動的過度變換(transition)了。通過普通的拖動手勢和一個UIKit提供的實體物件,幾行程式碼就搞定了。對於大多數的自定義互動過度變換,你可以在這裡停下來,用上面提到的方法做任何你想做得動畫 或是互動。
GPUImage自定義動畫
我們現在已經能夠實現一個完整的自定義動畫了,可以不用UIView 甚至Core Animation,做自己喜歡的動畫。一開始,我用Core Image實現了一個專案Letterpress-style。但是在我的舊iPhone4上面只能跑到大約9FPS,這個和我所期望的60FPS差距太大了。
但是當我使用GPUImage後,實現一個非常漂亮的自定義動畫效果變得非常簡單。我們希望這個動畫能夠做到畫素級的消融在2個view controller切換的時候。這個是通過分別對2個view controller 截圖,然後應用GPUImage的圖片濾鏡實現的。
首先,我們建立一個自定義類,實現animation 和 interactive transition 協議。
1 2 3 4 5 6 7 8 9 10 11 |
@interface GPUImageAnimator : NSObject <UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning> @property (nonatomic) BOOL interactive; @property (nonatomic) CGFloat progress; - (void)finishInteractiveTransition; - (void)cancelInteractiveTransition; @end |
為了讓這個動畫跑的飛快,我們只把圖片傳給GPU一次,然後把所有的影像處理繪製交給GPU,而不是傳給CPU(GPU和CPU之間的資料傳輸非常慢)。通過GPUImageView,我們可以用OpenGL繪製動畫效果(不需要手動編寫底層的OpenGL程式碼,我們可以繼續編寫上層程式碼)
建立這樣的濾鏡鏈非常方便。這裡可以看一下下面的例子。有一點挑戰的是實現動態的濾鏡。GPUImage不能給我們直接提供動畫效果。這裡我們通過在每一幀的時候更新濾鏡來實現動畫的繪製。我們使用CADisplayLink類來做這個。
1 2 |
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)]; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; |
在frame:方法中,我們根據時間更新動畫進度,然後更新濾鏡
1 2 3 4 5 6 7 8 |
- (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]; } |
以上就是我們所有要講得了。在互動變換中,我們需要確保我們的進度是根據手勢識別設定的,而不是根據時間。但是剩下的程式碼幾乎都一樣了。
這個真的太強大了,你可以使用GPUImage提供的任何濾鏡或是自己寫的OpenGL程式碼來實現上面的效果。
小結
我們這裡僅僅提到了導航控制器下面的2個 view controller 之間的動畫,事實上你可以做相同的事情在tabbar controller 或是自定義的container view controller。而且 UICollectionViewController 現在已經可以在layout上面自動實現互動動畫了。他們都是使用相同的機制。這個真的太強大了。
當我和Orta提到這個API時,他指出他已經使用這個功能建立了一些輕量級的view controller。不要在每一個view controller 儲存管理動畫的程式碼,而是建立一個新的view controller,然後實現2個view controlller檢視切換時的自定義的動畫效果。