什么是轉場動畫:
轉場動畫說的直接點就是你常見的界面跳轉的時候看到的動畫效果,我們比較常見的就是控制器之間的Push和Pop,還有Present和Dismiss的時候設置一下系統給我們的modalTransitionStyle,以及通過手勢的左滑或者是右滑的轉場等等,這些就是我們比較常見的,當然很大部分APP轉場的方式也是我們上面說的常見的。我自己的建議和理解,轉場動畫能幫你加深理解、總結你對動畫的學習,但不要輕易在你的項目中大量的去嘗試,還是覺得動畫用的好就有點睛之筆的感覺,但若是大量的使用,很容易給人造成審美和視覺疲勞。當然這個可能就是對你們設計或者是產品功力的考驗了。要他們真的做出了點睛的動畫也是希望我們搞的定。
我們要說的肯定就不是我們常見的轉場了,在那些特殊的轉場動畫面前我們應該怎么做。針對這問題,我們分開一步一步的解析,不想讓篇幅太長了,我們在這里會分成兩篇,第一篇主要說理論的地方和穿插一些小案例,第二篇我會把自己最近學習過得一下案例最好全都分享給大家,一起學習。
一:Transition(n. 過渡;轉變;[分子生物] 轉換;變調)
這個單詞估計就是我們轉場的基礎了,留給英文可能不是那么6的你我他。在下面你肯定會大量的看到它,對於這個Transition(轉場)過程中視圖控制器和其對應的視圖在結構上的變化我在巧神的博客中看到這張圖,說實話,不太理解這張圖表達了的是什么,把這張圖給大家分享出來,你要理解的話可以留言大家討論一下,接下來先說說在理解轉場之前我們需要理解的幾個概念:
*** 官方支持以下幾種方式的自定義轉場:
1、我們最常見的在 UINavigationController 中 push 和 pop;
2、也是比較常見的在 UITabBarController 中切換 Tab;
3、Modal 轉場:presentation 和 dismissal,俗稱視圖控制器的模態顯示和消失,僅限於modalPresentationStyle屬性為 UIModalPresentationFullScreen 或 UIModalPresentationCustom 這兩種模式,這里要區分上面說的modalTransitionStyle,下面會區分這兩個屬性。
4、UICollectionViewController 的布局轉場:UICollectionViewController 與 UINavigationController 結合的轉場方式;
*** 區分modalTransitionStyle和modalPresentationStyle,首先它們倆都是UIViewController的屬性:
1、先說說 modalTransitionStyle,這個是控制器跳轉時系統給的幾個動畫風格,這個在iPhone上用的比較多:
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) { // 默認的從下到上 UIModalTransitionStyleCoverVertical = 0, // 翻轉 UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED, // 漸顯 UIModalTransitionStyleCrossDissolve, // 類似你翻書時候的效果 UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, };
2、再說說modalPresentationStyle,這個是彈出時控制器的風格,modalPresentationStyle的分割在iPad上面統統有效,但在iPhone和iPod touch上面系統始終已UIModalPresentationFullScreen模式顯示presentedController,關於modalPresentationStyle在下面也通過注釋說一下:
typedef NS_ENUM(NSInteger, UIModalPresentationStyle) { //presented控制器充滿全屏,如果彈出VC的wantsFullScreenLayout設置為YES的,則會填充到狀態欄下邊,否則不會填充到狀態欄之下.iPhone默認是這個 UIModalPresentationFullScreen = 0, //presented控制器的高度和當前屏幕高度相同,寬度和豎屏模式下屏幕寬度相同,剩余未覆蓋區域將會變暗並阻止用戶點擊,這種彈出模式下,豎屏時跟UIModalPresentationFullScreen的效果一樣,橫屏時候兩邊則會留下變暗的區域 UIModalPresentationPageSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, //presented控制器的高度和寬度均會小於屏幕尺寸,presented VC居中顯示,四周留下變暗區域。 UIModalPresentationFormSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, //presented控制器的彈出方式和presenting VC的父VC的方式相同。 UIModalPresentationCurrentContext NS_ENUM_AVAILABLE_IOS(3_2), //自定義 UIModalPresentationCustom NS_ENUM_AVAILABLE_IOS(7_0), UIModalPresentationOverFullScreen NS_ENUM_AVAILABLE_IOS(8_0), UIModalPresentationOverCurrentContext NS_ENUM_AVAILABLE_IOS(8_0), // http://www.15yan.com/story/jlkJnPmVGzc/ 在iPad上彈出控制器 UIModalPresentationPopover NS_ENUM_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED, // None UIModalPresentationNone NS_ENUM_AVAILABLE_IOS(7_0) = -1, };
*** Presented和Presenting fromView和toView
這個借助於我看博客的時候看到的同行總結的比較好的一句話和圖示說明來說一下:
Presented和Presenting是一組相對的概念,它不受present或dismiss的影響,如果是從A視圖控制器present到B,那么A總是B的presentingViewController
,B總是A的presentedViewController
。
順便借助於這張圖示說明,我們還可以理解一下fromView和toView這個兩個概念:
fromView表示當前視圖toView表示要跳轉到的視圖。如果是從A視圖控制器present到B,則A是fromView,B是toView。從B視圖控制器dismiss到A時,B變成了fromView,A是toView。在后面在參考博客中我都會把這些博客鏈接總結發出來。
二:轉場的幾個關鍵點
轉場最關鍵的地方就是幾個轉場協議,我們分開一個一個的說這幾個轉場的協議,在說這幾個協議的過程中穿插一些簡單的轉場動畫的案列,這些例子最后都會上傳到git上去。
1、 轉場協議:UIViewControllerTransitioningDelegate
這個協議里面有五個方法,先看看這五個方法,然后把這幾個方法逐個解析一下:
先給大家再普及一個單詞!哈哈...最后兩個方法有這個interaction( 相互作用;[數] 交互作用),你就理解它就是交互
** 下面是這幾個方法的代碼的注釋,它的一些注意的地方以及一些解釋在下面代碼的注釋中有,看了下面的方法,我們也就大概掌握了這個協議:
#pragma mark - UIViewControllerTransitioningDelegate /* 不管是 present 還是dismiss 要是調用interactionControllerForPresentation 或者是 interactionControllerForDismissal 返回值是nil,就會走下面animationControllerForPresentedController和animationControllerForDismissedController方法 要是不是nil,就不會走下面這兩個方法了, 在我們這里也就是用手勢測試的時候是不會走的,點擊present或 者是dismiss會走 */ // 這個方法返回一個遵守 <UIViewControllerAnimatedTransitioning> 協議的對象 // 其實返回的就是PresentedController控制器的動畫 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ //ForithmAnimation這個類可以在Demo中去看,下面我們也會說 return [ForithmAnimation new]; } // 這個方法和上面的解釋是類似的,只不過這里的控制器就是DismissedController - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ //ForithmAnimation 遵守 UIViewControllerAnimatedTransitioning 協議 return [ForithmAnimation new]; } // UIKit還會調用代理的interactionControllerForPresentation:方法來獲取交互式控制器,如果得到了nil則執行非交互式動畫 // 如果獲取到了不是nil的對象,那么UIKit不會調用animator的animateTransition方法,而是調用交互式控制器的startInteractiveTransition:方法。 - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator{}; // 這個方法是在dismiss的時候的時候調用,也是交互轉場執行的時候 - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{}; // 這個方法的返回值是UIPresentationController // UIPresentationController提供了四個函數來定義present和dismiss動畫開始前后的操作,這個我們在下面再具體的詳細說 - (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0){};
我們接着我說第二點動畫協議,這兩個說完了,我們說一個簡單的實例.
2、 動畫協議: UIViewControllerAnimatedTransitioning
還是老辦法,我們要用的它的話我們得先了解它都有些什么東西,點進去看看它里面的方法,把方法也解釋一下:
這個協議看的出來還是很簡單的,終於不用那么長了是嗎?哈哈.....
這兩個方法我們就不在代碼里面添加注釋說明了,在這里一句話描述一下:
a: 第一個方法是返回動畫執行的一個時間,建議設置在0.5以內吧。
b: 核心方法,轉場動畫我們就是在這個方法里面添加的,所以,一般講動畫的文章,轉場動畫都會在最后說說,因為它需要基本動畫作為一個基礎。
3、 轉場環境協議 UIViewControllerContextTransitioning
不知道你有沒有注意到上面我們說的 UIViewControllerAnimatedTransitioning 協議的第二個方法里面,有個參數叫transitionContext 它的類型呢?它的最主要的作用就是獲取到轉場上下文,在接下來的例子中,大家注意下這個方法:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
在它里面最開始的時候,我們都會去獲取 fromViewController、或者是toViewController亦或是后面的fromView、toView等這些內容,還有contentView,這些就是他最主要的作用。
EXAMPLE-ONE:
下面的GIF實例分為三個,我們用我們上面說的第一點個第二點要素就能完成的是第一種,逐漸顯示,第二種的話需要我們接下來要說的第三點交互控制器協議方法面的東西,我們就在下面第三點說完再說;
這里是我學習這些內容的原文的博客的地址大家可以去看看原文,原文鏈接Demo還有Swift版本的Demo給大家,感謝作者!
Demo的下載地址我在這里給大家,我們現在說的就先是第一種:逐漸出現的轉場
前面的用UICollectionView寫的那個圈圈,哈哈.....圈圈代碼在ViewController里面,重要的其實就是每一個attributes的center屬性,很簡答的就不說了,相信大家也都懂。
重點代碼我們說說,在這里說過的我們在下面的代碼中就會一筆帶過不在解釋了:
// 點擊跳轉事件 -(void)presentNextControllerClicked{ // 跳轉到這個控制器ForithmToViewController,當然是繼承與UIViewController ForithmToViewController * toViewController =[[ForithmToViewController alloc]init]; toViewController.modalPresentationStyle = UIModalPresentationFullScreen; // NOTE:轉場的關鍵就是這個代理 transitioningDelegate // 指定了這個代理就需要遵守UIViewControllerTransitioningDelegate這個協議 // 協議里面的東西點進去可以仔細看看,我們指定toViewController的transitioningDelegate是我們的ForithmFromViewController,也就是 // fromViewController,這樣我們的fromViewController就要遵守這個協議 toViewController.transitioningDelegate = self; [self presentViewController:toViewController animated:YES completion:nil]; } #pragma mark - UIViewControllerTransitioningDelegate // 這個方法返回一個遵守 <UIViewControllerAnimatedTransitioning> 協議的對象 // 其實返回的就是PresentedController控制器的動畫 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ return [ForithmAnimation new]; } // 這個方法和上面的解釋是類似的,只不過這里的控制器就是DismissedController - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ //ForithmAnimation 遵守 UIViewControllerAnimatedTransitioning 協議 return [ForithmAnimation new]; }
現在這個重點就落在我們這兒,遵守了UIViewControllerAnimatedTransitioning協議的ForithmAnimation:順便也提一下這就是轉場動畫API強大的地方,它們都是一些協議API,不管你是誰,只要遵守這些個協議就OK,便捷了許多。也利於我們封裝,這也就是那些第三方的轉場庫你拿來就能直接用的原因。
接着說我們的ForithmAnimation,讓它遵守<UIViewControllerAnimatedTransitioning>,我們看下協議的方法,你可以看到重點全都在我們上面說的動畫方法里面:
// 這個方法簡單,就是轉場動畫執行的時間 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{ return 0.35; } // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{ // fromViewController UIViewController * fromViewController = [transitionContext viewControllerForKey:( UITransitionContextFromViewControllerKey)]; // toViewController UIViewController * toViewController = [transitionContext viewControllerForKey:( UITransitionContextToViewControllerKey)]; /* typedef NS_ENUM(NSInteger, UIModalTransitionStyle) { // 默認的從下到上 UIModalTransitionStyleCoverVertical = 0, // 翻轉 UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED, // 漸顯 UIModalTransitionStyleCrossDissolve, // 類似你翻書時候的效果 UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, };*/ // UIView * contentView = [transitionContext containerView]; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; fromView.frame = [transitionContext initialFrameForViewController:fromViewController]; toView.frame = [transitionContext finalFrameForViewController:toViewController]; fromView.alpha = 1.0f; toView.alpha = 0.0f; // 在present和,dismiss時,必須將toview添加到視圖層次中 [contentView addSubview:toView]; // 獲取執行時長 NSTimeInterval transitionDuration = [self transitionDuration:transitionContext]; [UIView animateWithDuration:transitionDuration animations:^{ fromView.alpha = 0.0f; toView.alpha = 1.0; } completion:^(BOOL finished) { //transitionWasCancelled 這個方法判斷轉場是否已經取消了,下面的completeTransition設置轉場完成 //動畫結束后一定要調用completeTransition方法 //通過transitionWasCancelled()方法來獲取轉場的狀態,使用completeTransition:來完成或取消轉場。 BOOL wasCancelled = [transitionContext transitionWasCancelled]; [transitionContext completeTransition:!wasCancelled]; }]; }
上面方法,一個簡單的自定義轉場我們就完成了,明白了上面這第一點個第二點的要素,理解這個轉場相信對你也不是什么問題,我們接着往下說。
4、 交互控制器協議 UIViewControllerInteractiveTransitioning
說這個UIViewControllerInteractiveTransitioning你就得先知道這個UIPercentDrivenInteractiveTransition,官方的鏈接給大家,先看的可以去看看,這是一個實現了UIViewControllerInteractiveTransitioning接口的類,為我們預先實現和提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。利用手勢來完成這個轉場,UIPercentDrivenInteractiveTransition為我們提供了很大的便利:
為了我們的篇幅考慮,不想一篇太長了,不然真的會沒有耐心看下去,我們在這里就簡單看看這個UIPercentDrivenInteractiveTransition的一些方法,它里面的一些屬性什么的我們就不說了,大家可以自己去看看:
它里面的方法就這四個,簡單說下這四個方法:
a: 第一個方法是暫停交互
b: 第二個是更新方法,一般交互時候的進度更新就在這個方法里面
c: 第三個是取消交互
d: 第四個的話就是設置交互完成
EXAMPLE-TWO
現在來說第二個轉場的實現:
1、這個轉場需要一個最基本的 UIScreenEdgePanGestureRecognizer 手勢,它是一個屏幕邊緣滑動手勢,這個手勢是繼承自UIPanGestureRecognizer滑動手勢的。這個是手勢說一點,就是它的 edges 屬性,你要往左邊拉動轉場的話你就需要設置這個屬性為UIRectEdgeRight,一個很簡單的理解就是往左邊拉動你需要設置它相應右邊的滑動手勢,這樣理解就OK。
//UIScreenEdgePanGestureRecognizer:UIPanGestureRecognizer //添加屏幕邊緣滑動手勢 UIScreenEdgePanGestureRecognizer * interactiveTransitionRecognizer; interactiveTransitionRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(interactiveTransitionRecognizerAction:)]; // 響應右邊的滑動事件 interactiveTransitionRecognizer.edges = UIRectEdgeRight; [self.view addGestureRecognizer:interactiveTransitionRecognizer];
2、接下來就按照我們說的上面的第一個轉場的理解,設置我們 toViewController 的 transitioningDelegate 代理,接下來的是就需要我們再這個代理中去看了,大家先別着急去看,想一想,我們說的代理那五個方法里面和交互有關的兩個,前面我們說過,設置了交互就不走動畫方法了,交互哪里你需要返回的就是一個上面我們說的遵守UIViewControllerInteractiveTransitioning協議的類,這時候上面說的UIPercentDrivenInteractiveTransition就華麗的出場了,注意下面這個方法,當然這是Presentation,我們的Dismissal也是大家可以去Demo里面看,道理是一樣的:
// UIKit還會調用代理的interactionControllerForPresentation:方法來獲取交互式控制器,如果得到了nil則執行非交互式動畫 // 如果獲取到了不是nil的對象,那么UIKit不會調用animator的animateTransition方法,而是調用交互式控制器的startInteractiveTransition:方法。 // interaction 交互 - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator { // 說說這里的SwipTransitionInteractionController 控制器, // 它是繼承 UIPercentDrivenInteractiveTransition 的,而這個UIPercentDrivenInteractiveTransition是遵守了 // UIViewControllerInteractiveTransitioning 協議的,所以這里初始化返回這個是沒有問題的 // 是手勢操作,就返回這個交互式控制器 if (self.gestureRecognizer) return [[SwipTransitionInteractionController alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge]; else return nil; }
看了上面的代碼,我們的UIPercentDrivenInteractiveTransition就算出場了,SwipTransitionInteractionController是繼承自UIPercentDrivenInteractiveTransition,你也知道UIPercentDrivenInteractiveTransition遵守了UIViewControllerInteractiveTransitioning協議,這里你也就應該理解我們初始化SwipTransitionInteractionController返回的意思了。
接着看我們SwipTransitionInteractionController里面核心的代碼:
/** 前面代理通過 interactionControllerForPresentation 方法獲取交互控制器的時候,手勢返回的就是SwipTransitionInteractionController,這個時候就會調用這個方法 interactive 交互 */ -(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ [super startInteractiveTransition:transitionContext]; self.transitionContext = transitionContext; } // 手勢觸發該方法 -(void)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer{ switch (gestureRecognizer.state){ case UIGestureRecognizerStateBegan: break; case UIGestureRecognizerStateChanged: // 調用updateInteractiveTransition來更新動畫進度 // 里面嵌套定義 percentForGesture 方法計算動畫進度 [self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]]; break; case UIGestureRecognizerStateEnded: //判斷手勢位置,要大於一般,就完成這個轉場,要小於一半就取消 if ([self percentForGesture:gestureRecognizer] >= 0.5f) // 完成交互轉場 [self finishInteractiveTransition]; else // 取消交互轉場 [self cancelInteractiveTransition]; break; default: [self cancelInteractiveTransition]; break; } } // 計算動畫進度 -(CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture{ UIView * transitionContainerView = self.transitionContext.containerView; // 手勢滑動 在transitionContainerView中 的位置 // 這個位置判斷的方法可以具體根據你的需求確定 CGPoint locationInSourceView = [gesture locationInView:transitionContainerView]; CGFloat width = CGRectGetWidth(transitionContainerView.bounds); CGFloat height = CGRectGetHeight(transitionContainerView.bounds); if (self.edge == UIRectEdgeRight) return (width - locationInSourceView.x) / width; else if (self.edge == UIRectEdgeLeft) return locationInSourceView.x / width; else if (self.edge == UIRectEdgeBottom) return (height - locationInSourceView.y) / height; else if (self.edge == UIRectEdgeTop) return locationInSourceView.y / height; else return 0.f; }
上面的代碼有幾個點說一下:
1、大家注意一下初始化的時候我們使用一個手勢去接收傳遞到我們SwipTransitionInteractionController的手勢,這也是下面手勢事件能夠執行的原因;
2、這個startInteractiveTransition方法是我們UIViewControllerInteractiveTransitioning協議里面的方法,這個最主要的功能及時獲取我們的交互上下文
self.transitionContext = transitionContext 也就是這句代碼
3、再有一點就是我們的gestureRecognizeDidUpdate手勢方法里面的更新進度以及取消和完成了,也就這幾個地方大家需要注意點;
NOTE: 看看下面的打印日志
fromView = nil
但我們的fromViewController.View 確實是存在的,在上面的Demo中你可以看一下打印,確實是這樣:以前看博客有同行說:
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView * fromView = fromViewController.View 是等價的,這樣說應該吃不成立的。
然后在這里:TransitionAnimation 學習筆記 開頭給出了答案,再理解一下。
Demo的下載地址這里再發一次: 這里是Demo的下載地址
END:
由於篇幅的原因,剩下的幾點要素我們在下一篇當中接着說,當然你看到這篇文章的時候,第二篇我也肯定是總結完了的。不然會看着斷片的。就像前面說的那樣,在第二篇當中多給一些實際的例子給大家參考。共同學習。
最后就是我們的Telegram群的事,要是有需要的伙伴,我創建了一個Telegram iOS 交流群,你要是對Telegram有興趣,或者是有什么問題的,可以在群里來找我,在我博客分類中有一個系列是專門說這個Telegram,最基本的編譯過程里面有,大家可以參考一下。系列的文章我肯定會接着更新的。
群號粘貼這里:485718322
iOS 轉場動畫探究(二) 我們接着看..........