從 iOS 的動畫說到轉場

動畫與轉場,我的認爲在概念上並不複雜,只是在代碼的組織和形式上比較複雜,所以我嘗試先講講概念,再講講實現,讓思緒清晰一些。html

什麼是動畫(Animation)?

所謂動畫,就是在一段時間內,一些 view 的位置、顏色等屬性會逐漸變化的一個現象。那麼要完成一個動畫,咱們只須要肯定三點:動畫有多久、動畫涉及到哪些 view 、這些 view 都有哪些屬性改變了,說簡單點兒就是時間、元素、變化形式。明確了這三點,各類 API 的變化只是在代碼的簡潔性和複用度上不停的作文章而已。app

那,什麼是轉場(Transition)?

咱們說到,動畫的三個主要元素是時間、元素、變化形式,在元素這裏動畫並無作過多的約束,而從概念上講,轉場就是一個動畫的子集,其約束動畫的元素必須爲兩個元素,而且通常都是兩個 view controller 的主 view 進行的轉換(因此說轉場是針對兩個 vc 的動畫也沒啥大毛病)。ide

iOS 中動畫怎麼作?

瞭解了動畫的關鍵概念,咱們來看看在 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

iOS 中轉場怎麼作?

前面咱們說過,轉場是針對於兩個特定的 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 的 modalPresentationStyleUIModalPresentationCustom,接下來將 presentingVC 的 transitioningDelegate 屬性指向一個實現了 UIViewControllerTransitioningDelegate協議的對象上。這樣就告訴 UIKit 任意一個 vc 用 prensentViewController:animated:completion 方法展現 presentingVC 時,presentingVC 的轉場效果徹底由 transitioningDelegate 屬性所指向的對象來負責。

// PresentingVC
self.transitioningDelegate = [TransitionDelegate new];// 轉場效果這一部分職責從 vc 中剝離了出去

TransitionDelegate 是一個實現了 UIViewControllerTransitioningDelegate 協議的對象,在這個協議中又將轉場效果的職責分爲三個對象去負責:一個負責轉場動畫效果的 Animator,一個負責轉場過程當中交互的 InteractiveAnimator,和一個則負責轉場過程當中 view 的層級關係以及在不一樣屏幕上的適配。這三個對象的職責,在代碼上的表現形式就是將UIViewControllerTransitioningDelegate的內容分爲三組。咱們來一個個瞭解一下。

TransitionAnimator

這個對象負責轉場的動畫效果,具體點兒來講,他決定了可見的視圖從 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 決定轉場的變化形式了。在這個方法中咱們要作的就是:

  1. 獲得 ToVC 的 view,設定其初始狀態

  2. 將 ToVC 的 view 添加到 containerView 中

  3. 經過任意一種動畫形式對 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

對於通常的轉場來講,實現了基本的動畫效果可能就夠了,可是實際開發中,咱們可能對於轉場有更加深刻的需求,好比但願轉場可以帶有用戶交互,像系統的全局返回手勢那樣,這個時候,咱們就須要額外返回一個 InteractiveAnimator 來告訴 UIKit 隨着用戶的手勢變化,動畫應該執行到百分之多少或者是否須要取消,這些操做咱們均可以經過 context 對象中的方法來完成:

- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;

所以,若是想實現一個交互式的轉場,咱們須要作以下幾件事兒:

  1. 在 presentingVC 中添加一個 button 點擊之外的『觸發器』(通常來講,都是一個 Gesture Recognizer),好比添加一個邊緣滑動的 Gesture Recognizer,當一個邊緣滑動開始時,咱們在對應的回調中 present PresentedVC。

  2. 在 presentedVC 的 transitionDelegate 中,返回一個 InteractiveAnimator。

  3. 在 Animator 中的 startInteractiveTransition: 方法中將 context 對象保存起來。

  4. 想辦法將 Gesture Recognizer 傳遞給 InteractiveAnimator,使得在 Animator 中能夠獲取當前手勢的信息,結合 context 對象中的 containerView 等信息,咱們能夠知道當前手勢在 view 中更具體的信息。

  5. 根據預先設定好的規則,在 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;
    }
}

PresentationController

以上的兩組接口,分別讓咱們自定義了轉場過程當中的動畫、動畫執行百分比,可是不論是哪一個,都會在最後將 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 ,咱們須要:

  1. 設置 presentedVC 的 presentStyle 爲 UIModalPresentationCustom

  2. 在 presentedVC 的 transitionDelegate 中返回咱們建立的 UIPresentationController 的子類

  3. 在子類中重載轉場生命週期的四個方法,添加咱們所須要的自定義的view

擴展閱讀

相關文章
相關標籤/搜索