動(dòng)畫與轉(zhuǎn)場,個(gè)人認(rèn)為在概念上并不復(fù)雜损痰,只是在代碼的組織和形式上比較復(fù)雜福侈,因此我嘗試先講講概念,再講講實(shí)現(xiàn)徐钠,讓思緒清晰一些癌刽。
什么是動(dòng)畫(Animation)?
所謂動(dòng)畫,就是在一段時(shí)間內(nèi)显拜,一些 view 的位置衡奥、顏色等屬性會(huì)逐漸變化的一個(gè)現(xiàn)象。那么要完成一個(gè)動(dòng)畫远荠,我們只需要確定三點(diǎn):動(dòng)畫有多久矮固、動(dòng)畫涉及到哪些 view 、這些 view 都有哪些屬性改變了譬淳,說簡單點(diǎn)兒就是時(shí)間档址、元素、變化形式邻梆。明確了這三點(diǎn)守伸,各種 API 的變化只是在代碼的簡潔性和復(fù)用度上不停的做文章而已。
那浦妄,什么是轉(zhuǎn)場(Transition)尼摹?
我們說到,動(dòng)畫的三個(gè)主要元素是時(shí)間剂娄、元素蠢涝、變化形式,在元素這里動(dòng)畫并沒有做過多的約束阅懦,而從概念上講和二,轉(zhuǎn)場就是一個(gè)動(dòng)畫的子集,其約束動(dòng)畫的元素必須為兩個(gè)元素耳胎,并且一般都是兩個(gè) view controller 的主 view 進(jìn)行的轉(zhuǎn)換(所以說轉(zhuǎn)場是針對(duì)兩個(gè) vc 的動(dòng)畫也沒啥大毛补呗馈)。
iOS 中動(dòng)畫怎么做场晶?
了解了動(dòng)畫的關(guān)鍵概念混埠,我們來看看在 iOS 中怠缸,應(yīng)該如何用代碼去描述這三個(gè)概念诗轻。
第一種:使用UIView 的 begin/commit :
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0f];// 這里描述時(shí)間
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 這里同時(shí)描述了元素和變化形式
[UIView commitAnimations];
第二種:直接通過 block 調(diào)用
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView animateWithDuration:1.0f delay:1.0f // 這里是時(shí)間
options:UIViewAnimationOptionCurveEaseIn // 這里是一些封裝的變化形式
animations:^{
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 這里同時(shí)描述了元素和變化形式
} completion:nil];
第三種:將對(duì)屬性的變化封裝到 CoreAnimation 對(duì)象中,然后應(yīng)用到某個(gè) view 的 layer 上
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;// 這里描述了時(shí)間
[_demoView.layer addAnimation:anima forKey:@"positionAnimation"];// 這里則是描述了元素
這三種方式中揭北,第一種是很久以前(iOS 4.0)使用的形式扳炬,無論是便捷度和復(fù)用度都不是很高。第二種是最方便的搔体,但是缺點(diǎn)在于不好復(fù)用(除非把 block 保存起來恨樟,可以在一個(gè) vc 中實(shí)現(xiàn)復(fù)用)。第三種是一種很容易復(fù)用的形式疚俱,將動(dòng)畫的三個(gè)元素中時(shí)間劝术、變化形式單獨(dú)抽離出來,使得其可以自由的應(yīng)用在任意的元素上。(由此可以看出养晋,如果想要代碼的復(fù)用度更高衬吆,就需要不斷的減少一段代碼或者一個(gè)對(duì)象在概念上的職責(zé))
iOS 中轉(zhuǎn)場怎么做?
前面我們說過绳泉,轉(zhuǎn)場是針對(duì)于兩個(gè)特定的 view 的動(dòng)畫逊抡,所以我們需要先約定一下術(shù)語,假如我們有兩個(gè) VC A/B零酪,我們要從 A 轉(zhuǎn)換到 B冒嫡,我們稱呼 A 為 presentingViewController(或者 fromViewController),稱呼 B 為 presentedViewController(或者 toViewController)四苇。當(dāng)從 B 結(jié)束轉(zhuǎn)換回到 A 時(shí)孝凌,我們?nèi)匀环Q呼 A 為 presentingViewController,B 為 presentedViewController月腋,但是我們會(huì)稱呼 A 為 toViewController 胎许,而 B 為 fromViewController。明白區(qū)別了么罗售?from/to 是針對(duì)一次動(dòng)畫的辜窑,而 presented/presenting 是針對(duì)一次完整的轉(zhuǎn)場的。
雖然從概念上來說寨躁,轉(zhuǎn)場是一種特定的動(dòng)畫穆碎,但是實(shí)際上轉(zhuǎn)場需要考慮的事情要比一般的動(dòng)畫要多(比如一般的動(dòng)畫可能不需要交互,但是轉(zhuǎn)場可能需要)职恳,因此在代碼的組織結(jié)構(gòu)上所禀,轉(zhuǎn)場使用了更多的對(duì)象去更加細(xì)致的拆分概念上的職責(zé)。
最基本的一種實(shí)現(xiàn)轉(zhuǎn)場的方式放钦,非常類似于上面所說的第二種動(dòng)畫的表現(xiàn)形式:
[self transitionFromViewController:self.fromVC
toViewController:self.toVC // 元素
duration:5 // 時(shí)間
options:UIViewAnimationOptionCurveEaseInOut // 變化形式的封裝
animations:^{
CGRect frame = self.thirdVC.view.frame;
frame.origin.y = 150;
self.thirdVC.view.frame = frame;
}
completion:nil];
這個(gè)轉(zhuǎn)場一般在容器 VC 中使用色徘。缺點(diǎn)其實(shí)是和最基本的動(dòng)畫調(diào)用方式一樣,都是不容易復(fù)用操禀,并且使用場景有限褂策,只能用在容器 vc 中,不能用在兩個(gè)平級(jí)的 vc 中颓屑。也就是說斤寂,為了從 A 轉(zhuǎn)到 B,我們必須首先有一個(gè) C ,然后讓 A揪惦、B 作為 C 的 child vc 遍搞,顯然很不方便啊,那么我們就需要考慮一種新的代碼組織形式器腋,將轉(zhuǎn)場的職責(zé)進(jìn)行拆分溪猿。
轉(zhuǎn)場的職責(zé)劃分
在一次自定義的轉(zhuǎn)場中钩杰,我們會(huì)將指責(zé)進(jìn)行如下形式的劃分:
首先,我們需要有兩個(gè) vc(廢話(╬▔皿▔))诊县,然后設(shè)置 presentingVC 的 modalPresentationStyle
為 UIModalPresentationCustom
榜苫,接下來將 presentingVC 的 transitioningDelegate
屬性指向一個(gè)實(shí)現(xiàn)了 UIViewControllerTransitioningDelegate
協(xié)議的對(duì)象上。這樣就告訴 UIKit 任意一個(gè) vc 用 prensentViewController:animated:completion
方法展示 presentingVC 時(shí)翎冲,presentingVC 的轉(zhuǎn)場效果完全由 transitioningDelegate
屬性所指向的對(duì)象來負(fù)責(zé)垂睬。
// PresentingVC
self.transitioningDelegate = [TransitionDelegate new];// 轉(zhuǎn)場效果這一部分職責(zé)從 vc 中剝離了出去
TransitionDelegate
是一個(gè)實(shí)現(xiàn)了 UIViewControllerTransitioningDelegate
協(xié)議的對(duì)象,在這個(gè)協(xié)議中又將轉(zhuǎn)場效果的職責(zé)分為三個(gè)對(duì)象去負(fù)責(zé):一個(gè)負(fù)責(zé)轉(zhuǎn)場動(dòng)畫效果的 Animator抗悍,一個(gè)負(fù)責(zé)轉(zhuǎn)場過程中交互的 InteractiveAnimator驹饺,和一個(gè)則負(fù)責(zé)轉(zhuǎn)場過程中 view 的層級(jí)關(guān)系以及在不同屏幕上的適配。這三個(gè)對(duì)象的職責(zé)缴渊,在代碼上的表現(xiàn)形式就是將UIViewControllerTransitioningDelegate
的內(nèi)容分為三組赏壹。我們來一個(gè)個(gè)了解一下。
TransitionAnimator
這個(gè)對(duì)象負(fù)責(zé)轉(zhuǎn)場的動(dòng)畫效果衔沼,具體點(diǎn)兒來說蝌借,他決定了可見的視圖從 PresentingViewController 的 view 到可見視圖變?yōu)?PresentedViewController 的 view 的過程中,兩個(gè) view 應(yīng)該如何去變化指蚁。在UIViewControllerTransitioningDelegate
協(xié)議中菩佑,該對(duì)象可以通過兩個(gè)方法返回:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
兩個(gè)方法中,前者決定了 present 過程中的動(dòng)畫效果凝化,后者則決定了 dismiss 過程中的動(dòng)畫效果稍坯。而具體 Animator 如何去控制轉(zhuǎn)場過程中的動(dòng)畫,我們就需要看看 UIViewControllerAnimatedTransitioning
這個(gè)協(xié)議中的方法都有些什么:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
第一個(gè)方法決定了轉(zhuǎn)場的時(shí)間搓劫,第二個(gè)方法則是通過一個(gè) transitionContext 對(duì)象傳遞給 Animator 對(duì)象轉(zhuǎn)場過程中的 FromVC/ToVC瞧哟,以及 containerView ,也就是轉(zhuǎn)場過程中的元素枪向,然后我們就可以通過 UIKit 的動(dòng)畫 API 決定轉(zhuǎn)場的變化形式了勤揩。在這個(gè)方法中我們要做的就是:
- 得到 ToVC 的 view,設(shè)定其初始狀態(tài)
- 將 ToVC 的 view 添加到 containerView 中
- 通過任意一種動(dòng)畫形式對(duì) ToVC 的 view 做動(dòng)畫秘蛔,然后在結(jié)束的時(shí)候調(diào)用
transitionContext
對(duì)象的completeTransition:
方法告知系統(tǒng)我們的動(dòng)畫做完了陨亡。
更具體的內(nèi)容,可以參見如下的一段代碼:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// 獲取所有需要的 view 以及 vc
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = transitionContext.containerView;
// 設(shè)定初始狀態(tài)
toVC.view.frame = CGRectMake(0, - CGRectGetHeight(fromVC.view.frame), CGRectGetWidth(fromVC.view.frame), CGRectGetHeight(fromVC.view.frame));
toVC.view.alpha = 0.0f;
// 一定要自己手動(dòng)添加 subview, fromVC 的 view UIKit 會(huì)自動(dòng)移除缠犀,但是 UIKit 不會(huì)自動(dòng)添加 toVC 的 view
[containerView addSubview:toVC.view];
// 獲取動(dòng)畫時(shí)間
NSTimeInterval duration = [self transitionDuration:transitionContext];
// 開始動(dòng)畫
[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
對(duì)于一般的轉(zhuǎn)場來說数苫,實(shí)現(xiàn)了基本的動(dòng)畫效果可能就夠了聪舒,但是實(shí)際開發(fā)中辨液,我們可能對(duì)于轉(zhuǎn)場有更加深入的需求,比如希望轉(zhuǎn)場能夠帶有用戶交互箱残,像系統(tǒng)的全局返回手勢(shì)那樣滔迈,這個(gè)時(shí)候止吁,我們就需要額外返回一個(gè) InteractiveAnimator
來告訴 UIKit 隨著用戶的手勢(shì)變化,動(dòng)畫應(yīng)該執(zhí)行到百分之多少或者是否需要取消燎悍,這些操作我們都可以通過 context 對(duì)象中的方法來完成:
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
因此敬惦,如果想實(shí)現(xiàn)一個(gè)交互式的轉(zhuǎn)場,我們需要做如下幾件事兒:
- 在 presentingVC 中添加一個(gè) button 點(diǎn)擊以外的『觸發(fā)器』(一般來說谈山,都是一個(gè) Gesture Recognizer)俄删,比如添加一個(gè)邊緣滑動(dòng)的 Gesture Recognizer,當(dāng)一個(gè)邊緣滑動(dòng)開始時(shí)奏路,我們?cè)趯?duì)應(yīng)的回調(diào)中 present PresentedVC畴椰。
- 在 presentedVC 的 transitionDelegate 中,返回一個(gè) InteractiveAnimator鸽粉。
- 在 Animator 中的
startInteractiveTransition:
方法中將 context 對(duì)象保存起來斜脂。 - 想辦法將 Gesture Recognizer 傳遞給 InteractiveAnimator,使得在 Animator 中可以獲取當(dāng)前手勢(shì)的信息触机,結(jié)合 context 對(duì)象中的 containerView 等信息帚戳,我們可以知道當(dāng)前手勢(shì)在 view 中更具體的信息。
- 根據(jù)預(yù)先設(shè)定好的規(guī)則儡首,在 Gesture Recognizer 的回調(diào)中調(diào)用 context 對(duì)象的 cancel/finished/update 方法
比如片任,如果我們想實(shí)現(xiàn)一個(gè)邊緣滑動(dòng)的交互動(dòng)畫效果,我們可以這么來寫代碼:
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 把 context 對(duì)象保存起來
self.transitionContext = transitionContext;
[super startInteractiveTransition:transitionContext];
}
// 根據(jù)手勢(shì)的偏移來計(jì)算當(dāng)前動(dòng)畫應(yīng)該有的完成度
- (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture
{
// 根據(jù) container view 以及 gesture recognizer 計(jì)算偏移量
UIView *transitionContainerView = self.transitionContext.containerView;
CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
// 根據(jù)偏移量得出百分比
CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
return (width - locationInSourceView.x) / width;
}
// gesture recognizer 的回調(diào)
- (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer
{
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
break;
case UIGestureRecognizerStateChanged:
// 計(jì)算百分比蔬胯,并返回
[self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
// 根據(jù)預(yù)先設(shè)定的閾值決定是結(jié)束還是取消蚂踊,這里我們?cè)O(shè)定 view 中間是分界線
if ([self percentForGesture:gestureRecognizer] >= 0.5f)
[self finishInteractiveTransition];
else
[self cancelInteractiveTransition];
break;
default:
// 其他情況,取消轉(zhuǎn)場
[self cancelInteractiveTransition];
break;
}
}
PresentationController
以上的兩組接口笔宿,分別讓我們自定義了轉(zhuǎn)場過程中的動(dòng)畫犁钟、動(dòng)畫執(zhí)行百分比,但是不管是哪個(gè)泼橘,都會(huì)在最后將 fromVC 的 view 從 containerView 上移除涝动,并且整個(gè)轉(zhuǎn)場過程中如果我們想添加一些額外的 view 也是無法做到的。如果想要實(shí)現(xiàn)這些功能炬灭,就需要我們創(chuàng)建一個(gè) UIPresentationController
的子類醋粟,然后重載其 四個(gè)轉(zhuǎn)場的生命周期方法:
- presentationTransitionWillBegin
- presentationTransitionDidEnd:
- dismissalTransitionWillBegin
- dismissalTransitionDidEnd:
在重載這些方法時(shí),我們也可以使用其 presentingViewController 屬性的 transitionCoordinator 來同步的為我們新添加的 view 執(zhí)行動(dòng)畫(所謂同步就是和我們之前在 Animator 中寫的動(dòng)畫同時(shí)執(zhí)行)重归。
比如米愿,我們可以為我們添加的一個(gè) dimming view 的透明度設(shè)置一個(gè)動(dòng)畫:
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
self.dimmingView.alpha = 0.f;
[transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.dimmingView.alpha = 0.5f;
} completion:NULL];
總結(jié)一下來說,如果我們想要使用 UIPresentationController 鼻吮,我們需要:
- 設(shè)置 presentedVC 的 presentStyle 為
UIModalPresentationCustom
- 在 presentedVC 的 transitionDelegate 中返回我們創(chuàng)建的
UIPresentationController
的子類 - 在子類中重載轉(zhuǎn)場生命周期的四個(gè)方法育苟,添加我們所需要的自定義的view