介绍
回到10看一下,通过创建导航栏控制器渐隐转换效果,我们是如何创建自定义控制器转换的。视图控制器交互转换添加了另一个维度,允许转换时交互式控制,通常使用手势。
今天学一下如何为模态视图控制器创建交互转换,类似卡片翻转。当用户拖动卡片翻转时,会跟随手指进行动画。
翻转动画
交互转换需要自定义动画,因此我们需要创建一个自定义动画,类似于创建渐隐效果的动画。我们需要符合
我们定义了一个属性dismissal,用来标识卡片翻转方向。
像之前我们需要实现下面两个方法:
animation方法看起来很复杂,但是它只是用了UIView新的键帧动画,在day11中已经看过了。需要注意的就是dismissal属性决定将要旋转的方向。更多的细节可以看一下day10和day11。
现在我们已经有了一个动画对象,需要把它关联到视图控制器转换中。我们创建了一个包含两个控制器的storyboard。第一个控制器包含一个按钮,点击时弹出一个模态控制器。第二个控制器包含一个按钮,点击时通过下面方法消失这个模态控制器:
如果现在启动app,我们能看到在弹出和消失模态控制器时的标准转换动画,但是我们的兴趣不是使用传统的动画,让我们添加自己定义的转换动画:
在day10我们控制了导航栏控制器转换的动画,所以实现了UINavigationControllerDelegate协议。这里我们控制模态视图控制器的转换,所以要实现UIViewControllerTransitioningDelegate协议。两个协议方法类似,但是现在我们需要下面两个方法areanimationControllerForPresentedController:presentingController:sourceController:和animationControllerForDismissedController:。
我们在主要的控制器中实现这些方法,并返回上面创建的动画对象:
注意在‘present’和‘dismiss’方法中对dismissal属性的设置,它决定翻转的方向。剩下就是在合适的视图控制器上设置自己作为转换代理。由于我们谈论的是弹出和消失一个视图控制器,而且这些方法都涉及视图控制器,所以代理必须设置为这个控制器。因为模态视图控制在storyboard流程中创建的,我们需要实现这个方法prepareForSegue:sender::
如果现在运行应用,你应该可以看到我们自定义的垂直卡片翻转动画已经替换开始默认的滑动动画。
转换交互
在UIViewControllerTransitioningDelegate协议中有两个方法来支持转换交互的,两个方法都返回一个支持UIViewControllerInteractiveTransitioning协议的对象。我们可以提前创建实现这个协议的对象,但是苹果已经提供了一个类UIPercentDrivenInteractiveTransition,它包含大多数用例。
一个交互器(符合UIViewControllerInteractiveTransitioning协议的对象)的概念是它控制动画(符合UIViewControllerAnimatedTransitioning协议的对象)的流程。UIPercentDrivenInteractiveTransition类提供一个方法能定义当前动画进度的百分比,以及取消动画和完成动画。
一旦我们理解我们的项目,这个部分将变得非常清晰了。我们想创建一个拖动手势,用户可以垂直拖动,来控制弹出和消失模态视图控制器。我将创建UIPercentDrivenInteractiveTransition的子类,有下列属性:
手势识别器像我们已经讨论过的一样,我们也提供了一个属性方法来标识交互是否在处理中,以及最后一个属性来指示当前弹出的控制器。后面会看到为什么我们需要这些属性,现在我们需要看一下它需要实现的一个简单的协议:
这个子类的实现:
首先我们需要重定义2个内部可读写的属性,并在构造时间创建手势识别器并设置目标方法。注意现在我们不会把它绑定到任何一个视图上,我们已经把它设置为属性了,所以可以在外面设置。
手势目的方法如下:
这是一个相当标准的手势方法,有每一个手势状态的处理事件。开始切换前我们计算了动画完成百分比,也就是说,给出手势已经滑动的长度,我们考虑转换将如何完成。然后转换如下:
1、 Began。这里我们设置交互正在当前处理流程中,并且使用我们已经添加到presentingViewController的方法开始转换。着重注意,我们使用手势开始转换的。交互器现在没有使用,只用了手势处理。因为现在还没有转换发生。一旦当我们在视图控制器中调用了这个方法(我们要正确实现),转换将会开始,并且交互器也将开始它的工作。
2、 Changed。现在我们一定在交互转换的过程中(因为当手势开始时我们已经开始了一个),因此我们仅仅调用这个被子类提供的方法,方法定义了我们的转换如何完成updateInteractiveTransition:。它将设置当前转换按动画指定的比例完成。
3、 Ended。当手势结束时,我们需要决定现在是完成转换还是取消转换。如果百分比低于0.5,我们调用子类提供的帮助方法取消转换(cancelInteractiveTransition),否则就结束的转换(finishInteractiveTransition)。我们也需要更新属性in-progress,因为转换已经结束了。
4、 Canceled。如果手势被取消,我们应该取消转换并更新属性interactionInProgress 。
在交互器中需要的代码已经全部完成,剩下的就是把所有的代码串起来。在开始控制器里面的协议
这些方法都是相同的(弹出和消失)。如果我们正在执行一个交互转换,我们仅仅想返回一个交互器,也就是说,如果用户是点击按钮而不是拖动屏幕,我们应该执行一个没有交互的转换。这就是交互器中属性interactionInProgress 的目的。这里我们返回ivar_animationInteractor ,它是在viewDidLoad中设置的:
当我们在交互器中创建手势识别器时,实际上并没有添加到view上,而在视图控制器的viewDidAppear中添加的:
一般我们是把手势添加到view上,但这里我们添加到了window对象上。这是因为当动画发生时,视图控制器将会移动,因此手势识别器不会像预料中那样执行。添加到window上可以保证它的行为与我们期待的一样。如果我们执行一个导航栏控制的转换,我们应该把它添加到导航栏控制器的view上。手势识别器在ViewDidAppear:中添加,因为在这时window对象才设置正确:
最后一个难题是设置交互器的属性presentingVC。为了实现设置,控制器需要实现
现在我们已经实现了协议中需要的方法,我们可以在ViewDidAppear中设置交互器的属性,这样可以保证在基础控制器被展现的时候正确设置,不管它是第一次展现还是当模态视图消失时:
所以当用户开始手势时,交互器将在当前控制器调用proceedToNextViewController ,当前控制器将开始弹出模态视图,这也是我们期待的。
在模态视图控制器中去执行相同操作也是需要与交互器关联的(所以更新presentingVC属性)。
在主控制器prepareForSegue:方法中设置该属性:
协议SCInteractiveTransitionViewControllerDelegate在proceedToNextViewController 方法中执行:
- (void)proceedToNextViewController { [self dismissViewControllerAnimated:YES completion:NULL]; }
最后,当模态视图控制器出现时,我们需要更新交互器属性,以确保下一个时间开始交互转换(也就是说,用户执行垂直拖动),它调用模态控制器的方法,不是主控制器的:
这就是全部。如果你现在运行应用,并垂直拖动,你将看到模态视图控制器将跟随着你的手指转换。如果你拖动到一半多的距离,转换将完成,否则会回到初始状态。
介绍
视图交互转换可能成了一个非常复杂的话题,主要由于大量需要实现的协议,并且不太容易观察哪个控制器实现哪个协议(例如哪个控制器应该实现手势识别器?)。然而,实际上,我们通过少量的代码确实获得了非常强大的功能。我鼓励你去试着自定义一个视图控制器转换功能,但要知道,能力越大,责任越大,虽然我们现在能够定义许多怪诞的视图控制器转换方式,但也应该确保不要让用户体验过分复杂。
回到10看一下,通过创建导航栏控制器渐隐转换效果,我们是如何创建自定义控制器转换的。视图控制器交互转换添加了另一个维度,允许转换时交互式控制,通常使用手势。
今天学一下如何为模态视图控制器创建交互转换,类似卡片翻转。当用户拖动卡片翻转时,会跟随手指进行动画。
翻转动画
交互转换需要自定义动画,因此我们需要创建一个自定义动画,类似于创建渐隐效果的动画。我们需要符合
1 | UIViewControllerAnimatedTransitioning协议的对象: |
2 | @interface SCFlipAnimation : NSObject <UIViewControllerAnimatedTransitioning> |
3 | @property (nonatomic, assign) BOOL dismissal; |
4 | @end |
我们定义了一个属性dismissal,用来标识卡片翻转方向。
像之前我们需要实现下面两个方法:
01 | - ( void )animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext |
02 | { |
03 | // Get the respective view controllers |
04 | UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; |
05 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; |
06 |
07 | // Get the views |
08 | UIView *containerView = [transitionContext containerView]; |
09 | UIView *fromView = fromVC.view; |
10 | UIView *toView = toVC.view; |
11 |
12 | // Add the toView to the container |
13 | [containerView addSubview:toView]; |
14 |
15 | // Set the frames |
16 | CGRect initialFrame = [transitionContext initialFrameForViewController:fromVC]; |
17 | fromView.frame = initialFrame; |
18 | toView.frame = initialFrame; |
19 |
20 | // Start building the transform - 3D so need perspective |
21 | CATransform3D transform = CATransform3DIdentity; |
22 | transform.m34 = -1/CGRectGetHeight(initialFrame); |
23 | containerView.layer.sublayerTransform = transform; |
24 |
25 | CGFloat direction = self.dismissal ? -1.0 : 1.0; |
26 |
27 | toView.layer.transform = CATransform3DMakeRotation(-direction * M_PI_2, 1, 0, 0); |
28 | [UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext] |
29 | delay:0.0 |
30 | options:0 |
31 | animations:^{ |
32 | // First half is rotating in |
33 | [UIView addKeyframeWithRelativeStartTime:0.0 |
34 | relativeDuration:0.5 |
35 | animations:^{ |
36 | fromView.layer.transform = CATransform3DMakeRotation(direction * M_PI_2, 1, 0, 0); |
37 | }]; |
38 | [UIView addKeyframeWithRelativeStartTime:0.5 |
39 | relativeDuration:0.5 |
40 | animations:^{ |
41 | toView.layer.transform = CATransform3DMakeRotation(0, 1, 0, 0); |
42 | }]; |
43 | } completion:^( BOOL finished) { |
44 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; |
45 | }]; |
46 | } |
47 |
48 | - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext |
49 | { |
50 | return 1.0; |
51 | } |
animation方法看起来很复杂,但是它只是用了UIView新的键帧动画,在day11中已经看过了。需要注意的就是dismissal属性决定将要旋转的方向。更多的细节可以看一下day10和day11。
现在我们已经有了一个动画对象,需要把它关联到视图控制器转换中。我们创建了一个包含两个控制器的storyboard。第一个控制器包含一个按钮,点击时弹出一个模态控制器。第二个控制器包含一个按钮,点击时通过下面方法消失这个模态控制器:
1 | - (IBAction)handleDismissPressed:(id)sender { |
2 | [self dismissViewControllerAnimated:YES completion:NULL]; |
3 | } |
如果现在启动app,我们能看到在弹出和消失模态控制器时的标准转换动画,但是我们的兴趣不是使用传统的动画,让我们添加自己定义的转换动画:
在day10我们控制了导航栏控制器转换的动画,所以实现了UINavigationControllerDelegate协议。这里我们控制模态视图控制器的转换,所以要实现UIViewControllerTransitioningDelegate协议。两个协议方法类似,但是现在我们需要下面两个方法areanimationControllerForPresentedController:presentingController:sourceController:和animationControllerForDismissedController:。
我们在主要的控制器中实现这些方法,并返回上面创建的动画对象:
01 | @interface SCViewController () <UIViewControllerTransitioningDelegate> { |
02 | SCFlipAnimation *_flipAnimation; |
03 | } |
04 | @end |
05 |
06 | @implementation SCViewController |
07 |
08 | - ( void )viewDidLoad |
09 | { |
10 | [super viewDidLoad]; |
11 | // Do any additional setup after loading the view, typically from a nib. |
12 | _flipAnimation = [SCFlipAnimation new ]; |
13 | } |
14 |
15 | - (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source |
16 | { |
17 | _flipAnimation.dismissal = NO; |
18 | return _flipAnimation; |
19 | } |
20 |
21 | - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed |
22 | { |
23 | _flipAnimation.dismissal = YES; |
24 | return _flipAnimation; |
25 | } |
注意在‘present’和‘dismiss’方法中对dismissal属性的设置,它决定翻转的方向。剩下就是在合适的视图控制器上设置自己作为转换代理。由于我们谈论的是弹出和消失一个视图控制器,而且这些方法都涉及视图控制器,所以代理必须设置为这个控制器。因为模态视图控制在storyboard流程中创建的,我们需要实现这个方法prepareForSegue:sender::
1 | - ( void )prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender |
2 | { |
3 | if ([segue.destinationViewController isKindOfClass:[SCModalViewController class ]]) { |
4 | // Set the delegate |
5 | SCModalViewController *vc = (SCModalViewController *)segue.destinationViewController; |
6 | vc.transitioningDelegate = self; |
7 | } |
8 | } |
如果现在运行应用,你应该可以看到我们自定义的垂直卡片翻转动画已经替换开始默认的滑动动画。
转换交互
在UIViewControllerTransitioningDelegate协议中有两个方法来支持转换交互的,两个方法都返回一个支持UIViewControllerInteractiveTransitioning协议的对象。我们可以提前创建实现这个协议的对象,但是苹果已经提供了一个类UIPercentDrivenInteractiveTransition,它包含大多数用例。
一个交互器(符合UIViewControllerInteractiveTransitioning协议的对象)的概念是它控制动画(符合UIViewControllerAnimatedTransitioning协议的对象)的流程。UIPercentDrivenInteractiveTransition类提供一个方法能定义当前动画进度的百分比,以及取消动画和完成动画。
一旦我们理解我们的项目,这个部分将变得非常清晰了。我们想创建一个拖动手势,用户可以垂直拖动,来控制弹出和消失模态视图控制器。我将创建UIPercentDrivenInteractiveTransition的子类,有下列属性:
1 | @interface SCFlipAnimationInteractor : UIPercentDrivenInteractiveTransition |
2 |
3 | @property (nonatomic, strong, readonly) UIPanGestureRecognizer *gestureRecogniser; |
4 | @property (nonatomic, assign, readonly) BOOL interactionInProgress; |
5 | @property (nonatomic, weak) UIViewController<SCInteractiveTransitionViewControllerDelegate> *presentingVC; |
6 |
7 | @end |
手势识别器像我们已经讨论过的一样,我们也提供了一个属性方法来标识交互是否在处理中,以及最后一个属性来指示当前弹出的控制器。后面会看到为什么我们需要这些属性,现在我们需要看一下它需要实现的一个简单的协议:
1 | @protocol SCInteractiveTransitionViewControllerDelegate <NSObject> |
2 | - ( void )proceedToNextViewController; |
3 | @end |
这个子类的实现:
01 | @interface SCFlipAnimationInteractor () |
02 | @property (nonatomic, strong, readwrite) UIPanGestureRecognizer *gestureRecogniser; |
03 | @property (nonatomic, assign, readwrite) BOOL interactionInProgress; |
04 | @end |
05 |
06 | @implementation SCFlipAnimationInteractor |
07 | - (instancetype)init |
08 | { |
09 | self = [super init]; |
10 | if (self) { |
11 | self.gestureRecogniser = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; |
12 | } |
13 | return self; |
14 | } |
15 | @end |
首先我们需要重定义2个内部可读写的属性,并在构造时间创建手势识别器并设置目标方法。注意现在我们不会把它绑定到任何一个视图上,我们已经把它设置为属性了,所以可以在外面设置。
手势目的方法如下:
01 | - ( void )handlePan:(UIPanGestureRecognizer *)pgr |
02 | { |
03 | CGPoint translation = [pgr translationInView:pgr.view]; |
04 | CGFloat percentage = fabs (translation.y / CGRectGetHeight(pgr.view.bounds)); |
05 | switch (pgr.state) { |
06 | case UIGestureRecognizerStateBegan: |
07 | self.interactionInProgress = YES; |
08 | [self.presentingVC proceedToNextViewController]; |
09 | break ; |
10 |
11 | case UIGestureRecognizerStateChanged: { |
12 | [self updateInteractiveTransition:percentage]; |
13 | break ; |
14 | } |
15 |
16 | case UIGestureRecognizerStateEnded: |
17 | if (percentage < 0.5) { |
18 | [self cancelInteractiveTransition]; |
19 | } else { |
20 | [self finishInteractiveTransition]; |
21 | } |
22 | self.interactionInProgress = NO; |
23 | break ; |
24 |
25 | case UIGestureRecognizerStateCancelled: |
26 | [self cancelInteractiveTransition]; |
27 | self.interactionInProgress = NO; |
28 |
29 | default : |
30 | break ; |
31 | } |
32 | } |
这是一个相当标准的手势方法,有每一个手势状态的处理事件。开始切换前我们计算了动画完成百分比,也就是说,给出手势已经滑动的长度,我们考虑转换将如何完成。然后转换如下:
1、 Began。这里我们设置交互正在当前处理流程中,并且使用我们已经添加到presentingViewController的方法开始转换。着重注意,我们使用手势开始转换的。交互器现在没有使用,只用了手势处理。因为现在还没有转换发生。一旦当我们在视图控制器中调用了这个方法(我们要正确实现),转换将会开始,并且交互器也将开始它的工作。
2、 Changed。现在我们一定在交互转换的过程中(因为当手势开始时我们已经开始了一个),因此我们仅仅调用这个被子类提供的方法,方法定义了我们的转换如何完成updateInteractiveTransition:。它将设置当前转换按动画指定的比例完成。
3、 Ended。当手势结束时,我们需要决定现在是完成转换还是取消转换。如果百分比低于0.5,我们调用子类提供的帮助方法取消转换(cancelInteractiveTransition),否则就结束的转换(finishInteractiveTransition)。我们也需要更新属性in-progress,因为转换已经结束了。
4、 Canceled。如果手势被取消,我们应该取消转换并更新属性interactionInProgress 。
在交互器中需要的代码已经全部完成,剩下的就是把所有的代码串起来。在开始控制器里面的协议
1 | UIViewControllerTransitioningDelegate: |
2 | - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator { return _animationInteractor.interactionInProgress ? _animationInteractor : nil; } - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator { return _animationInteractor.interactionInProgress ? _animationInteractor : nil; } |
这些方法都是相同的(弹出和消失)。如果我们正在执行一个交互转换,我们仅仅想返回一个交互器,也就是说,如果用户是点击按钮而不是拖动屏幕,我们应该执行一个没有交互的转换。这就是交互器中属性interactionInProgress 的目的。这里我们返回ivar_animationInteractor ,它是在viewDidLoad中设置的:
1 | - ( void )viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib._animationInteractor = [SCFlipAnimationInteractor new]; _flipAnimation = [SCFlipAnimation new]; } |
当我们在交互器中创建手势识别器时,实际上并没有添加到view上,而在视图控制器的viewDidAppear中添加的:
1 | - ( void )viewDidAppear:( BOOL )animated { // Add the gesture recogniser to the window first render timeif (![self.view.window.gestureRecognizers containsObject:_animationInteractor.gestureRecogniser]) { [self.view.window addGestureRecognizer:_animationInteractor.gestureRecogniser]; } } |
一般我们是把手势添加到view上,但这里我们添加到了window对象上。这是因为当动画发生时,视图控制器将会移动,因此手势识别器不会像预料中那样执行。添加到window上可以保证它的行为与我们期待的一样。如果我们执行一个导航栏控制的转换,我们应该把它添加到导航栏控制器的view上。手势识别器在ViewDidAppear:中添加,因为在这时window对象才设置正确:
最后一个难题是设置交互器的属性presentingVC。为了实现设置,控制器需要实现
1 | SCInteractiveTransitionViewControllerDelegate协议。在我们主控制器这非常简单: |
2 | @interface SCViewController () <SCInteractiveTransitionViewControllerDelegate, UIViewControllerTransitioningDelegate> {SCFlipAnimationInteractor *_animationInteractor; SCFlipAnimation *_flipAnimation; } @end#pragma mark - SCInteractiveTransitionViewControllerDelegate methods - ( void )proceedToNextViewController { [self performSegueWithIdentifier:@ "displayModal" sender:self]; } |
现在我们已经实现了协议中需要的方法,我们可以在ViewDidAppear中设置交互器的属性,这样可以保证在基础控制器被展现的时候正确设置,不管它是第一次展现还是当模态视图消失时:
1 | - ( void )viewDidAppear:( BOOL )animated { ... // Set the recipeint of the interactor_animationInteractor.presentingVC = self; } |
所以当用户开始手势时,交互器将在当前控制器调用proceedToNextViewController ,当前控制器将开始弹出模态视图,这也是我们期待的。
在模态视图控制器中去执行相同操作也是需要与交互器关联的(所以更新presentingVC属性)。
1 | @interface SCModalViewController : UIViewController <SCInteractiveTransitionViewControllerDelegate> ... @property (nonatomic, weak) SCFlipAnimationInteractor *interactor; @end |
在主控制器prepareForSegue:方法中设置该属性:
1 | - ( void )prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.destinationViewController isKindOfClass:[SCModalViewController class ]]) { // Set the delegate... vc.interactor = _animationInteractor; } } |
协议SCInteractiveTransitionViewControllerDelegate在proceedToNextViewController 方法中执行:
- (void)proceedToNextViewController { [self dismissViewControllerAnimated:YES completion:NULL]; }
最后,当模态视图控制器出现时,我们需要更新交互器属性,以确保下一个时间开始交互转换(也就是说,用户执行垂直拖动),它调用模态控制器的方法,不是主控制器的:
1 | - ( void )viewDidAppear:( BOOL )animated { // Reset which view controller should be the receipient of the// interactor's transitionself.interactor.presentingVC = self; } |
这就是全部。如果你现在运行应用,并垂直拖动,你将看到模态视图控制器将跟随着你的手指转换。如果你拖动到一半多的距离,转换将完成,否则会回到初始状态。
介绍
视图交互转换可能成了一个非常复杂的话题,主要由于大量需要实现的协议,并且不太容易观察哪个控制器实现哪个协议(例如哪个控制器应该实现手势识别器?)。然而,实际上,我们通过少量的代码确实获得了非常强大的功能。我鼓励你去试着自定义一个视图控制器转换功能,但要知道,能力越大,责任越大,虽然我们现在能够定义许多怪诞的视图控制器转换方式,但也应该确保不要让用户体验过分复杂。