動畫與轉場,我的認爲在概念上並不複雜,只是在代碼的組織和形式上比較複雜,所以我嘗試先講講概念,再講講實現,讓思緒清晰一些。html
所謂動畫,就是在一段時間內,一些 view 的位置、顏色等屬性會逐漸變化的一個現象。那麼要完成一個動畫,咱們只須要肯定三點:動畫有多久、動畫涉及到哪些 view 、這些 view 都有哪些屬性改變了,說簡單點兒就是時間、元素、變化形式。明確了這三點,各類 API 的變化只是在代碼的簡潔性和複用度上不停的作文章而已。app
咱們說到,動畫的三個主要元素是時間、元素、變化形式,在元素這裏動畫並無作過多的約束,而從概念上講,轉場就是一個動畫的子集,其約束動畫的元素必須爲兩個元素,而且通常都是兩個 view controller 的主 view 進行的轉換(因此說轉場是針對兩個 vc 的動畫也沒啥大毛病)。ide
瞭解了動畫的關鍵概念,咱們來看看在 iOS 中,應該如何用代碼去描述這三個概念。動畫
第一種:使用UIView 的 begin/commit :ui
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:1.0f];// 這裏描述時間 _demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 這裏同時描述了元素和變化形式 [UIView commitAnimations];
第二種:直接經過 block 調用spa
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50); [UIView animateWithDuration:1.0f delay:1.0f // 這裏是時間 options:UIViewAnimationOptionCurveEaseIn // 這裏是一些封裝的變化形式 animations:^{ _demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 這裏同時描述了元素和變化形式 } completion:nil];
第三種:將對屬性的變化封裝到 CoreAnimation 對象中,而後應用到某個 view 的 layer 上code
CABasicAnimation *anima = [CABasicAnimation animationWithKeyPath:@"position"]; anima.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, SCREEN_HEIGHT/2-75)]; anima.toValue = [NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-75)];// 這裏描述了變化形式 anima.duration = 1.0f;// 這裏描述了時間 [_demoView.layer addAnimation:anima forKey:@"positionAnimation"];// 這裏則是描述了元素
這三種方式中,第一種是好久之前(iOS 4.0)使用的形式,不管是便捷度和複用度都不是很高。第二種是最方便的,可是缺點在於很差複用(除非把 block 保存起來,能夠在一個 vc 中實現複用)。第三種是一種很容易複用的形式,將動畫的三個元素中時間、變化形式單獨抽離出來,使得其能夠自由的應用在任意的元素上。(由此能夠看出,若是想要代碼的複用度更高,就須要不斷的減小一段代碼或者一個對象在概念上的職責)htm
前面咱們說過,轉場是針對於兩個特定的 view 的動畫,因此咱們須要先約定一下術語,假如咱們有兩個 VC A/B,咱們要從 A 轉換到 B,咱們稱呼 A 爲 presentingViewController(或者 fromViewController),稱呼 B 爲 presentedViewController(或者 toViewController)。當從 B 結束轉換回到 A 時,咱們仍然稱呼 A 爲 presentingViewController,B 爲 presentedViewController,可是咱們會稱呼 A 爲 toViewController ,而 B 爲 fromViewController。明白區別了麼?from/to 是針對一次動畫的,而 presented/presenting 是針對一次完整的轉場的。對象
雖然從概念上來講,轉場是一種特定的動畫,可是實際上轉場須要考慮的事情要比通常的動畫要多(好比通常的動畫可能不須要交互,可是轉場可能須要),所以在代碼的組織結構上,轉場使用了更多的對象去更加細緻的拆分概念上的職責。blog
最基本的一種實現轉場的方式,很是相似於上面所說的第二種動畫的表現形式:
[self transitionFromViewController:self.fromVC toViewController:self.toVC // 元素 duration:5 // 時間 options:UIViewAnimationOptionCurveEaseInOut // 變化形式的封裝 animations:^{ CGRect frame = self.thirdVC.view.frame; frame.origin.y = 150; self.thirdVC.view.frame = frame; } completion:nil];
這個轉場通常在容器 VC 中使用。缺點實際上是和最基本的動畫調用方式同樣,都是不容易複用,而且使用場景有限,只能用在容器 vc 中,不能用在兩個平級的 vc 中。也就是說,爲了從 A 轉到 B,咱們必須首先有一個 C ,而後讓 A、B 做爲 C 的 child vc ,顯然很不方便啊,那麼咱們就須要考慮一種新的代碼組織形式,將轉場的職責進行拆分。
在一次自定義的轉場中,咱們會將指責進行以下形式的劃分:
首先,咱們須要有兩個 vc(廢話(╬▔皿▔)),而後設置 presentingVC 的 modalPresentationStyle
爲 UIModalPresentationCustom
,接下來將 presentingVC 的 transitioningDelegate
屬性指向一個實現了 UIViewControllerTransitioningDelegate
協議的對象上。這樣就告訴 UIKit 任意一個 vc 用 prensentViewController:animated:completion
方法展現 presentingVC 時,presentingVC 的轉場效果徹底由 transitioningDelegate
屬性所指向的對象來負責。
// PresentingVC self.transitioningDelegate = [TransitionDelegate new];// 轉場效果這一部分職責從 vc 中剝離了出去
TransitionDelegate
是一個實現了 UIViewControllerTransitioningDelegate
協議的對象,在這個協議中又將轉場效果的職責分爲三個對象去負責:一個負責轉場動畫效果的 Animator,一個負責轉場過程當中交互的 InteractiveAnimator,和一個則負責轉場過程當中 view 的層級關係以及在不一樣屏幕上的適配。這三個對象的職責,在代碼上的表現形式就是將UIViewControllerTransitioningDelegate
的內容分爲三組。咱們來一個個瞭解一下。
這個對象負責轉場的動畫效果,具體點兒來講,他決定了可見的視圖從 PresentingViewController 的 view 到可見視圖變爲 PresentedViewController 的 view 的過程當中,兩個 view 應該如何去變化。在UIViewControllerTransitioningDelegate
協議中,該對象能夠經過兩個方法返回:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
兩個方法中,前者決定了 present 過程當中的動畫效果,後者則決定了 dismiss 過程當中的動畫效果。而具體 Animator 如何去控制轉場過程當中的動畫,咱們就須要看看 UIViewControllerAnimatedTransitioning
這個協議中的方法都有些什麼:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
第一個方法決定了轉場的時間,第二個方法則是經過一個 transitionContext 對象傳遞給 Animator 對象轉場過程當中的 FromVC/ToVC,以及 containerView ,也就是轉場過程當中的元素,而後咱們就能夠經過 UIKit 的動畫 API 決定轉場的變化形式了。在這個方法中咱們要作的就是:
獲得 ToVC 的 view,設定其初始狀態
將 ToVC 的 view 添加到 containerView 中
經過任意一種動畫形式對 ToVC 的 view 作動畫,而後在結束的時候調用 transitionContext
對象的 completeTransition:
方法告知系統咱們的動畫作完了。
更具體的內容,能夠參見以下的一段代碼:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { // 獲取全部須要的 view 以及 vc UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containerView = transitionContext.containerView; // 設定初始狀態 toVC.view.frame = CGRectMake(0, - CGRectGetHeight(fromVC.view.frame), CGRectGetWidth(fromVC.view.frame), CGRectGetHeight(fromVC.view.frame)); toVC.view.alpha = 0.0f; // 必定要本身手動添加 subview, fromVC 的 view UIKit 會自動移除,可是 UIKit 不會自動添加 toVC 的 view [containerView addSubview:toVC.view]; // 獲取動畫時間 NSTimeInterval duration = [self transitionDuration:transitionContext]; // 開始動畫 [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ toVC.view.alpha = 1.0f; toVC.view.frame = fromVC.view.frame; } completion:^(BOOL finished) { if (finished) { [transitionContext completeTransition:YES]; NSLog(@"finished"); } }]; }
對於通常的轉場來講,實現了基本的動畫效果可能就夠了,可是實際開發中,咱們可能對於轉場有更加深刻的需求,好比但願轉場可以帶有用戶交互,像系統的全局返回手勢那樣,這個時候,咱們就須要額外返回一個 InteractiveAnimator
來告訴 UIKit 隨着用戶的手勢變化,動畫應該執行到百分之多少或者是否須要取消,這些操做咱們均可以經過 context 對象中的方法來完成:
- (void)cancelInteractiveTransition; - (void)finishInteractiveTransition; - (void)updateInteractiveTransition:(CGFloat)percentComplete;
所以,若是想實現一個交互式的轉場,咱們須要作以下幾件事兒:
在 presentingVC 中添加一個 button 點擊之外的『觸發器』(通常來講,都是一個 Gesture Recognizer),好比添加一個邊緣滑動的 Gesture Recognizer,當一個邊緣滑動開始時,咱們在對應的回調中 present PresentedVC。
在 presentedVC 的 transitionDelegate 中,返回一個 InteractiveAnimator。
在 Animator 中的 startInteractiveTransition:
方法中將 context 對象保存起來。
想辦法將 Gesture Recognizer 傳遞給 InteractiveAnimator,使得在 Animator 中能夠獲取當前手勢的信息,結合 context 對象中的 containerView 等信息,咱們能夠知道當前手勢在 view 中更具體的信息。
根據預先設定好的規則,在 Gesture Recognizer 的回調中調用 context 對象的 cancel/finished/update 方法
好比,若是咱們想實現一個邊緣滑動的交互動畫效果,咱們能夠這麼來寫代碼:
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext { // 把 context 對象保存起來 self.transitionContext = transitionContext; [super startInteractiveTransition:transitionContext]; } // 根據手勢的偏移來計算當前動畫應該有的完成度 - (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture { // 根據 container view 以及 gesture recognizer 計算偏移量 UIView *transitionContainerView = self.transitionContext.containerView; CGPoint locationInSourceView = [gesture locationInView:transitionContainerView]; // 根據偏移量得出百分比 CGFloat width = CGRectGetWidth(transitionContainerView.bounds); return (width - locationInSourceView.x) / width; } // gesture recognizer 的回調 - (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { switch (gestureRecognizer.state) { case UIGestureRecognizerStateBegan: break; case UIGestureRecognizerStateChanged: // 計算百分比,並返回 [self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]]; break; case UIGestureRecognizerStateEnded: // 根據預先設定的閾值決定是結束仍是取消,這裏咱們設定 view 中間是分界線 if ([self percentForGesture:gestureRecognizer] >= 0.5f) [self finishInteractiveTransition]; else [self cancelInteractiveTransition]; break; default: // 其餘狀況,取消轉場 [self cancelInteractiveTransition]; break; } }
以上的兩組接口,分別讓咱們自定義了轉場過程當中的動畫、動畫執行百分比,可是不論是哪一個,都會在最後將 fromVC 的 view 從 containerView 上移除,而且整個轉場過程當中若是咱們想添加一些額外的 view 也是沒法作到的。若是想要實現這些功能,就須要咱們建立一個 UIPresentationController
的子類,而後重載其 四個轉場的生命週期方法:
presentationTransitionWillBegin
presentationTransitionDidEnd:
dismissalTransitionWillBegin
dismissalTransitionDidEnd:
在重載這些方法時,咱們也可使用其 presentingViewController 屬性的 transitionCoordinator 來同步的爲咱們新添加的 view 執行動畫(所謂同步就是和咱們以前在 Animator 中寫的動畫同時執行)。
好比,咱們能夠爲咱們添加的一個 dimming view 的透明度設置一個動畫:
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator; self.dimmingView.alpha = 0.f; [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) { self.dimmingView.alpha = 0.5f; } completion:NULL];
總結一下來講,若是咱們想要使用 UIPresentationController ,咱們須要:
設置 presentedVC 的 presentStyle 爲 UIModalPresentationCustom
在 presentedVC 的 transitionDelegate 中返回咱們建立的 UIPresentationController
的子類
在子類中重載轉場生命週期的四個方法,添加咱們所須要的自定義的view