本文由我们团队的王瑞华童鞋撰写。
在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 动画。
- 我们首先创建两个 viewController VC1 和 VC2,我们要实现 VC1 present 出 VC2,同时模拟 push 和 pop 动画的效果。
- 然后我们需要创建出两个管理过渡动画的类,用于管理 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
而不是图层,交互式过渡不支持 CATransition
或 CALayer
动画。
交互式过渡的交互控制器应当是 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 中收到通知。