iOS學習筆記(8)-自定義過渡動畫

這篇筆記翻譯自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效果的展示怎虫。

圖1 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 最終效果圖

2 過渡動畫API探究

過渡動畫API涉及到的一些角色如圖3所示谅将,下面分開介紹:

圖3 過渡動畫API角色

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 過渡動畫流程

    1. 你觸發(fā)一個過渡動作√睿可以通過編碼或者segue來觸發(fā)转培。
    1. UIKit詢問要過渡到的目的視圖控制器它是否有自定義的過渡動畫代理。如果沒有浆竭,則UIKit將使用iOS自帶的過渡動畫浸须。
    1. 然后,UIKit通過過渡動畫代理邦泄,獲取到動畫控制器删窒。比如通過 animationControllerForPresentedController(_:presentingController:sourceController:)方法獲取到動畫控制器,如果返回空顺囊,則使用默認的動畫控制器肌索。
    1. 一旦找到了動畫控制器,UIKit構(gòu)建上下文對象特碳。
    1. 接著诚亚,UIKit通過動畫控制器的 transitionDuration(_:)方法獲取動畫執(zhí)行時長晕换。
    1. 再接著調(diào)用動畫控制器的animateTransition(_:)完成過渡動畫。
    1. 最后動畫控制器調(diào)用上下文對象的completeTransition(_:)方法指示動畫完成站宗。圖4是官方文檔的一個過渡動畫的API角色示意圖闸准。
圖4 過渡動畫角色示意圖2

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í)行完成
        }
     ];
}

額外說明幾點:

    1. 注釋2這段代碼跟原文的swift的有點不一樣修壕,直接通過transitionContext viewControllerForKey:UITransitionContextToViewKey等函數(shù)取到的View Controller發(fā)現(xiàn)是nil愈捅,這樣就沒法取到動畫過程中的視圖信息。而通過transitionContext viewForKey:UITransitionContextToViewKey取到的視圖是正常的慈鸠,看網(wǎng)上資料說可能是ios8的BUG蓝谨,沒有確切資料可以確認,如果是其他設置問題,麻煩大蝦們告知一下譬巫。
    1. 關于旋轉(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咸作,最后再試快照視圖放大到整個屏幕锨阿。
    1. 最后的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超過了多少則表示切換到下一個視圖础拨,否則切回上一個視圖知举。

圖5 交互動畫示例

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];
}

至此整個動畫效果完成,完整代碼參見

3 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鳍怨,一起剝皮案震驚了整個濱河市呻右,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鞋喇,老刑警劉巖声滥,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侦香,居然都是意外死亡落塑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門罐韩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憾赁,“玉大人,你說我怎么就攤上這事散吵×迹” “怎么了蟆肆?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晦款。 經(jīng)常有香客問我炎功,道長,這世上最難降的妖魔是什么缓溅? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任蛇损,我火速辦了婚禮,結(jié)果婚禮上坛怪,老公的妹妹穿的比我還像新娘淤齐。我一直安慰自己,他們只是感情好酝陈,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毁涉,像睡著了一般沉帮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贫堰,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天穆壕,我揣著相機與錄音,去河邊找鬼其屏。 笑死喇勋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的偎行。 我是一名探鬼主播川背,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蛤袒!你這毒婦竟也來了熄云?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妙真,失蹤者是張志新(化名)和其女友劉穎缴允,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珍德,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡练般,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锈候。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薄料。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖泵琳,靈堂內(nèi)的尸體忽然破棺而出都办,到底是詐尸還是另有隱情嫡锌,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布琳钉,位于F島的核電站势木,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏歌懒。R本人自食惡果不足惜啦桌,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望及皂。 院中可真熱鬧甫男,春花似錦、人聲如沸验烧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碍拆。三九已至若治,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間感混,已是汗流浹背端幼。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弧满,地道東北人婆跑。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像庭呜,于是被迫代替她去往敵國和親滑进。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容