從 iOS 的動(dòng)畫說到轉(zhuǎn)場

動(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 的 modalPresentationStyleUIModalPresentationCustom榜苫,接下來將 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è)方法中我們要做的就是:

  1. 得到 ToVC 的 view,設(shè)定其初始狀態(tài)
  2. 將 ToVC 的 view 添加到 containerView 中
  3. 通過任意一種動(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)場,我們需要做如下幾件事兒:

  1. 在 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畴椰。
  2. 在 presentedVC 的 transitionDelegate 中,返回一個(gè) InteractiveAnimator鸽粉。
  3. 在 Animator 中的 startInteractiveTransition: 方法中將 context 對(duì)象保存起來斜脂。
  4. 想辦法將 Gesture Recognizer 傳遞給 InteractiveAnimator,使得在 Animator 中可以獲取當(dāng)前手勢(shì)的信息触机,結(jié)合 context 對(duì)象中的 containerView 等信息帚戳,我們可以知道當(dāng)前手勢(shì)在 view 中更具體的信息。
  5. 根據(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 鼻吮,我們需要:

  1. 設(shè)置 presentedVC 的 presentStyle 為 UIModalPresentationCustom
  2. 在 presentedVC 的 transitionDelegate 中返回我們創(chuàng)建的 UIPresentationController 的子類
  3. 在子類中重載轉(zhuǎn)場生命周期的四個(gè)方法育苟,添加我們所需要的自定義的view

擴(kuò)展閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市椎木,隨后出現(xiàn)的幾起案子违柏,更是在濱河造成了極大的恐慌博烂,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漱竖,死亡現(xiàn)場離奇詭異禽篱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)馍惹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門躺率,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人万矾,你說我怎么就攤上這事肥照。” “怎么了勤众?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵舆绎,是天一觀的道長。 經(jīng)常有香客問我们颜,道長吕朵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任窥突,我火速辦了婚禮努溃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阻问。我一直安慰自己梧税,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布称近。 她就那樣靜靜地躺著第队,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刨秆。 梳的紋絲不亂的頭發(fā)上凳谦,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音衡未,去河邊找鬼尸执。 笑死,一個(gè)胖子當(dāng)著我的面吹牛缓醋,可吹牛的內(nèi)容都是我干的如失。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼送粱,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼褪贵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起葫督,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤竭鞍,失蹤者是張志新(化名)和其女友劉穎板惑,沒想到半個(gè)月后橄镜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偎快,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年洽胶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晒夹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姊氓,死狀恐怖丐怯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翔横,我是刑警寧澤读跷,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站禾唁,受9級(jí)特大地震影響效览,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荡短,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一丐枉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掘托,春花似錦瘦锹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泪掀,卻和暖如春抽兆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背族淮。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工辫红, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人祝辣。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓贴妻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蝙斜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子名惩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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