一篇非常酷的導航轉場動畫博文,用到IOS 7 中的新技術。
在iOS7以前,開發者如果希望定製導航控制器推入推出視圖時的轉場動畫,一般都只能通過子類化UINavigationController或者自己編寫動畫代碼去覆蓋相應的方法,現在iOS7爲開發者帶來了福音,蘋果公司引入了大量新API,給予了開發者很高的自由度,在處理由UIViewController管理的UIView動畫時,這些API使用方便,可擴展性也很強,定製起來非常輕鬆:
全新的針對UIView的動畫block方法
全新的UIViewControllerAnimatedTransitioning協議以及動畫控制器的概念
Interaction Controllers以及Transition Coordinators
全新的針對動畫的助手API(簡便方法)
這裏我編寫了一個示例應用程序,其中展示了我將在這篇文章中所提到的一些技巧, 爲了快速理解我們應當如何使用iOS7的新API來處理
UIViewController的轉場動畫,請在此鏈接中下載該示例。
全新的針對UIView的動畫block方法
iOS4的發佈帶來了強大的block方法,在編寫UIView動畫時使用block可以輕鬆地得到滿意的效果,然而有些情況下,我們還是不得不直接使用Core Animation。幸運的是,蘋果公司在iOS7中增加了2個新的基於block的方法,這樣我們就很少再需要直接使用Core Animation了。
關鍵幀動畫
iOS7爲UIView封裝了一組API,讓我們很容易的得到與Core Animation框架中的CAKeyframeAnimation一樣的效果。
[UIView animateKeyframesWithDuration:duration delay:delay
options:options animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0
relativeDuration:0.5 animations:^{
//第一幀要執行的動畫
}];
[UIView addKeyframeWithRelativeStartTime:0.5
relativeDuration:0.5 animations:^{
//第二幀要執行的動畫
}];
} completion:^(BOOLfinished) {
//動畫結束後執行的代碼塊
}];
新引入的animateKeyframesWithDuration與CAKeyframeAnimation的關係,可以比對animateWithDuration和CABasicAnimation,我們只需要將每一幀動畫加入到block方法中,並傳入此段動畫在全過程中的相對開始時間和執行時間(duration具體是指此段動畫的執行時間佔全過程的百分比)。同時,你可以在一次動畫中使用多個關鍵幀,只需使用addKeyframe依次將所有關鍵幀加入動畫執行棧中。
下面是一個簡單的例子:在示例應用中,我使用關鍵幀block來退出模態視圖控制器。
[UIView addKeyframeWithRelativeStartTime:0.0
relativeDuration:0.15 animations:^{
//順時針旋轉90度
snapshot.transform = CGAffineTransformMakeRotation(M_PI *
-1.5);
}];
[UIView addKeyframeWithRelativeStartTime:0.15
relativeDuration:0.10 animations:^{
//180度
snapshot.transform = CGAffineTransformMakeRotation(M_PI *
1.0);
}];
[UIView addKeyframeWithRelativeStartTime:0.25
relativeDuration:0.20 animations:^{
//擺過中點,225度
snapshot.transform = CGAffineTransformMakeRotation(M_PI *
1.3);
}];
[UIView addKeyframeWithRelativeStartTime:0.45
relativeDuration:0.20 animations:^{
//再擺回來,140度
snapshot.transform = CGAffineTransformMakeRotation(M_PI *
0.8);
}];
[UIView addKeyframeWithRelativeStartTime:0.65
relativeDuration:0.35 animations:^{
//旋轉後掉落
//最後一步,視圖淡出並消失
CGAffineTransform shift =
CGAffineTransformMakeTranslation(180.0, 0.0);
CGAffineTransform rotate =
CGAffineTransformMakeRotation(M_PI * 0.3);
snapshot.transform = CGAffineTransformConcat(shift,
rotate);
_coverView.alpha = 0.0;
}];
視圖彷彿在重力的牽引下繞左下角順時針旋轉,並在最低點擺動了一下,最後脫落。
彈簧動畫
iOS7新引入的另一個block方法可以讓你輕鬆將真實物理世界中的彈性效果集成進視圖動畫中。蘋果公司一直建議開發者儘可能將動畫效果做的跟真實物理世界一樣——在視圖滑動時,可以像彈簧一樣,稍微拉伸一些,再彈回正確位置。使用新的彈簧動畫API來實現此效果相較以往要簡單很多。
[UIView animateWithDuration:duration delay:delay
usingSpringWithDamping:damping initialSpringVelocity:velocity
options:options animations:^{
//這裏書寫動畫相關代碼
} completion:^(BOOLfinished) {
//動畫結束後執行的代碼塊
}];
這裏用到了一些物理上的概念:damping參數代表彈性阻尼,隨着阻尼值越來越接近0.0,動畫的彈性效果會越來越明顯,而如果設置阻尼值爲1.0,則視圖動畫不會有彈性效果——視圖滑動時會直接減速到0並立刻停止,不會有彈簧類的拉伸效果。
velocity參數代表彈性修正速度,它表示視圖在彈跳時恢復原位的速度,例如,如果在動畫中視圖被拉伸的最大距離是200像素,你想讓視圖以100像素每秒的速度恢復原位,那麼就設置velocity的值爲0.5。(譯者:建議大家看看源代碼,代碼中damping設置爲0.8不夠明顯,你可以將damping調爲0.1,然後慢慢調整velocity看看效果)
在示例應用程序中,我用彈簧動畫讓模態視圖控制器從屏幕底部滑上來,設置彈性阻尼爲0.8,彈性修正速度爲1.0,運行後可以看到,視圖將衝出15像素的距離,然後慢慢降回原位。如果我設置彈性阻尼爲0.6或者更小,那麼視圖會衝得更高,而且降回原位前還會繼續向下反彈。(也就是停止前來回彈的次數越來越多,彈性效果越來越明顯)需要注意的是,不要將彈性動畫與UIKit的動態特效引擎相混淆。彈性動畫是一個標準的UIView動畫API,僅僅提供了有限的幾種真實物理效果。
自定義UIViewController的轉場動畫
現在讓我們來看一個好東西。蘋果公司不僅爲開發者引入了新的動畫API,而且還擴大了其應用範圍。在使用UIViewController管理視圖的推入推出時,可以很容易地自定義以下轉場動畫:
UIViewController
presentViewController
UITabBarController
setSelectedViewController
setSelectedIndex
UINavigationController
pushViewController
popViewController
setViewControllers
在示例應用程序中,我創建了一系列轉場動畫,在動畫中使用了之前講解過的新引入的彈簧動畫和關鍵幀block方法,現在讓我們來看看如何使用新API來自定義上述的轉場動畫。
核心概念:動畫控制器
那麼,如何在使用自定義動畫的同時不影響視圖的其他屬性?對此蘋果公司提供了一個新的協議:UIViewControllerAnimatedTransitioning,我們可以在協議方法中編寫自定義的動畫代碼。蘋果開發者文檔中稱實現了此協議的對象爲動畫控制器。
由於我們使用了協議這一語法特性,自定義動畫的代碼可以靈活的放在自己想要的位置。你可以創建一個專門用於管理動畫的類, 也可以讓UIViewController實現UIViewControllerAnimatedTransitioning接口。由於需要實現一系列不同的動畫,因此選擇爲每個動畫創建一個類。接下來創建這些動畫類的通用父類——BaseAnimation,它定義了一些通用的屬性和助手方法。
讓我們來看第一個動畫,使用UINavigationController推入推出視圖時,會有一個簡單的縮放效果。
-(void)animateTransition:
(id)transitionContext {
//獲取容器視圖引用
UIView *containerView = [transitionContext
containerView];
UIViewController *fromViewController = [transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey
];
UIViewController *toViewController = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
if(self.type == AnimationTypePresent) {
//插入“to”視圖,初始縮放值爲0.0
toViewController.view.transform =
CGAffineTransformMakeScale(0.0, 0.0);
[containerView insertSubview:toViewController.view
aboveSubview:fromViewController.view];
//縮放“to”視圖爲想要的效果
[UIView animateWithDuration:[self
transitionDuration:transitionContext] animations:^{
toViewController.view.transform =
CGAffineTransformMakeScale(1.0, 1.0);
} completion:^(BOOLfinished) {
[transitionContext completeTransition:YES];
}];
} elseif(self.type == AnimationTypeDismiss) {
//插入“to”視圖
[containerView insertSubview:toViewController.view
belowSubview:fromViewController.view];
//縮小“from”視圖,直到其消失
[UIView animateWithDuration:[self
transitionDuration:transitionContext] animations:^{
fromViewController.view.transform =
CGAffineTransformMakeScale(0.0, 0.0);
} completion:^(BOOLfinished) {
[transitionContext completeTransition:YES];
}];
}
}
-(NSTimeInterval)transitionDuration:
(id)transitionContext {
return0.4;
}
符合UIViewControllerAnimatedTransitioning協議的任何對象都需要實現animateTransition:和transitionDuration:兩個方法。你也可以選擇實現@optional方法animationEnded:,它在動畫完成後由系統自動調用,相當於completion block,非常方便。
在animateTransition:中你需要處理以下過程:
1. 將“to”視圖插入容器視圖
2. 將“to”和“from”視圖分別移動到自己想要的位置
3. 最後,在動畫完成時千萬別忘了調用completeTransition: 方法
UIViewControllerAnimatedTransitioning協議中的方法都帶有一個參數:transitionContext,這是一個系統級的對象,它符合 UIView-ControllerContextTransitioning協議,我們可以從該對象中獲取用於控制轉場動畫的必要信息,主要包括以下內容:
顯然,蘋果公司幫助開發者完成了大部分讓人討厭的細節工作,僅僅需要我們自己完成的工作就是定義動畫的初始狀態和終止狀態,並調整到自己滿意的效果。最後我再囉嗦兩句有關transitionContext的重要注意事項:
1.獲取frame的方法可能會返回CGRectZero——如果系統無法確定該frame的值具體是什麼。例如,如果你使用自定義的模態視圖控制器
推出動畫,在結束時系統無法確定其finalFrame。
2.如果視圖控制器已經從屏幕上移除了,那麼獲取frame的方法也會返回CGRectZero。例如在導航控制器的轉場動畫結束後,試圖獲取“from”視圖的finalFrame。
你不用手動去移除“from”視圖,transitionContext將自動幫你完成。
3.如果你在應用的其他地方需要使用transitionContext,你可以放心地使用動畫控制器保留一個transitionContext的引用。
將動畫控制器應用到轉場動畫中。
現在,我們已經開發好了動畫控制器,那麼最後需要做的就是,將它們應用到轉場動畫中:我們需要對管理轉場動畫的UIViewController做一些操作。
一般來說,我們只需要讓UIViewController符合UIViewController-TransitioningDelegate 協議, 編寫animationController-ForPresentedController和animationControllerForDismissedController方法。在我的示例應用程序中,我設置了一個屬性,用來讓動畫控制器知道目前正在推入還是推出視圖:
-(id)
animationControllerForPresentedController:(UIViewController
*)presented presentingController:(UIViewController
*)presenting sourceController:(UIViewController *)source {
modalAnimationController.type = AnimationTypePresent;
returnmodalAnimationController;
}
-(id)
animationControllerForDismissedController:(UIViewController
*)dismissed {
modalAnimationController.type = AnimationTypeDismiss;
returnmodalAnimationController;
}
然後,在推入模態視圖控制器時,我們設置modalPresentationStyle爲UIModalPresentationFullScreen或UIModalPresentationCustom。我們還必須將一個符合UIViewControllerTransitioningDelegate協議的對象設置爲它的transitioningDelegate,一般來說都是推入該模態視圖控制器的UIViewController。
OptionsViewController *modal = [[OptionsViewController alloc]
initWithNibName:@"OptionsViewController"bundle:[NSBundle
mainBundle]];
modal.transitioningDelegate = self;
modal.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController:modal animated:YES
completion:nil];
如果需要將動畫控制器應用到UINavigationController的轉場動畫中,我們需要使用UINavigationControllerDelegate協議中的一個新方法:animationControllerForOperation。對於任何自定義的導航轉場動畫,導航欄都會有一個淡入淡出的動畫過程。同樣,對於UITabBarController,使用UITabBarControllerDelegate協議的新方法——animationController-ForTransitionFromViewController。
爲轉場動畫定義交互方式
在iOS7中,蘋果到處都在使用交互式彈出手勢,同時,蘋果也給開發者們提供了一系列工具,只需簡單幾步就能將交互手勢應用在視圖切換過程中。我們可以通過相應的委託方法返回一個交互控制器:
UINavigationController
interactionControllerForAnimationController
UITabBarController
interactionControllerForAnimationController
UIViewController
interactionControllerForPresentation
interactionControllerForDismissal
這裏唯一需要注意的是,如果沒有自定義轉場動畫,這些方法就不會起作用。例如,你必須從animationControllerForOperation得到一個有效的動畫控制器,UINavigationController纔會調用interactionController-
ForAnimationController——即使你在轉場交互中沒有使用動畫控制器。
其次,交互控制器非常靈活,有很強的可擴展性。雖然在示例應用程序中我使用手勢檢測來控制交互,但是你也可以用手勢以外的其他方式來實現。你可以設計任意你想要的效果用以轉場交互。
交互控制器:最簡單的實現方式有兩種方式可以創建交互控制器。第一個也是最簡單的一個,就是使用UIPercentDrivenInteractiveTransition。
@interface UIPercentDrivenInteractiveTransition : NSObject
@property (readonly) CGFloat duration;
@property (readonly) CGFloat percentComplete;
@property (nonatomic,assign) CGFloat completionSpeed;
@property (nonatomic,assign) UIViewAnimationCurve
completionCurve;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
這個類具體實現了UIViewControllerInteractiveTransitioning協議,我們可以使用它輕鬆爲動畫控制器添加自定義的交互方式。只要爲目標視圖加入手勢(或者其他交互方式)並調用updateInteractiveTransition:,傳入動畫時間佔整個過程的百分比即可。同時, 記住在交互完成後調用finishInteractiveTransition: , 交互被取消時調用cancel-InteractiveTransition:。下面的例子展示瞭如何將捏合手勢應用到轉場動畫中:
-(void)handlePinch:(UIPinchGestureRecognizer*)pinch {
CGFloat scale = pinch.scale;
switch(pinch.state) {
caseUIGestureRecognizerStateBegan: {
_startScale = scale;
self.interactive = YES;
[self.navigationController
popViewControllerAnimated:YES];
break;
}
caseUIGestureRecognizerStateChanged: {
CGFloat percent = (1.0 - scale/_startScale);
[self updateInteractiveTransition:(percent < 0.0) ?
0.0 : percent];
break;
}
caseUIGestureRecognizerStateEnded: {
CGFloat percent = (1.0 - scale/_startScale);
BOOLcancelled = ([pinch velocity] < 5.0 && percent
<= 0.3);
if(cancelled) [self cancelInteractiveTransition];
else[self finishInteractiveTransition];
break;
}
caseUIGestureRecognizerStateCancelled: {
CGFloat percent = (1.0 - scale/_startScale);
BOOLcancelled = ([pinch velocity] < 5.0 && percent
<= 0.3);
if(cancelled) [self cancelInteractiveTransition];
else[self finishInteractiveTransition];
break;
}
}
}
當你繼承了UIPercentDrivenInteractiveTransition類,交互過程中系統會自動調用動畫控制器的animateTransition:方法,按照你傳遞的percentComplete參數實時地展現動畫效果。在交互完成後,它還自動調用animateTransition:方法恢復到正常狀態,一旦交互完成,我們就可以改變completionSpeed和completionCurve屬性來修改其他的一些樣式。
交互控制器:通過自定義的方式
如果你需要深入控制UIPercentDrivenInteractiveTransition處理轉場動畫的細節,那麼就不用去繼承該類,而是使用UIViewController-InteractiveTransitioning協議。此協議與UIViewController-AnimatedTransitioning類似,我們可以通過該協議控制所有關於轉場動畫的細節。在該協議中我們需要完成以下步驟:
1. 實現startInteractiveTransition:方法,用於初始化專場動畫。
2. 獲取transitionContext 對象的引用(如果繼承了UIPercentDrivenInteractiveTransition,可以看到它自動幫我們完成了這一步驟,因此這裏我們必須手動獲取該對象)。
3. 和之前一樣,在適當的情況下調用updateInteractiveTransition:,cancelInteractiveTransition和finishInteractiveTransition(對於導航控制器來說,完成方法中還需要顯示或隱藏導航欄)。
4. 完成後仍然請記住調用transitionCompleted:。
下面是我通過自定義的交互控制器來實現與之前相同的動畫,仍然是使用捏合手勢控制轉場動畫。
-(void)startInteractiveTransition:
(id)transitionContext {
//獲取transitionContext對象的引用
_context = transitionContext;
//獲取容器視圖引用
UIView *containerView = [transitionContext
containerView];
UIViewController *fromViewController = [transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey
];
UIViewController *toViewController = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
//插入“to”視圖
toViewController.view.frame = [transitionContext
finalFrameForViewController:toViewController];
[containerView insertSubview:toViewController.view
belowSubview:fromViewController.view];
//保留需要縮?小的視圖的引用
_transitioningView = fromViewController.view;
}
-(void)updateWithPercent:(CGFloat)percent {
CGFloat scale = fabsf(percent-1.0);
_transitioningView.transform =
CGAffineTransformMakeScale(scale, scale);
[_context updateInteractiveTransition:percent];
}
-(void)end:(BOOL)cancelled {
if(cancelled) {
[UIView animateWithDuration:_completionSpeed
animations:^{
_transitioningView.transform =
CGAffineTransformMakeScale(1.0, 1.0);
} completion:^(BOOLfinished) {
[_context cancelInteractiveTransition];
[_context completeTransition:NO];
}];
} else{
[UIView animateWithDuration:_completionSpeed
animations:^{
_transitioningView.transform =
CGAffineTransformMakeScale(0.0, 0.0);
} completion:^(BOOLfinished) {
[_context finishInteractiveTransition];
[_context completeTransition:YES];
}];
}
}
你可以讓動畫控制器同時實現UIViewControllerInteractive-Transitioning和 UIViewControllerAnimatedTransitioning(像示例程序中那樣),從而把所有代碼都放在一個類中。你也可以將交互控制器和動畫控制器分成兩個類——協議這一語法特性的妙處在於,你可以輕鬆實現符合需求的最佳解決方案。
更多小技巧
在block中選擇是否進行動畫
開發者或許會遇到這樣一種情況:在一串精美的動畫效果中,我們需要讓某些視圖不進行動畫,從而營造一種動靜相宜的效果。在動畫block方法推出之前,我們可以在[UIView beginAnimations]和[UIView commitAnimations]之間使用setAnimationsEnabled方法來設置哪些動畫不需要執行。而在iOS7SDK中,蘋果公司爲開發者提供了新方法,只要把不需要執行的動畫寫在block中即可:
[UIView performWithoutAnimation:^{
//確保不執行動畫
}];
你可以隨時執行這段代碼來控制不需要執行的動畫。
集合視圖的導航轉場動畫
你可能對UICollectionView的setLayout:animated:方法非常熟悉了。在iOS7中,當導航控制器推入推出集合視圖控制器時,如果開啓了 useLayout-ToLayoutNavigationTransitions屬性,系統將自動調用setLayout:animated:方法。因此,在你推入集合視圖控制器時,只需要設置該屬性,導航控制器就可以自動執行動畫,和你手動對集合視圖調用setLayout:animated方法的效果一樣。
CollectionViewController*VC = [[CollectionViewController
alloc] initWithCollectionViewLayout:flowLayout];
VC.title = @"Mini Apples";
VC.useLayoutToLayoutNavigationTransitions = YES;
[self.navigationController pushViewController:VC
animated:YES];
轉場動畫調度器
還有一個非常有用的API, 它可以幫助視圖控制器管理轉場動畫:UIViewControllerTransitionCoordinator協議。在iOS7中,每一個視圖控制器(當然也包括UINavigationController和UITabBarController)都有一個transitionCoordinator屬性,該屬性提供了一系列用於轉場動畫的強大工具,首先我們來看看animateAlongsideTransition:方法。
[self.transitionCoordinator
animateAlongsideTransition:^(id
rdinatorContext> context) {
//要執行的動畫
}
completion:^(id
context) {
//動畫結束後執行的代碼塊
}];
我們可以通過這個方法在進行轉場動畫時並行執行一些其他動畫,context參數和之前提到的符合UIViewControllerContextTransitioning協議的transitionContext參數相類似,從該參數中我們可以獲取有關轉場過程的一些重要信息,包括container view和轉場效果。蘋果公司甚至允許開發者不傳入context參數,只傳入完成後執行的block。所以請大膽嘗試使用它吧。
對於交互轉場來說, 視圖在轉場過程中狀態可能發生改變, 於是notifyWhenInteractionEndsUsingBlock:方法特別有用——它可以用來管理視圖狀態。在交互轉場中,viewWillAppear:方法或許會在某個視圖控制器推入時被調用,但是按照常理隨後應該會被調用的viewDidAppear:則不一定,因爲用戶隨時可能取消該交互(例如在之前的例子中,捏到一半又恢復原狀)。
由此,如果我們不希望在這種情況下修改視圖狀態,我們可以使用該方法,恢復對視圖的更改(使用UIViewControllerTransitionCoordinatorContext的isCancelled屬性)。
[self.transitionCoordinator
notifyWhenInteractionEndsUsingBlock:^(id
sitionCoordinatorContext> context) {
//動畫結束後執?行的代碼塊
}];
屏幕快照
在iOS7 以前, 獲取一個UIView的快照有以下步驟: 首先創建一個UIGraphics的圖像上下文,然後將視圖的layer渲染到該上下文中,從而取得一個圖像,最後關閉圖像上下文,並將圖像顯示在UIImageView中。現在我們只需要一行代碼就可以完成上述步驟了:
[view snapshotViewAfterScreenUpdates:NO];
這個方法制作了一個UIView的副本,如果我們希望視圖在執行動畫之前保存現在的外觀,以備之後使用(動畫中視圖可能會被子視圖遮蓋或者發生其他一些變化),該方法就特別方便。
afterUpdates參數表示是否在所有效果應用在視圖上了以後再獲取快照。例如,如果該參數爲NO,則立馬獲取該視圖現在狀態的快照,反之,以下代碼只能得到一個空白快照:
[view snapshotViewAfterScreenUpdates:YES];
[view setAlpha:0.0];
由於我們設置afterUpdates參數爲YES,而視圖的透明度值被設置成了0,所以方法將在該設置應用在視圖上了之後才進行快照,於是乎屏幕空空如也。另外就是……你可以對快照再進行快照……繼續快照……
結論
蘋果公司在iOS7中爲開發者添加了新的用於創建和自定義動畫的API。iOS7 SDK不僅引入了強大的動畫block和許多易於使用的方法,而且徹底改變了爲視圖自定義動畫的方式。最後,強烈建議你將示例應用程序的代碼下載下來,看看我在本文中介紹的API的使用方法!