簡書上的所有內(nèi)容都可以在我的個人博客上找到
這兩天學習了一下自定義轉(zhuǎn)場動畫的內(nèi)容,剛開始看的時候被這幾個又長又很相似的協(xié)議弄的暈頭轉(zhuǎn)向,所以希望能寫一篇淺顯易懂的入門文章。本文的內(nèi)容會比較基礎(chǔ),不會涉及的很深的,就像題目說的初窺
。通過本文可以用最簡單的方式實現(xiàn)最簡單的自定義轉(zhuǎn)場動畫。如果你需要更加深入的知識,可以參考官方文檔。
本文中的 Demo 可以從 這里 下載
在講主要的內(nèi)容之前我們需要分清幾個概念:
- presentingViewController
- presentedViewController
- fromViewController
- toViewController
presentingVC
和presentedVC
的概念比較容易理解,前者就是 執(zhí)行present 動作的那個控制器,而后者就是 被present 的那個控制器,這兩個控制器的身份是始終不會改變的。
fromVC
和toVC
是個相對的概念,在執(zhí)行 present 的時候presentingVC
就是fromVC
,而presentedVC
就是toVC
。在 dismiss 的時候就要反一反了presentedVC
是fromVC
,而presentingVC
是toVC
。官方有一張圖是這樣的:
普通 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è)置presentedVC
的transitioningDelegate
屬性為一個實現(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 自帶的
isBeingPresented
和isBeingDismissed
屬性來判斷當前是那種類型的轉(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)場
UINavigationController
,UITabBarController
都屬于 容器VC。與普通的 VC 不同的是,它們通過 UINavigationControllerDelegate
或者UITabBarControllerDelegate
的代理方法來返回動畫控制器
,而動畫控制器的具體實現(xiàn),幾乎是一模一樣的。我放在 github 上的 Demo 中,有這三種自定轉(zhuǎn)場的代碼,用的基本上是同一個 animationController ,如果你掌握了前面的內(nèi)容,那么這里也就不是問題了。具體的代碼我就不展開了。