寫在前面
本文中提到的 presented 和 presenting ,分別指被展示者和展示者,譬如 [viewControllerA presentViewController:viewControllerB animated:NO] 中,viewControllerA 是 presenting,viewControllerB 是 presented。
從熟悉的地方開始
蘋果提供的幾種轉(zhuǎn)場動(dòng)畫,可以用 UIModalPresentationStyle 和 UIModalTransitionStyle 來設(shè)置,從 presentation 和 transition 兩個(gè)關(guān)鍵詞的語意來理解轉(zhuǎn)場動(dòng)畫,presentation 重靜態(tài)的展示,可通過 UIModalPresentationStyle 來定義 presented view 的展示形式(全屏、formsheet、popover 等);transition 重動(dòng)態(tài)的轉(zhuǎn)場,可通過 UIModalTransitionStyle 來定義從 presenting view 上呈現(xiàn) presented view 的動(dòng)畫。
| UIModalPresentationStyle | Discussion |
|---|---|
| UIModalPresentationFullScreen | 全屏展示presented view,presenting view 在轉(zhuǎn)場動(dòng)畫完成后被移除 |
| UIModalPresentationPageSheet | horizontally compact 場景下同 UIModalPresentationFullScreen;horizontally regular 場景下 presented view 的 width 和 height 等于 presenting view 在 portrait 模式下的 width 與 height;未遮擋部分虛化禁止用戶交互 |
| UIModalPresentationFormSheet | horizontally compact 場景下同 UIModalPresentationFullScreen; horizontally regular 場景下,presented view 的長寬小于 screen 、居中顯示,landscape mode 下presented view 會(huì)隨著鍵盤的彈出而上移;未遮擋部分虛華禁止用戶交互 |
| UIModalPresentationCurrentContext | 轉(zhuǎn)場動(dòng)畫開始前,UIKit 開始從 presenting view controller 向上尋找,presented view controller 的內(nèi)容將覆蓋第一個(gè)找到的 definesPresentationContext = YES 的 view controller 的內(nèi)容;轉(zhuǎn)場動(dòng)畫結(jié)束時(shí),被覆蓋的內(nèi)容將被移除 |
| UIModalPresentationCustom | 將轉(zhuǎn)場動(dòng)畫交由 view controller 的 transitioningDelegate 對象來管理 |
| UIModalPresentationOverFullScreen | presented view 覆蓋的 content 不會(huì)從 view hierarchy 中移除,未被遮擋的部分對用戶可見 |
| UIModalPresentationOverCurrentContext | CurrentContext 和 OverFullScreen 的結(jié)合 |
| UIModalPresentationPopover | horizontally regular 場景下, popover 展示;horizontally compact 場景下,同 FullScreen |
| UIModalTransitionStyle | Discussion |
|---|---|
| UIModalTransitionStyleCoverVertical | 默認(rèn)值,從屏幕底部上推 |
| UIModalTransitionStyleFlipHorizontal | 旋轉(zhuǎn)門 自己體會(huì) |
| UIModalTransitionStyleCrossDissolve | 漸入漸出 |
| UIModalTransitionStylePartialCurl | 浮夸的翻頁 |
(transition style 和 presentation style 由 presented view controller 來設(shè)置,這也符合 presented one 不應(yīng)該依賴 presenting one 的思想。)
如果上述預(yù)定義轉(zhuǎn)場動(dòng)畫不能滿足你,那么需要實(shí)現(xiàn)自定義動(dòng)畫;自定義動(dòng)畫主要通過實(shí)現(xiàn)或集成了 UIViewControllerAnimatedTransitioning 協(xié)議和 UIPresentationController 類的動(dòng)畫控制器 animator 來實(shí)現(xiàn),稍作了解后,你會(huì)發(fā)現(xiàn) UIViewControllerAnimatedTransitioning 和 UIModalTransitionStyle 相似, UIPresentationController 和 UIModalPresentationStyle 相似。
預(yù)定義轉(zhuǎn)場動(dòng)畫通過給 UIViewController 的 modalPresentationStyle 和 modalTransitionStyle 屬性賦值來實(shí)現(xiàn),而自定義轉(zhuǎn)場動(dòng)畫則須是令 modalPresentationStyle = UIModalPresentationStyleCustom ,并且給 transitioningDelegate 賦值一個(gè)繼承了 UIViewControllerTransitioningDelegate 的對象 (初次使用時(shí)容易把 UIViewControllerAnimatedTransitioning 和 UIViewControllerTransitioningDelegate 弄混,其實(shí)二者是截然不同的概念),繼而在此對象的協(xié)議方法中分配動(dòng)畫控制器。通常我們使用當(dāng)前的 view controller 來實(shí)現(xiàn) UIViewControllerTransitioningDelegate 協(xié)議,具體代碼如下:
@interface ViewController () <UIViewControllerTransitioningDelegate>
@end
@implementation ViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) != nil) {
[self setModalPresentationStyle:UIModalPresentationCustom];
[self setTransitioningDelegate:self];
}
return self;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return nil; // return a transition animator
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return nil; // return a transition animator
}
- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source
{
return nil; // return a presentation animator
}
上述代碼中,animationControllerForPresentedController 要求 programer 返回一個(gè) present 當(dāng)前 ViewController 的動(dòng)畫控制器,animationControllerForDismissController 要求 coder 返回一個(gè) dismiss 當(dāng)前 view controller 的動(dòng)畫控制器,如何編寫動(dòng)畫控制器就是我們要發(fā)揮想象力的地方了。
好在蘋果給了一個(gè)很好用的協(xié)議,就是上文中提到的 UIViewControllerAnimatedTransitioning ,它有以下方法:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
- calls when presenting or dismissing a view controller
- configure your custom transition in it
- use view-based animation or core animation as you like
- all animations must take place in view specified by the containerView property of transitionContext
animateTransition: 前三段解釋都好理解,第四條提出了一個(gè) containerView 的屬性, containerView 是轉(zhuǎn)場動(dòng)畫中涉及的所有 view 的 superview,且 containerView 默認(rèn)添加了 presenting view controller 的 view ,你要做的事情是將 presented view controller 的 view 也添加到 containerView 上。
(不得不說 containerView 是 UIKit 作的很大的一個(gè)改變,在 iOS5 之前,view controller 被蘋果封裝得“緊密嚴(yán)合”,雖然知道不同 view controller 之前的轉(zhuǎn)場不過是 view 的疊加,但我們不能真的用[presentingViewController.view addSubview:presentedViewController.view]這樣的代碼來替代 presentViewController:方法,而在 animateTransition: 方法中,你可以真切地感受到 view 的 present 與 dismiss 是通過 addSubview: 和 removeFromSuperView 來完成的,不同之處是 containerView 必須是所有 view 的 "container")
- (void)animationEnded:(BOOL)transitionCompleted;
- transitionCompleted = YES if the transition completed successfully and the new view controller is displayed
- transitionCompleted = NO if the transition is canceled and the original view is still visible
- use this method to perform any final cleanup operations required by your transition animator
animationEnded: 類似 completion handler 和 failure handler
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
- return the duration in seconds of your custom transition
- the value you provide should be the same value that you use when configuring the animations in your
animateTransition:method. - UIKit uses the value to synchronize the actions of other objects that might be involved in the transition
了解了必要的知識以后,開始做真正有意思的事情吧~
(如果你想了解實(shí)現(xiàn)思路,可以瀏覽后文中的 GIF 圖和偽代碼;如果想要看到具體的實(shí)現(xiàn),可以使用【真·代碼閱讀術(shù)】前往 JYCustomTransition 哦)
仿iPhone相冊-點(diǎn)擊圖片放大的轉(zhuǎn)場動(dòng)畫

深情凝視 iOS 系統(tǒng)自帶相冊,會(huì)發(fā)現(xiàn)在照片流里 "點(diǎn)擊一張照片放大查看" 的過程,看似是 UIImageView 的 frame 發(fā)生了變化,實(shí)際上是 UICollectionViewController 切換到了 UIPageViewController 。當(dāng)然這只是沒找到源代碼之后基于觀察的猜測,兩個(gè)不同 view controller 切換更符合 Cocoa Frameworks 中一貫的各司其職的原則。
那么,仿制一個(gè)蘋果系統(tǒng)相冊,coder 須要做的是將 presentViewController 的具體過程委托給自定義的第三方(也就是上文提到的動(dòng)畫控制器)來實(shí)現(xiàn),以達(dá)到欺騙用戶眼球的效果。重點(diǎn)在于我們看到的 “整齊排列的照片中一張照片被放大” 的效果,筆者的做法是創(chuàng)建一個(gè)獨(dú)立于 viewController 的 animateImageView ,設(shè)置 animateImageView 的隱式動(dòng)畫來欺騙眼球,動(dòng)畫完成后即 remove,偽代碼如下:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
UIImage *image = getImageFromPhotoGraphy();
UIImageView *animateImageView = [[UIImageView alloc] initWithImage:image];
[animateImageView setFrame:getInitialFrameFromPhotoGraphy()];
[containerView addSubview:animateImageView ];
[UIView animateWithDuration:0.5 animations:^{
[animateImageView setFrame:getFinalFrameFromPhotoGraphy()];
} completion: ^(BOOL finished) {
[animateImageView removeFromSuperView]
}]
}
在具體實(shí)現(xiàn)的過程中,筆者還考慮了不同圖片尺寸的修正來達(dá)到視覺上的流暢效果,感興趣的小伙伴可以前往 JYCustomTransition 查看~
為了欺騙而欺騙-開門/關(guān)門的轉(zhuǎn)場動(dòng)畫
首先直接上GIF圖吧

依然是 “看起來” 是一張圖片被切割、被旋轉(zhuǎn),實(shí)際上是
view controller 之間的轉(zhuǎn)場。(筆者在敲代碼的時(shí)候想了想,非游戲類的 App 應(yīng)該不會(huì)特意做這么花哨并且毫無意義的轉(zhuǎn)場動(dòng)畫,不過最后還是決定做出來,一個(gè)是為了向小伙伴們展示 coder 們?yōu)榱似垓_用戶眼球可以做到什么地步,二是為了介紹轉(zhuǎn)場過程中用到的干貨 - CATransform 3D,有很多漂亮的轉(zhuǎn)場效果都有小小的用到 3D Animation 哦)
具體的實(shí)現(xiàn)過程是用 UIKit 提供的
UIGraphicsGetImageFromCurrentImageContext() 給 presenting view controller(雪山背景那個(gè))截屏生成 snapshotImage,通過對snapshotImage切割生成的 UIImageView 設(shè)置 transform 屬性來實(shí)現(xiàn)旋轉(zhuǎn)開門/關(guān)門效果,偽代碼如下:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIImage *snapshotImage = getSnapshotImageFromPresentingViewController();
UIImageView *leftImageView = cropImageViewFromLeftSnapshotImage();
[[leftImageView layer] setAnchorPoint:CGPointMake(0.0, 0.5)];
[leftImageView setFrame:leftRect];
[containerView insertSubview:leftImageView aboveSubview:toView];
UIImageView *rightImageView = cropImageViewFromRightSnapshotImage();
[[rightImageView layer] setAnchorPoint:CGPointMake(1.0, 0.5)];
[rightImageView setFrame:rightRect];
[containerView insertSubview:rightImageView aboveSubview:toView];
CATransform3D leftRotateTransform = CATransform3DIdentity;
leftRotateTransform.m34 = 4.5 / -2000;
leftRotateTransform = CATransform3DRotate(leftRotateTransform, 90.0 * M_PI / 180.0f, 0, 1.0, 0);
CATransform3D rightRotateTransform = CATransform3DIdentity;
rightRotateTransform.m34 = 4.5 / -2000;
rightRotateTransform = CATransform3DRotate(rightRotateTransform, -90.0 * M_PI / 180.0f, 0, 1.0, 0);
[UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[[leftImageView layer] setTransform:leftRotateTransform];
[[rightImageView layer] setTransform:rightRotateTransform];
} completion:^(BOOL finished) {
[leftImageView removeFromSuperview];
[rightImageView removeFromSuperview];
}];
}
筆者在具體實(shí)現(xiàn)過程遇到了多個(gè) layer 相互遮擋的問題,后來發(fā)現(xiàn)是由于 [leftImageView layer] 和 [rightImageView layer] 的 z 坐標(biāo)小于 presented view controller 的 layer 的 z 坐標(biāo)的緣故,舉一反三的小伙伴們可以停下來想想怎么解決這個(gè)問題,也可以空降到 JYCustomTransition 去看看具體的解決方法哦
終于用到了UIPresentationController-彈出便箋的轉(zhuǎn)場動(dòng)畫

最后這個(gè)看起來最樸素的動(dòng)畫包含了
UIViewControllerAnimatedTransitioning 、UIPresentationController 兩大主力, UIViewControllerAnimatedTransitioning 實(shí)現(xiàn)轉(zhuǎn)場中的 frame 變化,偽代碼如下:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *containerView = [transitionContext containerView];
const CGRect endFrame = [transitionContext finalFrameForViewController:toViewController];
const CGRect startFrame = CGRectOffset(endFrame, 0.0, CGRectGetHeight(endFrame));
[toView setFrame:startFrame];
[toView setAlpha:0.0f];
[containerView addSubview:toView];
[UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{
[toView setFrame:endFrame];
[toView setAlpha:1.0];
} completion:^(BOOL finished) {
if ([transitionContext transitionWasCancelled]) {
[toView removeFromSuperview];
}
[transitionContext completeTransition:YES];
}];
}
而展示的效果則是在 UIPresentationController 的子類實(shí)現(xiàn),設(shè)置了 presented view controller 中的 view 的 frame(portrait 模式下固定高度 500,landscape 模式下占據(jù)全屏),并為 view 加了一個(gè)半透明的背景 backgroundView ,這樣在 presented view controller 的初始化方法中就可以放心大膽的使用 [[self view] addSubview:subview] 而不用擔(dān)心 subview 相對上邊界的偏移,偽代碼如下:
@implementation _JYMemoViewController_PresentationController
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController
{
if ((self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController]) != nil) {
_backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
[_backgroundView setUserInteractionEnabled:NO];
[_backgroundView setTranslatesAutoresizingMaskIntoConstraints:NO];
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTapGestureRecognizer:)];
[_tapGestureRecognizer setDelegate:self];
}
return self;
}
- (CGRect)frameOfPresentedViewInContainerView
{
CGFloat height = fmin(CGRectGetHeight([[self containerView] frame]), 500.0);
return CGRectMake(0.0, CGRectGetMaxY([[self containerView] frame]) - height, CGRectGetWidth([[self containerView] frame]), height);
}
- (void)presentationTransitionWillBegin
{
[super presentationTransitionWillBegin];
[_backgroundView setBackgroundColor:[UIColor colorWithWhite:0.0 alpha:0.5]];
[_backgroundView setAlpha:0.0];
[[self containerView] addSubview:_backgroundView];
[[[_backgroundView topAnchor] constraintEqualToAnchor:[[self containerView] topAnchor]] setActive:YES];
[[[_backgroundView bottomAnchor] constraintEqualToAnchor:[[self containerView] bottomAnchor]] setActive:YES];
[[[_backgroundView leadingAnchor] constraintEqualToAnchor:[[self containerView] leadingAnchor]] setActive:YES];
[[[_backgroundView trailingAnchor] constraintEqualToAnchor:[[self containerView] trailingAnchor]] setActive:YES];
[[[self presentingViewController] transitionCoordinator] animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
[_backgroundView setAlpha:1.0];
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled]) {
[[[self presentedView] layer] setShadowOpacity:0.0f];
[_backgroundView setAlpha:0.0];
}
}];
}
- (void)presentationTransitionDidEnd:(BOOL)completed
{
if (completed) {
[[self containerView] addGestureRecognizer:_tapGestureRecognizer];
}
[super presentationTransitionDidEnd:completed];
}
- (void)dismissalTransitionWillBegin
{
[super dismissalTransitionWillBegin];
[[self containerView] removeGestureRecognizer:_tapGestureRecognizer];
[[[self presentingViewController] transitionCoordinator] animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
[[[self presentedView] layer] setShadowOpacity:0.0f];
[_backgroundView setAlpha:0.0];
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled]) {
[[[self presentedView] layer] setShadowOpacity:1.0f];
[_backgroundView setAlpha:1.0];
}
else {
[_backgroundView removeFromSuperview];
}
}];
}
- (void)dismissalTransitionDidEnd:(BOOL)completed
{
if (!completed) {
[[self containerView] addGestureRecognizer:_tapGestureRecognizer];
}
[super dismissalTransitionDidEnd:completed];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
[[self presentedView] setFrame:CGRectMake(CGRectGetMinX([[self containerView] frame]), CGRectGetMinY([[self containerView] frame]) + fmax(size.height - 500.0, 0.0), CGRectGetWidth([[self containerView] frame]), CGRectGetHeight([[self containerView] frame]) - fmax(size.height - 500.0, 0.0))];
} completion:nil];
}
- (void)_handleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
[[self presentingViewController] dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (gestureRecognizer == _tapGestureRecognizer) {
if ([touch view] != nil) {
return [touch view] != [self presentedView] && ![[touch view] isDescendantOfView:[self presentedView]];
}
}
return YES;
}
@end
依然可以前往 JYCustomTransition 看到全部代碼哦