自定义过渡动画

本文由我们团队的王瑞华童鞋撰写。


在iOS 7 发布之后,UI上的两个重要的变化是丰富的动画使用和界面上各个方面对真实物理世界的模拟。然而交互式自定义过渡不是一个新特性,至少在iOS 3.2 中就已经存在了。例如,翻页动画就不仅是从一个页面到另一个的过渡。它是一个交互式过渡——随着手指移动的过渡。交互式自定义过渡是提升应用品质,使其在 App Store 大放异彩的重要工具。iOS 7 之后 SDK 允许自定义大部分过渡,包括视图控制器的出现和消失、UINavigationController 的推入和淡出过渡、UITabBarController 的过渡,甚至是集合视图的布局变化过渡。

UICollectionView 的过渡动画

在iOS 7 之后的日历和照片应用当中,就运用集合视图的过渡方式实现了一个viewController 向另一个 viewControler 的过渡。在 UICollectionViewController 中引入了 useLayoutToLayoutNavigationTransitions 这一属性。当此属性设置为 YES 时,在将 collecionViewController 推入导航控制器之前,推入过渡会使用 -setColletionViewLayout:animated:来完成集合视图布局的变化。开发者要做的只是设置 useLayoutToLayoutNavigationTransitions = YES,剩下的交给系统处理便可以。注意,该方法要求两个 collectionView 拥有相同的数据。

自定义 viewController 过渡

实现 transition delegate 是 transition 动画和自定义 presentation 的起点。该 transition delegate 就是开发者定义一个对象并遵循 UIViewControllerTransitioningDelegate 协议。下面看看该协议中包含什么。

//  return 动画对象,该动画对象符合 UIViewControllerAnimatedTransitioning 协议,负责显示 present 动画。
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
    
// return 动画对象,负责显示 dismiss 动画。
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
    
// return 交互式动画对象,该动画符合 UIViewControllerInteractiveTransitioning 协议,采用触摸手势或手势识别器作为动画的驱动,显示 present 动画。
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
    
// return 交互式动画,显示 dismiss 动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
    
// return UIPresentationController,系统已经提供了各个演示样式。
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

示意图如下:

解释了这么多,对过渡动画需要遵循的 delegate 已经有了初步的了解,下面通过present 的方式实现一个 push 动画。

  1. 我们首先创建两个 viewController VC1 和 VC2,我们要实现 VC1 present 出 VC2,同时模拟 push 和 pop 动画的效果。
  2. 然后我们需要创建出两个管理过渡动画的类,用于管理 present 动画和 dismiss 动画,两个管理类大体实现相似。在 viewController类中遵从 UIViewControllerTransitioningDelegate 协议,实现协议方法。

以下是 present 动画的实例,dismiss 动画与之相似,不再赘述。

CustomPushAnimation.h

@interface CustomPushAnimation : NSObject <UIViewControllerAnimatedTransitioning>
// 告诉系统动画将花费的时间
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.35;
}
    
// 执行实际的动画
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
        
    // 目标 viewController
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        
    CGRect toFrame = [transitionContext finalFrameForViewController:toVC];
    // 如果要对视图做转场动画,视图就必须要加入containerView中才能进行,可以理解containerView管理着所有做转场动画的视图
    UIView *containerView = [transitionContext containerView];
        
    [containerView addSubview:toVC.view];
        
    toVC.view.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, toVC.view.frame.size.width, toVC.view.frame.size.height);
        
    NSTimeInterval duration = [self transitionDuration:transitionContext];
        
    [UIView animateWithDuration:duration animations:^{
        toVC.view.frame = toFrame;
    } completion:^(BOOL finished) {
        // 通知系统的过渡动画就完成了。你动画完成后必须调用这个方法通知系统的过渡动画就完成了。传递的参数必须显示动画是否成功完成。
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];
}

使用交互式自定义过渡

交互式过渡是由事件驱动的。可以是动作事件或者手势,通常为手势。要实现一个交互式过渡,除了需要跟之前相同的动画,还需要告诉交互控制器动画完成了多少。开发者只需要确定已经完成的百分比,其他交给系统去做就可以了。例如,(平移和缩放的距离 / 速度的量可以作为计算完成的百分比的参数)。

交互式控制器实现了 UIViewControllerInteractiveTransitioning 协议,该协议中包含如下方法:

- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

这个方法里只能有一个动画块,动画应该基于 UIView 而不是图层,交互式过渡不支持 CATransitionCALayer 动画。

交互式过渡的交互控制器应当是 UIPercentDrivenInteractiveTransition 子类。动画类负责计算完成百分比,系统会自动更新动画的中间状态。

- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;

根据手势移动或者缩放的距离,计算出百分比并调用相应方法。

下面是简单的交互式过渡的代码片段,该事例只做了交互式 dismiss 的部分,也只罗列了比较关键的部分。

SecondViewController.m

#pragma mark - UIViewControllerTransitioningDelegate    
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return self.dismissAnimator;
}

- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
    // 当切换完成或者取消的时候,记得把 return 设置为 nil。因为如果下一次的转场是非交互的, 我们不应该返回这个旧的 interactionAnimator。
    return self.interactiveAnimator.isInteractive? self.interactiveAnimator: nil;
}

CustomInteractiveTransition.m

- (instancetype)initWithViewController:(UIViewController *)viewController {
    if (self = [super init]) {
        _isInteractive = NO;
        _viewController = viewController;
    }
    return self;
}
    
- (void)panGestureAction:(UIPanGestureRecognizer *)recognizer {
// 计算手势距离与屏幕的比例,从而决定交互动画的进度
    CGFloat progress = [recognizer translationInView:self.viewController.view].y / (self.viewController.view.bounds.size.height * 1.0);
    progress = MIN(1.0, MAX(0.0, progress));
// 标记手势交互状态,为点击按钮等非交互式 dismiss 动画留出余地
    self.isInteractive = YES;
    if (recognizer.state == UIGestureRecognizerStateBegan) {  
        [self.viewController dismissViewControllerAnimated:YES completion:nil];
    }
    else if (recognizer.state == UIGestureRecognizerStateChanged) {
        [self updateInteractiveTransition:progress];
    }
    else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
        if (progress > 0.5) {
            [self finishInteractiveTransition];
        }
        else {
            [self cancelInteractiveTransition];
        }
    }
}

UIViewControllerTransitionCoordinator 过渡协调器

所有的过渡都会创建一个过渡协调器,无论是否自定义。也就是说,当执行默认的模态过渡或push过渡时,也可以对视图中的其他部分做动画。

- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion; 
- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
- (void)notifyWhenInteractionEndsUsingBlock: (void (^)(id <UIViewControllerTransitionCoordinatorContext>context))handler;

在 iOS中,可以取消一个过渡。这意味着,第二个视图的 -viewWillApear 被调用,但 -viewDidApear不一定被调用。如果代码写的假定 -viewDidAppear 总是在 -viewWillAppear 之后执行则需要重新考虑逻辑实现。这种情况下UIViewControllerTransitionCoordinator 就有用了。在交互式过渡结束的时候,会在 block 中收到通知。

Demo在这

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容