這篇筆記翻譯自raywenderlick網(wǎng)站的過渡動畫的一篇文章宛瞄,原文用的swift,由于考慮到swift版本變動以及一些語法兼容問題,這里我還是用Objective-C進行了改寫妓羊,沒有逐字翻譯,加了部分自己的理解稍计。原文鏈接Creating Custom UIViewController Transitions躁绸。過渡動畫有些地方也是翻譯成轉(zhuǎn)場動畫,即從一個視圖控制器切到另一個視圖控制器臣嚣,本文以過渡來譯净刮。
1 前言
iOS自身就提供了很多針對UIViewController的過渡動畫,比如Cover Vertically(從下往上彈出效果)
硅则,Cross Dissolve(淡入淡出效果)
淹父,Partial Curl(書卷翻頁效果)
等。如圖1就是本文用到的示例中的iOS原生的Cover Vertically
效果的展示怎虫。
為了自己的APP更有個性暑认,自帶的效果往往不夠酷炫,所以需要自定義過渡動畫大审,通過這篇文章蘸际,我們會GET到下面幾個技能:
- 過渡動畫API的構(gòu)建。
- 使用自定義的過渡動畫來present和dismiss一個視圖控制器饥努。present過渡會在應用視圖層級結(jié)構(gòu)中添加一個新的視圖控制器捡鱼,而dismiss過渡會從層級結(jié)構(gòu)中刪除一個或多個視圖控制器八回。
- 學會使用交互式過渡動畫酷愧。
在我們開始的示例代碼中驾诈,還沒有加入自定義過渡動畫,已經(jīng)有的內(nèi)容是一個PageViewController溶浴,里面裝載的為CardViewController(內(nèi)容為一個UIView+一個Label用于展示圖片描述)乍迄,點擊CardViewController里面的卡片,會切換到RevealViewController(包含一個Label展示圖片名字士败,一個Image View展示寵物圖片闯两,一個按鈕用于返回到卡片視圖)。而我們最終要達到的效果如圖2所示:
2 過渡動畫API探究
過渡動畫API涉及到的一些角色如圖3所示谅将,下面分開介紹:
2.1 過渡動畫API中的角色
本節(jié)內(nèi)容對過渡動畫API中的各個角色進行說明漾狼,包含的角色參照圖3。
2.1.1 過渡動畫代理(Transitioning Delegate)
每個View Controller都有一個transitionDelegate屬性饥臂,這個代理實現(xiàn)了UIViewControllerTransitioningDelegate協(xié)議逊躁。
每當你要present或者dismiss一個View Controller的時候,UIKit會去過渡動畫代理中查詢需要使用的動畫效果隅熙。實際項目中稽煤,我們可以設置代理為自定義的類來返回我們需要的自定義的動畫效果。
2.1.2 動畫控制器(Animation Controller)
動畫控制器是實現(xiàn)了UIViewControllerAnimatedTransitioning協(xié)議的用于執(zhí)行過渡動畫的對象囚戚。
2.1.3 過渡動畫上下文對象(Transitioning Context)
上下文對象實現(xiàn)了UIViewControllerContextTransitioning協(xié)議酵熙,在動畫過程中是至關重要的,它封裝了所有的參與過渡動畫的View Controllers的信息驰坊。不過我們不用寫代碼實現(xiàn)它匾二,在動畫控制器里面,過渡動畫執(zhí)行的時候庐橙,我們的函數(shù)會接收到一個上下文對象作為參數(shù)并從中獲取相關View Controller的信息假勿。
2.2 過渡動畫流程
- 你觸發(fā)一個過渡動作√睿可以通過編碼或者segue來觸發(fā)转培。
- UIKit詢問要過渡到的目的視圖控制器它是否有自定義的過渡動畫代理。如果沒有浆竭,則UIKit將使用iOS自帶的過渡動畫浸须。
- 然后,UIKit通過過渡動畫代理邦泄,獲取到動畫控制器删窒。比如通過
animationControllerForPresentedController(_:presentingController:sourceController:)
方法獲取到動畫控制器,如果返回空顺囊,則使用默認的動畫控制器肌索。
- 然后,UIKit通過過渡動畫代理邦泄,獲取到動畫控制器删窒。比如通過
- 一旦找到了動畫控制器,UIKit構(gòu)建上下文對象特碳。
- 接著诚亚,UIKit通過動畫控制器的
transitionDuration(_:)
方法獲取動畫執(zhí)行時長晕换。
- 接著诚亚,UIKit通過動畫控制器的
- 再接著調(diào)用動畫控制器的
animateTransition(_:)
完成過渡動畫。
- 再接著調(diào)用動畫控制器的
- 最后動畫控制器調(diào)用上下文對象的
completeTransition(_:)
方法指示動畫完成站宗。圖4是官方文檔的一個過渡動畫的API角色示意圖闸准。
- 最后動畫控制器調(diào)用上下文對象的
2.3 實現(xiàn)Presentation過渡動畫
我們總共要實現(xiàn)三個動畫效果,一個是Presentation過渡動畫梢灭,一個是dismiss過渡動畫夷家,另外還有一個交互動畫。
Presentation的效果主要如下:
- 點擊卡片的時候敏释,卡片翻轉(zhuǎn)顯示第二個視圖库快,且第二個視圖初始大小跟卡片大小一樣。
- 第二個視圖放大至整個屏幕大小钥顽。
2.3.1 創(chuàng)建Presentation動畫控制器
我們創(chuàng)建一個名為FlipPresentAnimationController的類來完成Presentation動畫效果缺谴,這個類在我們上面說的角色中就是動畫控制器。
核心代碼如下耳鸯,代碼中有注解:
/*設置動畫時長函數(shù)*/
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 2.0;
}
/*執(zhí)行動畫的函數(shù)*/
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
//1 上下文對象transitionContext包含了參與過渡動畫的視圖
// 和視圖控制器信息湿蛔,可以通過對應的參數(shù)獲取。
CardViewController *fromVC = (CardViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewKey];
UIView *containerView = [transitionContext containerView];
RevealViewController *toVC = (RevealViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewKey];
//2 設置過渡目的視圖的初始大小和結(jié)束大小县爬。
// 初始大小為第一個視圖的卡片的大小阳啥,結(jié)束大小為整個屏幕大小。
BOOL hasViewForKey = [transitionContext respondsToSelector:@selector(viewForKey:)];
UIView *fromView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextFromViewKey] : fromVC.view;
UIView *toView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextToViewKey] : toVC.view;
CGRect initialFrame = self.originFrame;
CGRect finalFrame = hasViewForKey? toView.frame : [transitionContext finalFrameForViewController:toVC];
//3 獲取一個目的視圖的一個快照财喳。設置初始frame為initFrame察迟。
UIView *snapshot = [toView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = initialFrame;
snapshot.layer.cornerRadius = 25;
snapshot.layer.masksToBounds = YES;
//4 containerView加入目的視圖和快照視圖,并先隱藏目的視圖耳高。
// 我們的動畫都在containerView來實現(xiàn)扎瓶。
[containerView addSubview:toView];
[containerView addSubview:snapshot];
toView.hidden = YES;
//5 設置動畫視角,將快照視圖先沿Y軸旋轉(zhuǎn)到PI/2的位置泌枪。
[AnimationHelper persipectiveTransformForContainerView:containerView];
snapshot.layer.transform = [AnimationHelper yRotation:M_PI_2];
CGFloat duration = [self transitionDuration:transitionContext];
[UIView animateKeyframesWithDuration:duration delay:0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0/3 animations:^{
//6 將第一個視圖旋轉(zhuǎn)到-PI/2的位置概荷,方向是順時針
fromView.layer.transform = [AnimationHelper yRotation:-M_PI_2];
}];
[UIView addKeyframeWithRelativeStartTime:1.0/3 relativeDuration:1.0/3 animations:^{
//7 將快照視圖從PI/2的位置旋轉(zhuǎn)到軸線位置,也是順時針碌燕。正好接上6的旋轉(zhuǎn)效果误证。
snapshot.layer.transform = [AnimationHelper yRotation:0.0];
}];
[UIView addKeyframeWithRelativeStartTime:2.0/3 relativeDuration:1.0/3 animations:^{
//8 將快照視圖的frame放大至整個屏幕。
snapshot.frame = finalFrame;
}];
} completion:^(BOOL finished){
toView.hidden = NO; //顯示目的視圖
fromView.layer.transform = [AnimationHelper yRotation:0.0]; //恢復第一個視圖的位置
[snapshot removeFromSuperview]; //移除快照視圖
[transitionContext completeTransition:![transitionContext transitionWasCancelled]]; //通知UIKit動畫執(zhí)行完成
}
];
}
額外說明幾點:
- 注釋2這段代碼跟原文的swift的有點不一樣修壕,直接通過
transitionContext viewControllerForKey:UITransitionContextToViewKey
等函數(shù)取到的View Controller發(fā)現(xiàn)是nil愈捅,這樣就沒法取到動畫過程中的視圖信息。而通過transitionContext viewForKey:UITransitionContextToViewKey
取到的視圖是正常的慈鸠,看網(wǎng)上資料說可能是ios8的BUG蓝谨,沒有確切資料可以確認,如果是其他設置問題,麻煩大蝦們告知一下譬巫。
- 注釋2這段代碼跟原文的swift的有點不一樣修壕,直接通過
- 關于旋轉(zhuǎn)方向的問題稽亏,通過上一篇筆記我們總結(jié)了三維視圖中沿Y軸旋轉(zhuǎn)的正反方向,正方向為逆時針缕题。因此注釋5中我們的快照視圖顯示逆時針的轉(zhuǎn)到了PI/2的位置,而注釋6會先將第一個視圖轉(zhuǎn)到-PI/2的位置胖腾,動畫中的旋轉(zhuǎn)方向是以距離最近來旋轉(zhuǎn)烟零,因此第一個視圖會順時針旋轉(zhuǎn)PI/2,然后快照視圖也是順時針旋轉(zhuǎn)PI/2咸作,最后再試快照視圖放大到整個屏幕锨阿。
- 最后的
completeTransition
方法調(diào)用是必須的,如果不調(diào)用的話记罚,動畫結(jié)束后目的視圖將無法接受事件響應墅诡。
- 最后的
2.3.2 連接動畫控制器
在我們的CardViewController中加入動畫控制器初始化代碼。這里的CardViewController實現(xiàn)了UIViewControllerTransitioningDelegate協(xié)議桐智,我們要設置目的控制器的transitionDelegate為CardViewController末早。并實現(xiàn)代理的方法返回我們剛剛創(chuàng)建的動畫控制器。代碼如下:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
self.flipPresentAnimationController.originFrame = self.cardView.frame;
return self.flipPresentAnimationController;
}
// 在CardViewController的prepareSegue方法中说庭,設置了transitionDelegate。
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
......
revealViewController.transitioningDelegate = self;
}
2.4 實現(xiàn)dismiss過渡動畫
dismiss的過渡動畫原理類似,不過多介紹了纬凤,實現(xiàn)功能是:
- 第二個視圖的圖片先縮小到第一個視圖的卡片大小胸墙。
- 兩個視圖先后翻轉(zhuǎn),最終回到初始位置捆憎。
代碼如下:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
CardViewController *fromVC = (CardViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewKey];
UIView *containerView = [transitionContext containerView];
RevealViewController *toVC = (RevealViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewKey];
BOOL hasViewForKey = [transitionContext respondsToSelector:@selector(viewForKey:)];
UIView *fromView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextFromViewKey] : fromVC.view;
UIView *toView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextToViewKey] : toVC.view;
CGRect initialFrame = fromView.frame;
CGRect finalFrame = self.destinationFrame;
UIView *snapshot = [fromView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = initialFrame;
snapshot.layer.cornerRadius = 25;
snapshot.layer.masksToBounds = YES;
[containerView addSubview:toView];
[containerView addSubview:snapshot];
fromView.hidden = YES;
[AnimationHelper persipectiveTransformForContainerView:containerView];
toView.layer.transform = [AnimationHelper yRotation:-M_PI_2];
CGFloat duration = [self transitionDuration:transitionContext];
[UIView animateKeyframesWithDuration:duration delay:0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0/3.0 animations:^{
snapshot.frame = finalFrame;
}];
[UIView addKeyframeWithRelativeStartTime:1.0/3.0 relativeDuration:1.0/3.0 animations:^{
snapshot.layer.transform = [AnimationHelper yRotation:M_PI_2];
}];
[UIView addKeyframeWithRelativeStartTime:2.0/3.0 relativeDuration:1.0/3.0 animations:^{
toView.layer.transform = [AnimationHelper yRotation:0.0];
}];
} completion:^(BOOL finished){
fromView.hidden = NO;
[snapshot removeFromSuperview];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}
];
}
當然舅柜,也少不了要在代理類中關聯(lián)好dismiss的動畫控制器。
2.5 實現(xiàn)交互動畫
2.5.1 交互動畫示例
iPhone上面的設置APP就是交互動畫的一個很典型的例子躲惰,如圖5所示致份,從左邊緣開始滑動,過渡動畫的進度是跟隨你的手指滑動的位置來確定的(比如坐標X超過了多少則表示切換到下一個視圖础拨,否則切回上一個視圖知举。
2.5.2 交互動畫原理
交互動畫通過交互控制器來控制,為了實現(xiàn)交互動畫太伊,過渡動畫代理需要額外提供一個交互控制器雇锡。交互控制器只要實現(xiàn)了UIViewControllerInteractiveTransitioning協(xié)議即可,它響應觸控事件僚焦,通過交互控制器锰提,動畫會隨著手勢拖動逐漸展開而不是像之前那樣直接執(zhí)行完畢。
iOS提供了一個UIPercentDrivenInteractiveTransition類,它實現(xiàn)了UIViewControllerInteractiveTransitioning協(xié)議立肘,我們在例子中要用到這個類边坤。
2.5.3 創(chuàng)建交互過渡動畫
創(chuàng)建交互動畫代碼如下,我們需要添加拖動事件響應谅年,在處理事件響應的函數(shù)handleGesture中茧痒,我們根據(jù)當前手勢狀態(tài)和所在的位置來進行處理。注意到gestureRecognizer.view
是對應的目的視圖也就是RevealViewController對應的View融蹂。而它的superview則是UITransitionView這個視圖旺订。
- (void)wireToViewController:(UIViewController *)viewController {
self.viewController = viewController;
[self prepareGestureRecognizerInView:viewController.view];
}
- (void)prepareGestureRecognizerInView:(UIView *)view {
UIScreenEdgePanGestureRecognizer *gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action: @selector(handleGesture:)];
gesture.edges = UIRectEdgeLeft;
[view addGestureRecognizer:gesture];
}
- (void)handleGesture:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
//1 獲取手勢當前的坐標點
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
CGFloat progress = (translation.x / 200);
progress = fminf(fmaxf(progress, 0.0), 1.0);
switch (gestureRecognizer.state) {
//2 開始手勢,設置開始交互的標識超燃,開始觸發(fā)dismissal操作区拳。
case UIGestureRecognizerStateBegan:
self.interactionInProgress = YES;
[self.viewController dismissViewControllerAnimated:YES completion:nil];
Break;
//3 手勢拖動,判斷當前的手勢橫軸坐標是否大于100意乓,大于100則設置過渡動畫完成樱调。
case UIGestureRecognizerStateChanged:
self.shouldCompleteTransition = progress > 0.5;
[self updateInteractiveTransition:progress];
Break;
//4 手勢取消,設置交互狀態(tài)為NO届良,并取消交互動畫笆凌。
case UIGestureRecognizerStateCancelled:
self.interactionInProgress = NO;
[self cancelInteractiveTransition];
Break;
//5 手勢結(jié)束,根據(jù)進度來判斷是取消還是完成交互動畫士葫。
case UIGestureRecognizerStateEnded:
self.interactionInProgress = NO;
if (!self.shouldCompleteTransition) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
default:
NSLog(@"Unsupported");
break;
}
在CardViewController中需要加入對應代碼才能呈現(xiàn)交互動畫菩颖,加入代碼如下:
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
return self.swipeInteractionControllers.interactionInProgress ? self.swipeInteractionControllers : nil;
}
/* 在CardViewController的prepareSegue方法中,
設置了transitionDelegate为障,加入交互動畫事件捕獲晦闰。*/
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
......
revealViewController.transitioningDelegate = self;
[self.swipeInteractionControllers wireToViewController:revealViewController];
}
至此整個動畫效果完成,完整代碼參見