iOS 自定義轉(zhuǎn)場動畫 初窺

簡書上的所有內(nèi)容都可以在我的個人博客上找到

這兩天學習了一下自定義轉(zhuǎn)場動畫的內(nèi)容,剛開始看的時候被這幾個又長又很相似的協(xié)議弄的暈頭轉(zhuǎn)向,所以希望能寫一篇淺顯易懂的入門文章。本文的內(nèi)容會比較基礎(chǔ),不會涉及的很深的,就像題目說的初窺。通過本文可以用最簡單的方式實現(xiàn)最簡單的自定義轉(zhuǎn)場動畫。如果你需要更加深入的知識,可以參考官方文檔。

Customizing the Transition Animations

本文中的 Demo 可以從 這里 下載


在講主要的內(nèi)容之前我們需要分清幾個概念:

  • presentingViewController
  • presentedViewController
  • fromViewController
  • toViewController

presentingVCpresentedVC的概念比較容易理解,前者就是 執(zhí)行present 動作的那個控制器,而后者就是 被present 的那個控制器,這兩個控制器的身份是始終不會改變的。

fromVCtoVC是個相對的概念,在執(zhí)行 present 的時候presentingVC就是fromVC,而presentedVC就是toVC。在 dismiss 的時候就要反一反了presentedVCfromVC,而presentingVCtoVC。官方有一張圖是這樣的:

普通 view controller 自定義轉(zhuǎn)場


我們先以普通的 view controller 為例子,講 present 和 dismiss 的轉(zhuǎn)場。往簡單了說,我們只需要知道 3 個協(xié)議就可以實現(xiàn)自定義轉(zhuǎn)場。

@protocol UIViewControllerContextTransitioning <NSObject>
@protocol UIViewControllerAnimatedTransitioning <NSObject>
@protocol UIViewControllerTransitioningDelegate <NSObject>

第一個協(xié)議UIViewControllerContextTransitioning 實現(xiàn)了這個協(xié)議的對象,我它為轉(zhuǎn)場上下文,一般來說,轉(zhuǎn)場上下文不用我們自己實現(xiàn),由系統(tǒng)提供給我們。通過它,我們可以獲取到很多轉(zhuǎn)場相關(guān)的信息。這里我只列舉幾個重要的屬性或方法,其他的看名字和注釋也能很容易知道它們的用途:

// 所有要執(zhí)行動畫的 view 都要加入到 containerView
- (UIView *)containerView;  
// 通過 key 來返回轉(zhuǎn)場前,轉(zhuǎn)場后的 view controller
// UITransitionContextFromViewControllerKey
// UITransitionContextToViewControllerKey
- (UIViewController *)viewControllerForKey:(NSString *)key;
// 可以得到參與轉(zhuǎn)場的 view controller 起始和結(jié)束時的 frame,一般來說通過 fromVC 的到起始的,通過 toVC 的到結(jié)束時的
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
// 在轉(zhuǎn)場動畫結(jié)束,或者取消時要通知系統(tǒng)是否完成
- (void)completeTransition:(BOOL)didComplete;

第二個協(xié)議UIViewControllerAnimatedTransitioning 負責的是轉(zhuǎn)場動畫的內(nèi)容,我把實現(xiàn)了這個協(xié)議的對象稱作動畫控制器。它有兩個必須實現(xiàn)的方法:- (NSTimeInterval)transitionDuration:- (void)animateTransition:。第一個方法用來返回動畫的時長,而第二個方法就是實現(xiàn)動畫的過程,它有一個參數(shù),這個參數(shù)就是一個轉(zhuǎn)場上下文,通過這個參數(shù)我們可以取到很多轉(zhuǎn)場的信息,然后進行動畫。

第三個協(xié)議UIViewControllerTransitioningDelegate 我們需設(shè)置presentedVCtransitioningDelegate屬性為一個實現(xiàn)了這個協(xié)議的對象。我們現(xiàn)在只要先關(guān)注這個協(xié)議的兩個方法:

// 返回 present 動畫控制器
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
// 返回 dismiss 動畫控制器
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

我們可以把 present 和 dismiss 動畫控制器寫成一個對象,在內(nèi)部通過一些邏輯判斷來執(zhí)行對應的動畫,當然也可以分成兩個對象。

在知道了上面這些內(nèi)容后,我們就可以一起來寫一個 demo 來學習自定義轉(zhuǎn)場動畫了。

Demo


先寫兩個 view controller,分別是 presentingVC 和 presentedVC,內(nèi)容基本上是一樣的,我就不貼兩份了。只有buttonClicked方法不一樣,一個是 present, 一個是 dismiss。

@implementation PresentingViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.bounds = CGRectMake(0, 0, 200, 30);
    button.center = self.view.center;
    [button setTitle:@"present view controller" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];

}
- (void)buttonClicked {
    PresentedViewController *presentedVC = [PresentedViewController new];
    [self presentViewController:presentedVC animated:YES completion:nil];
}
@end

// ----- in PresentedViewController.m -----
- (void)buttonClicked {
    [self dismissViewControllerAnimated:YES completion:nil];
}

完成這些后我們的程序就能實現(xiàn)轉(zhuǎn)場了,只不過是系統(tǒng)默認的轉(zhuǎn)場方式。接下來,我們要寫一個自己的動畫控制器,為了區(qū)分和系統(tǒng)的動畫,我們的動畫選擇從上往下劃出的轉(zhuǎn)場方式。我們新建一個類,讓它遵循UIViewControllerAnimatedTransitioning協(xié)議。

// AnimationController.h
@interface AnimationController : NSObject<UIViewControllerAnimatedTransitioning>
@end

// AnimationController.m
@implementation AnimationController
// 轉(zhuǎn)場的時間
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.8;
}
// 轉(zhuǎn)場動畫實現(xiàn)
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    // 通過 key 取到 fromVC 和 toVC
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 把 toVC 加入到 containerView
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    // 一些動畫要用的的數(shù)據(jù)
    CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    // 動畫過程
    if (toVC.isBeingPresented) {
        toVC.view.frame = CGRectOffset(finalFrame, 0, -finalFrame.size.height);
        [UIView animateWithDuration:duration
                         animations:^{
                             toVC.view.frame = finalFrame;
                         }
                         completion:^(BOOL finished) {
                             // 結(jié)束后要通知系統(tǒng)
                             [transitionContext completeTransition:YES];
                         }];
    }
    if (fromVC.isBeingDismissed) {
        [containerView sendSubviewToBack:toVC.view];
        [UIView animateWithDuration:duration
                         animations:^{
                             fromVC.view.frame = CGRectOffset(finalFrame, 0, -finalFrame.size.height);
                         }
                         completion:^(BOOL finished) {
                             // dismiss 動畫添加了手勢后可能出現(xiàn)轉(zhuǎn)場取消的狀態(tài),所以要根據(jù)狀態(tài)來判定是否完成轉(zhuǎn)場
                             BOOL isComplete = ![transitionContext transitionWasCancelled];
                             [transitionContext completeTransition:isComplete];
                         }];
    }
}
@end
  • 第一個方法,我們返回了動畫的時間 0.8 秒
  • 第二個方法
    • 先通過 key 取到 fromVC 和 toVC。
    • 然后把 toVC 的 view 加入到 containerView 中 ,fromVC 的 view 是本來就在 containerView 中的
    • 動畫的過程分為兩塊,分別是 present 動畫 和 dismiss 動畫,我們可以通過 UIViewController 自帶的 isBeingPresentedisBeingDismissed 屬性來判斷當前是那種類型的轉(zhuǎn)場。動畫的過程就可以自己發(fā)揮想象力了。在動畫結(jié)束后要通知系統(tǒng)完成轉(zhuǎn)場,這里要注意的是,因為稍后我們要給 dismiss 添加手勢驅(qū)動,所以轉(zhuǎn)場存在取消的可能,所以我們通過[transitionContext transitionWasCancelled]來得到轉(zhuǎn)場的狀態(tài),再判斷是否通知系統(tǒng)轉(zhuǎn)場完成。

最后一步,我們要通過UIViewControllerTransitioningDelegate動畫控制器視圖控制器聯(lián)系起來。我們要給PresentingViewController添加一些內(nèi)容。

先讓 presentingVC 遵循 UIViewControllerTransitioningDelegate 協(xié)議,并且添加一個屬性。

@interface PresentingViewController ()<UIViewControllerTransitioningDelegate>
// 動畫控制器
@property (nonatomic, strong)id<UIViewControllerAnimatedTransitioning> animationController;
@end

然后在- (void)viewDidLoad中初始化動畫控制器 _animationController = [AnimationController new];

- (void)viewDidLoad {
    [super viewDidLoad];
    ...
    // 初始化動畫控制器
    _animationController = [AnimationController new];
}

接著在 - (void)buttonClicked方法中設(shè)置presentedVC的代理

- (void)buttonClicked {
    PresentedViewController *presentedVC = [PresentedViewController new];
    // 設(shè)置 presented view controller 的轉(zhuǎn)場代理
    presentedVC.transitioningDelegate = self;
    [self presentViewController:presentedVC animated:YES completion:nil];
}

最后添加兩個代理方法

// 返回 present 動畫控制器
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return _animationController;
}

// 返回 dismiss 動畫控制器
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return _animationController;
}

至此,我們已經(jīng)完成了一個自定義的轉(zhuǎn)場動畫了??

注意點

presentedVC 的 modalPresentationStyle 默認為UIModalPresentationFullScreen,這種情況下在轉(zhuǎn)場完成后系統(tǒng)會自動隱藏 presentingVC 的 view 。如果我們設(shè)置了 UIModalPresentationCustom那么轉(zhuǎn)場完成后,presentingVC 的 view 不會隱藏。一般來說在動畫的時候我們都會把 toVC 的 view 加入到 containerView 中,在這種模式下執(zhí)行 dismiss 的時候我們不能把 toVC.view(presentingVC.view) 加入到 containerView 中,因為這個 view 并由系統(tǒng)額外管理,如果我們改變了它,那就有可能把從原來的視圖層次中移除而導致它消失不見。

交互式轉(zhuǎn)場


實現(xiàn)交互式的轉(zhuǎn)場需要在UIViewControllerTransitioningDelegate的協(xié)議方法中返回一個實現(xiàn)了UIViewControllerInteractiveTransitioning的對象。官方已經(jīng)給我們封裝好了一個UIPercentDrivenInteractiveTransition類,我們只要繼承這個類在加入我們自己的一些內(nèi)容就可以實現(xiàn)交互式轉(zhuǎn)場。有幾個方法我們需要先知道:

// 更新轉(zhuǎn)場狀態(tài)
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
// 取消轉(zhuǎn)場
- (void)cancelInteractiveTransition;
// 完成轉(zhuǎn)場
- (void)finishInteractiveTransition;

需要注意的是,在轉(zhuǎn)場發(fā)生時,如果返回了交互控制器,但是卻沒有通過交互的方式來執(zhí)行轉(zhuǎn)場,那么整個過程就卡住。所以我們需要給交互控制器添加一個屬性,用來監(jiān)聽當前是否是通過手勢驅(qū)動,如果不是我們就返回一個 nil,這樣就不會執(zhí)行交互式轉(zhuǎn)場,而只會執(zhí)行普通的動畫轉(zhuǎn)場。接下來我們來看一下代碼:

// InteractionController.h
@interface InteractionController : UIPercentDrivenInteractiveTransition
// 標記是否是交互轉(zhuǎn)場
@property (nonatomic, readonly, getter=isInteracting)BOOL interacting;
// 一些初始化工作
- (void)prepareForViewController:(UIViewController *)viewController;
@end

// InteractionController.m
@interface InteractionController ()
@property (nonatomic, weak)UIViewController *presentedVC;   // 注意是弱引用
@property (nonatomic, assign)BOOL shouldComplete;
@end

@implementation InteractionController
// 給 viewController 的 view 添加手勢
- (void)prepareForViewController:(UIViewController *)viewController {
    _presentedVC = viewController;
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
    [viewController.view addGestureRecognizer:panGesture];
}
- (void)panGestureAction:(UIPanGestureRecognizer *)gestureRecognizer {
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
    // 動畫的百分比
    CGFloat percent = 0.0;
    switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            // 設(shè)置交互狀態(tài)為 YES
            _interacting = YES;
            // 手勢開始時要調(diào)用 dismiss
            [_presentedVC dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged:
            // 計算百分比
            percent = -translation.y/_presentedVC.view.bounds.size.height;
            // 更新轉(zhuǎn)場的進度 傳入的參數(shù)值要在 0.0~1.0 之間
            [self updateInteractiveTransition:percent];
            // 如果滑動超過 30% 就視為轉(zhuǎn)場完成
            _shouldComplete = (percent > 0.3);
            break;
        case UIGestureRecognizerStateCancelled:
            _interacting = NO;
            [self cancelInteractiveTransition];
            break;
        case UIGestureRecognizerStateEnded:
            _interacting = NO;
            if (_shouldComplete) {
                [self finishInteractiveTransition];
            } else {
                [self cancelInteractiveTransition];
            }
            break;
        default:
            break;
    }
}
@end

注釋已經(jīng)寫得很詳細了,需要注意的是在完成或者取消的時候一定要調(diào)用對應的方法來通知系統(tǒng)。完成了這個類后我們需要再次修改 PresentingViewController的內(nèi)容。

再添加一個屬性

@property (nonatomic, strong)InteractionController *interactiveTransition;

并且在- (void)viewDidLoad中初始化它

- (void)viewDidLoad {
    [super viewDidLoad];
    ...
    // 初始化交互控制器
    _interactiveTransition = [InteractionController new];
}

- (void)buttonClicked中執(zhí)行- (void)prepareForViewController:(UIViewController *)viewController方法

- (void)buttonClicked {
    ...
    // 添加交互
    [_interactiveTransition prepareForViewController:presentedVC];
    [self presentViewController:presentedVC animated:YES completion:nil];
}

在最后添加協(xié)議方法,在方法中要通過 isInteracting 屬性來判斷是否是執(zhí)行交互式轉(zhuǎn)場,如果不是則返回 nil

// 返回 dismiss 的交互控制器
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
    return _interactiveTransition.isInteracting ? _interactiveTransition : nil;
}

關(guān)于普通的 view controller 的自定義轉(zhuǎn)場就到此結(jié)束了。如果你按照文章的步驟一步一步寫下來,相信你已經(jīng)完成了一個最簡單的自定義轉(zhuǎn)場。

容器 view controller 的自定義轉(zhuǎn)場


UINavigationControllerUITabBarController 都屬于 容器VC。與普通的 VC 不同的是,它們通過 UINavigationControllerDelegate或者UITabBarControllerDelegate的代理方法來返回動畫控制器,而動畫控制器的具體實現(xiàn),幾乎是一模一樣的。我放在 github 上的 Demo 中,有這三種自定轉(zhuǎn)場的代碼,用的基本上是同一個 animationController ,如果你掌握了前面的內(nèi)容,那么這里也就不是問題了。具體的代碼我就不展開了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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