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

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

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

Customizing the Transition Animations

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


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

  • presentingViewController
  • presentedViewController
  • fromViewController
  • toViewController

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

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

普通 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è)置presentedVCtransitioningDelegate屬性為一個實(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 自帶的 isBeingPresentedisBeingDismissed 屬性來判斷當(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)容,那么這里也就不是問題了股淡。具體的代碼我就不展開了身隐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唯灵,隨后出現(xiàn)的幾起案子贾铝,更是在濱河造成了極大的恐慌,老刑警劉巖埠帕,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垢揩,死亡現(xiàn)場離奇詭異,居然都是意外死亡敛瓷,警方通過查閱死者的電腦和手機(jī)叁巨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呐籽,“玉大人锋勺,你說我怎么就攤上這事蚀瘸。” “怎么了庶橱?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵贮勃,是天一觀的道長。 經(jīng)常有香客問我苏章,道長寂嘉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任布近,我火速辦了婚禮垫释,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撑瞧。我一直安慰自己棵譬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布预伺。 她就那樣靜靜地躺著订咸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪酬诀。 梳的紋絲不亂的頭發(fā)上脏嚷,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音瞒御,去河邊找鬼父叙。 笑死,一個胖子當(dāng)著我的面吹牛肴裙,可吹牛的內(nèi)容都是我干的趾唱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蜻懦,長吁一口氣:“原來是場噩夢啊……” “哼甜癞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宛乃,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悠咱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后征炼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體析既,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年谆奥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眼坏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡雄右,死狀恐怖空骚,靈堂內(nèi)的尸體忽然破棺而出纺讲,到底是詐尸還是另有隱情,我是刑警寧澤囤屹,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布熬甚,位于F島的核電站,受9級特大地震影響肋坚,放射性物質(zhì)發(fā)生泄漏乡括。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一智厌、第九天 我趴在偏房一處隱蔽的房頂上張望诲泌。 院中可真熱鬧,春花似錦铣鹏、人聲如沸敷扫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葵第。三九已至,卻和暖如春合溺,著一層夾襖步出監(jiān)牢的瞬間卒密,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工棠赛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哮奇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓睛约,卻偏偏與公主長得像鼎俘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子痰腮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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