簡書上的所有內(nèi)容都可以在我的個人博客上找到
這兩天學(xué)習(xí)了一下自定義轉(zhuǎn)場動畫的內(nèi)容,剛開始看的時(shí)候被這幾個又長又很相似的協(xié)議弄的暈頭轉(zhuǎn)向,所以希望能寫一篇淺顯易懂的入門文章。本文的內(nèi)容會比較基礎(chǔ),不會涉及的很深的,就像題目說的初窺
烧给。通過本文可以用最簡單的方式實(shí)現(xiàn)最簡單的自定義轉(zhuǎn)場動畫。如果你需要更加深入的知識喝噪,可以參考官方文檔础嫡。
本文中的 Demo 可以從 這里 下載
在講主要的內(nèi)容之前我們需要分清幾個概念:
- presentingViewController
- presentedViewController
- fromViewController
- toViewController
presentingVC
和presentedVC
的概念比較容易理解,前者就是 執(zhí)行present 動作的那個控制器酝惧,而后者就是 被present 的那個控制器榴鼎,這兩個控制器的身份是始終不會改變的。
fromVC
和toVC
是個相對的概念晚唇,在執(zhí)行 present 的時(shí)候presentingVC
就是fromVC
巫财,而presentedVC
就是toVC
。在 dismiss 的時(shí)候就要反一反了presentedVC
是fromVC
,而presentingVC
是toVC
哩陕。官方有一張圖是這樣的:
普通 view controller 自定義轉(zhuǎn)場
我們先以普通的 view controller 為例子平项,講 present 和 dismiss 的轉(zhuǎn)場赫舒。往簡單了說,我們只需要知道 3 個協(xié)議就可以實(shí)現(xiàn)自定義轉(zhuǎn)場闽瓢。
@protocol UIViewControllerContextTransitioning <NSObject>
@protocol UIViewControllerAnimatedTransitioning <NSObject>
@protocol UIViewControllerTransitioningDelegate <NSObject>
第一個協(xié)議UIViewControllerContextTransitioning
實(shí)現(xiàn)了這個協(xié)議的對象接癌,我它為轉(zhuǎn)場上下文
,一般來說扣讼,轉(zhuǎn)場上下文不用我們自己實(shí)現(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é)束時(shí)的 frame有咨,一般來說通過 fromVC 的到起始的,通過 toVC 的到結(jié)束時(shí)的
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
// 在轉(zhuǎn)場動畫結(jié)束曙搬,或者取消時(shí)要通知系統(tǒng)是否完成
- (void)completeTransition:(BOOL)didComplete;
第二個協(xié)議UIViewControllerAnimatedTransitioning
負(fù)責(zé)的是轉(zhuǎn)場動畫的內(nèi)容,我把實(shí)現(xiàn)了這個協(xié)議的對象稱作動畫控制器
鸽嫂。它有兩個必須實(shí)現(xiàn)的方法:- (NSTimeInterval)transitionDuration:
和- (void)animateTransition:
纵装。第一個方法用來返回動畫的時(shí)長,而第二個方法就是實(shí)現(xiàn)動畫的過程据某,它有一個參數(shù)橡娄,這個參數(shù)就是一個轉(zhuǎn)場上下文
,通過這個參數(shù)我們可以取到很多轉(zhuǎn)場的信息癣籽,然后進(jìn)行動畫挽唉。
第三個協(xié)議UIViewControllerTransitioningDelegate
我們需設(shè)置presentedVC
的transitioningDelegate
屬性為一個實(shí)現(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í)行對應(yīng)的動畫瓶籽,當(dāng)然也可以分成兩個對象。
在知道了上面這些內(nèi)容后埂材,我們就可以一起來寫一個 demo 來學(xué)習(xí)自定義轉(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];
}
完成這些后我們的程序就能實(shí)現(xiàn)轉(zhuǎn)場了莹痢,只不過是系統(tǒng)默認(rèn)的轉(zhuǎn)場方式种蘸。接下來墓赴,我們要寫一個自己的動畫控制器,為了區(qū)分和系統(tǒng)的動畫劈彪,我們的動畫選擇從上往下
劃出的轉(zhuǎn)場方式竣蹦。我們新建一個類,讓它遵循UIViewControllerAnimatedTransitioning
協(xié)議沧奴。
// AnimationController.h
@interface AnimationController : NSObject<UIViewControllerAnimatedTransitioning>
@end
// AnimationController.m
@implementation AnimationController
// 轉(zhuǎn)場的時(shí)間
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.8;
}
// 轉(zhuǎn)場動畫實(shí)現(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
- 第一個方法,我們返回了動畫的時(shí)間 0.8 秒
- 第二個方法
- 先通過 key 取到 fromVC 和 toVC滔吠。
- 然后把 toVC 的 view 加入到 containerView 中 纲菌,fromVC 的 view 是本來就在 containerView 中的
- 動畫的過程分為兩塊,分別是 present 動畫 和 dismiss 動畫疮绷,我們可以通過 UIViewController 自帶的
isBeingPresented
和isBeingDismissed
屬性來判斷當(dāng)前是那種類型的轉(zhuǎn)場翰舌。動畫的過程就可以自己發(fā)揮想象力了。在動畫結(jié)束后要通知系統(tǒng)完成轉(zhuǎn)場冬骚,這里要注意的是椅贱,因?yàn)樯院笪覀円o 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)場動畫了??
注意點(diǎn)
presentedVC 的 modalPresentationStyle 默認(rèn)為UIModalPresentationFullScreen
,這種情況下在轉(zhuǎn)場完成后系統(tǒng)會自動隱藏 presentingVC 的 view 饮醇。如果我們設(shè)置了 UIModalPresentationCustom
那么轉(zhuǎn)場完成后,presentingVC 的 view 不會隱藏秕豫。一般來說在動畫的時(shí)候我們都會把 toVC 的 view 加入到 containerView 中驳阎,在這種模式下執(zhí)行 dismiss 的時(shí)候我們不能把 toVC.view(presentingVC.view) 加入到 containerView 中,因?yàn)檫@個 view 并由系統(tǒng)額外管理馁蒂,如果我們改變了它呵晚,那就有可能把從原來的視圖層次中移除而導(dǎo)致它消失不見。
交互式轉(zhuǎn)場
實(shí)現(xiàn)交互式的轉(zhuǎn)場需要在UIViewControllerTransitioningDelegate
的協(xié)議方法中返回一個實(shí)現(xiàn)了UIViewControllerInteractiveTransitioning
的對象沫屡。官方已經(jīng)給我們封裝好了一個UIPercentDrivenInteractiveTransition
類饵隙,我們只要繼承這個類在加入我們自己的一些內(nèi)容就可以實(shí)現(xiàn)交互式轉(zhuǎn)場。有幾個方法我們需要先知道:
// 更新轉(zhuǎn)場狀態(tài)
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
// 取消轉(zhuǎn)場
- (void)cancelInteractiveTransition;
// 完成轉(zhuǎn)場
- (void)finishInteractiveTransition;
需要注意的是沮脖,在轉(zhuǎn)場發(fā)生時(shí)金矛,如果返回了交互控制器
芯急,但是卻沒有通過交互的方式來執(zhí)行轉(zhuǎn)場,那么整個過程就卡住驶俊。所以我們需要給交互控制器添加一個屬性娶耍,用來監(jiān)聽當(dāng)前是否是通過手勢驅(qū)動,如果不是我們就返回一個 nil饼酿,這樣就不會執(zhí)行交互式轉(zhuǎn)場榕酒,而只會執(zhí)行普通的動畫轉(zhuǎn)場。接下來我們來看一下代碼:
// InteractionController.h
@interface InteractionController : UIPercentDrivenInteractiveTransition
// 標(biāo)記是否是交互轉(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;
// 手勢開始時(shí)要調(diào)用 dismiss
[_presentedVC dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged:
// 計(jì)算百分比
percent = -translation.y/_presentedVC.view.bounds.size.height;
// 更新轉(zhuǎn)場的進(jì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)寫得很詳細(xì)了故俐,需要注意的是在完成或者取消的時(shí)候一定要調(diào)用對應(yīng)的方法來通知系統(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
的代理方法來返回動畫控制器
碌廓,而動畫控制器的具體實(shí)現(xiàn),幾乎是一模一樣的蝙云。我放在 github 上的 Demo 中氓皱,有這三種自定轉(zhuǎn)場的代碼路召,用的基本上是同一個 animationController 勃刨,如果你掌握了前面的內(nèi)容,那么這里也就不是問題了股淡。具體的代碼我就不展開了身隐。