转自:点击打开链接
下列列出一些我看到 有助于理解的地方。
iOS 7 中最让我激动的特性之一就是提供了新的 API 来支持自定义 view contrioller 之间的转场动画。
我们先来看看在 iOS 7 中 navigation controller 之间的默认的行为发生了那些改变:在 navigation controller 中,切换两个 view controller 的动画变得更有交互性。比方说你想要 pop 一个 view controller 出去,你可以用手指从屏幕的左边缘开始拖动,慢慢地把当前的 view controller 向右拖出屏幕去。
接下来,我们来看看这个新 API。很有趣的一个现象是,这部分 API 大量的使用了协议而不是具体的对象。这初看起来有点奇怪,但我个人更喜欢这样的 API 设计,因为这种设计给了我们这些开发者更大的灵活性。下面,让我们来做件简单的事情:在 Navigation Controller 中,实现一个自定义的 push 动画效果(本文中的示例代码托管在 Github)。
为了完成这个任务,需要实现UINavigationControllerDelegate
中的新方法:
- (id)
navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush) {
return self.animator;
}
return nil;
}
从上面的代码可以看出,我们可以根据不同的 operation(Push 或 Pop)返回不同的 animator。我们可以把 animator 存到一个属性中,从而在多个 operation 之间实现共享,或者我们也可以为每个 operation 都创建一个新的 animator 对象,这里的灵活性很大。
为了让动画运行起来,我们创建一个自定义类,并且实现 UIViewControllerContextTransitioning
这个协议:
@interface Animator : NSObject
@end
这个协议要求我们实现两个方法,其中一个定义了动画的持续时间:
- (NSTimeInterval)transitionDuration:(id )transitionContext
{
return 0.25;
}
另一个方法描述整个动画的执行效果:
- (void)animateTransition:(id)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1);
toViewController.view.alpha = 1;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
从上面的例子中,你可以看到如何运用协议的:这个方法中通过接受一个类型为 id
的参数,来获取 transition context。值得注意的是,执行完动画之后,我们需要调用 transitionContext 的 completeTransition:
这个方法来更新 view controller 的状态。剩下的代码和 iOS 7 之前的一样了,我们从 transition context 中得到了需要做转场的两个 view controller,然后使用最简单的 UIView
animation 来实现了转场动画。这就是全部代码了,我们已经实现了一个缩放效果的转场动画。
注意,这里只是为 Push 操作实现了自定义效果的转场动画,对于 Pop 操作,还是会使用默认的滑动效果,另外,上面我们实现的转场动画无法交互(不能用手触摸边框滑动),下面我们就来看看解决这个问题。
UINavigationControllerDelegate
的方法:
- (id )navigationController:(UINavigationController*)navigationController
interactionControllerForAnimationController:(id )animationController
{
return self.interactionController;
}
注意,在非交互式动画效果中,该方法返回 nil。
这里返回的 interaction controller 是 UIPercentDrivenInteractionTransition
类的一个实例,开发者不需要任何配置就可工作。我们创建了一个拖动手势(Pan Recognizer),下面是处理该手势的代码:
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
if (location.x > CGRectGetMidX(view.bounds)) {
navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
[self performSegueWithIdentifier:PushSegueIdentifier sender:self];
}
}
编者注 这里的代码有一点示意的意思,和实际代码有些出入,为了尊重原作者,我们没有进行修改,您可以参考原文在 Github 上的示例代码进行对比。
只有当用户从屏幕右半部分开始触摸的时候,我们才把下一次动画效果设置为交互式的(通过设置
interactionController
这个属性来实现),然后执行方法performSegueWithIdentifier:
(如果你不是使用的 storyboards,那么就直接调用pushViewController...
这类方法)。为了让转场动画持续进行,我们需要调用 interaction controller 的一个方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1;
[interactionController updateInteractiveTransition:d];
}
该方法会根据用户手指拖动的距离计算一个百分比,切换的动画效果也随着这个百分比来走。最酷的是,interaction controller 会和 animation controller 一起协作,我们只使用了简单的 UIView
animation 的动画效果,但是interaction controller 却控制了动画的执行进度,我们并不需要把 interaction controller 和 animation controller 关联起来,因为所有这些系统都以一种解耦的方式自动地替我们完成了。
最后,我们需要根据用户手势的停止状态来判断该操作是结束还是取消,并调用 interaction controller 中对应的方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
if ([panGestureRecognizer velocityInView:view].x < 0) {
[interactionController finishInteractiveTransition];
} else {
[interactionController cancelInteractiveTransition];
}
navigationControllerDelegate.interactionController = nil;
}
注意,当切换完成或者取消的时候,记得把 interaction controller 设置为 nil。因为如果下一次的转场是非交互的, 我们不应该返回这个旧的 interaction controller。
现在我们已经实现了一个完全自定义的可交互的转场动画了。通过简单的手势识别和 UIKit 提供的一个类,用几行代码就达到完成了。对于大部分的应用场景,你读到这儿就够用了,使用上面提到的方法就可以达到你想要的动画效果了。但如果你想更对转场动画或者交互效果进行深度定制,请继续阅读下面一节。
/**
* 自定义的动画类
* 实现协议------>@protocol UIViewControllerAnimatedTransitioning
* 这个接口负责切换的具体内容,也即“切换中应该发生什么”
*/
@interface MTHCustomAnimator : NSObject
@end
@implementation MTHCustomAnimator
// 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间
- (NSTimeInterval)transitionDuration:(id)transitionContext
{
return 1.0;
}
// 完成容器转场动画的主要方法,我们对于切换时的UIView的设置和动画都在这个方法中完成
- (void)animateTransition:(id)transitionContext
{
// 可以看做为destination ViewController
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 可以看做为source ViewController
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// 添加toView到容器上
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0.0;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
// 动画效果有很多,这里就展示个左偏移
fromViewController.view.transform = CGAffineTransformMakeTranslation(-320, 0);
toViewController.view.alpha = 1.0;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
// 声明过渡结束-->记住,一定别忘了在过渡结束时调用 completeTransition: 这个方法
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
从协议中两个方法可以看出,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从UIViewControllerContextTransitioning 协议的对象。通常情况下,当我们使用系统的类时,系统框架为我们提供的转场代理(Transitioning Delegates),为我们创建了转场上下文对象,并把它传递给动画控制器。
// MainViewController
@interface MTHMainViewController ()
@property (nonatomic,strong) MTHCustomAnimator *customAnimator;
@property (nonatomic,strong) PDTransitionAnimator *minToMaxAnimator;
@property (nonatomic,strong) MTHNextViewController *nextVC;
// 交互控制器 (Interaction Controllers) 通过遵从 UIViewControllerInteractiveTransitioning 协议来控制可交互式的转场。
@property (strong, nonatomic) UIPercentDrivenInteractiveTransition* interactionController;
@end
@implementation MTHMainViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.navigationItem.title = @"Demo";
self.view.backgroundColor = [UIColor yellowColor];
// 设置代理
self.navigationController.delegate = self;
// 设置转场动画
self.customAnimator = [[MTHCustomAnimator alloc] init];
self.minToMaxAnimator = [PDTransitionAnimator new];
self.nextVC = [[MTHNextViewController alloc] init];
// Present的代理和自定义设置
_nextVC.transitioningDelegate = self;
_nextVC.modalPresentationStyle = UIModalPresentationCustom;
// Push
UIButton *pushButton = [UIButton buttonWithType:UIButtonTypeSystem];
pushButton.frame = CGRectMake(140, 200, 40, 40);
[pushButton setTitle:@"Push" forState:UIControlStateNormal];
[pushButton addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:pushButton];
// Present
UIButton *modalButton = [UIButton buttonWithType:UIButtonTypeSystem];
modalButton.frame = CGRectMake(265, 500, 50, 50);
[modalButton setTitle:@"Modal" forState:UIControlStateNormal];
[modalButton addTarget:self action:@selector(modal) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:modalButton];
// 实现交互操作的手势
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didClickPanGestureRecognizer:)];
[self.navigationController.view addGestureRecognizer:panRecognizer];
}
- (void)push
{
[self.navigationController pushViewController:_nextVC animated:YES];
}
- (void)modal
{
[self presentViewController:_nextVC animated:YES completion:nil];
}
#pragma mark - UINavigationControllerDelegate iOS7新增的2个方法
// 动画特效
- (id) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
/**
* typedef NS_ENUM(NSInteger, UINavigationControllerOperation) {
* UINavigationControllerOperationNone,
* UINavigationControllerOperationPush,
* UINavigationControllerOperationPop,
* };
*/
if (operation == UINavigationControllerOperationPush) {
return self.customAnimator;
}else{
return nil;
}
}
// 交互
- (id )navigationController:(UINavigationController*)navigationController interactionControllerForAnimationController:(id )animationController
{
/**
* 在非交互式动画效果中,该方法返回 nil
* 交互式转场,自我理解意思是,用户能通过自己的动作来(常见:手势)控制,不同于系统缺省给定的push或者pop(非交互式)
*/
return _interactionController;
}
#pragma mark - Transitioning Delegate (Modal)
// 前2个用于动画
-(id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
self.minToMaxAnimator.animationType = AnimationTypePresent;
return _minToMaxAnimator;
}
-(id)animationControllerForDismissedController:(UIViewController *)dismissed
{
self.minToMaxAnimator.animationType = AnimationTypeDismiss;
return _minToMaxAnimator;
}
// 后2个用于交互
- (id )interactionControllerForPresentation:(id )animator
{
return _interactionController;
}
- (id )interactionControllerForDismissal:(id )animator
{
return nil;
以上实现的是非交互的转场,指的是完全按照系统指定的切换机制,用户无法中途取消或者控制进度切换.那怎么来实现交互转场呢:
UIPercentDrivenInteractiveTransition实现了UIViewControllerInteractiveTransitioning接口的类,可以用一个百分比来控制交互式切换的过程。我们在手势识别中只需要告诉这个类的实例当前的状态百分比如何,系统便根据这个百分比和我们之前设定的迁移方式为我们计算当前应该的UI渲染,十分方便。具体的几个重要方法:
-(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通过手势识别的长度之类的来计算一个值,然后进行更新。之后的例子里会看到详细的用法
-(void)cancelInteractiveTransition 报告交互取消,返回切换前的状态
–(void)finishInteractiveTransition 报告交互完成,更新到切换后的状态
#pragma mark - 手势交互的主要实现--->UIPercentDrivenInteractiveTransition
- (void)didClickPanGestureRecognizer:(UIPanGestureRecognizer*)recognizer
{
UIView* view = self.view;
if (recognizer.state == UIGestureRecognizerStateBegan) {
// 获取手势的触摸点坐标
CGPoint location = [recognizer locationInView:view];
// 判断,用户从右半边滑动的时候,推出下一个VC(根据实际需要是推进还是推出)
if (location.x > CGRectGetMidX(view.bounds) && self.navigationController.viewControllers.count == 1){
self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
//
[self presentViewController:_nextVC animated:YES completion:nil];
}
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
// 获取手势在视图上偏移的坐标
CGPoint translation = [recognizer translationInView:view];
// 根据手指拖动的距离计算一个百分比,切换的动画效果也随着这个百分比来走
CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds));
// 交互控制器控制动画的进度
[self.interactionController updateInteractiveTransition:distance];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPoint translation = [recognizer translationInView:view];
// 根据手指拖动的距离计算一个百分比,切换的动画效果也随着这个百分比来走
CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds));
// 移动超过一半就强制完成
if (distance > 0.5) {
[self.interactionController finishInteractiveTransition];
} else {
[self.interactionController cancelInteractiveTransition];
}
// 结束后一定要置为nil
self.interactionController = nil;
}
}