前言
本文是我学习了onevcat的这篇转场入门做的一点笔记。
今天我们来实现一个简单的自定义转场,我们先来看看这篇文章将要实现的一个效果图吧:
过程详解
热身准备
我们先创建一个工程,首先用storyboard快速的创建两个控制器,一个作为主控制器,叫ViewController
,另外一个作为present出来的控制器,叫PresentViewController
,并且用autoLayout快速搭建好界面。就像这样:
我们先做好点击ViewController
上面的按钮,present出 PresentViewController
,点击PresentViewController
上面的按钮,dismiss掉PresentViewController
的逻辑。这里有两个注意点:
因为此处我使用了
segue
,所以在ViewController
按钮点击的时候,我们只需要这样调用就行。1
2
3
4
-(IBAction)presentBtnClick:(UIButton *)sender {
[self performSegueWithIdentifier:@"PresentSegue" sender:nil];
}我们平时写dismiss的时候,一般都会是在第二个控制器中直接给self发送
dismissViewController
的相关方法。在现在的SDK中,如果当前的VC是被显示的话,这个消息会被直接转发到显示它的VC去。但是这并不是一个好的实现,违反了程序设计的哲学,也很容易掉到坑里。所以我们用标准的delegate
方式实现dismiss
。
首先我们在PresentViewController
控制器中申明一个代理方法。
1 |
|
在button的点击事件中,让代理去完成关闭当前控制器的工作。
1 |
|
与此同时,在ViewController
中需要设置PresentViewController
的代理,并且实现代理方法:
1 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { |
OK,到这里,我们一个基本的转场就完成了(这也是系统自带的一个效果)。like this:
主要内容
接下来,要接触我们今天要讲的主要内容了,我们用iOS7中一个新的类UIViewControllerTransitioning
来实现自定义转场。
UIViewControllerAnimatedTransitioning
首先我们需要一个实现了协议名为UIViewControllerAnimatedTransitioning
的对象。创建一个类叫做PresentAnimation
继承于NSObject
并且实现了UIViewControllerAnimatedTransitioning
协议。(注意:需要导入UIKit框架)
1 | @interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning> |
这个协议负责转场的具体内容。开发者在做自定义切换效果时大部门代码会是用来实现这个协议的,这个协议只有两个方法必须要实现的:
1 | // 返回动画的时间 |
实现这两个方法
1 | - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { |
注意点
UITransitionContextToViewControllerKey
与UITransitionContextFromViewControllerKey
比如从A present 出B,此时A是FromViewController
,B是ToViewController
如果从B dismiss 到A,此时A是ToViewController
,B是FromViewController
UIViewControllerTransitioningDelegate
这个接口的作用比较单一,在需要VC切换的时候系统会向实现了这个接口的对象询问是否需要使用自定义转场效果。
所以,一个比较好的地方是直接在主控制器ViewController
中实现这个协议。
在ViewController
中完成如下代码:
1 | @interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate> |
现在看下我们的效果:
相对于上面系统自带的效果来说,我们在present出第二个控制器的时候,带有弹簧效果。
手势驱动百分比切换
现在我们增加一个功能,就是用手势滑动来dismiss,通俗的说,就是让present出来的那个控制器使用手势dismiss。
创建一个类,继承自
UIPercentDrivenInteractiveTransition
1
2
3
4
@interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition
-(void)panToDismiss:(UIViewController *)viewController;
@end- 我们写一个方法提供给外部类调用。让外部类可以看到传入手势dismiss的VC的入口。
既然传入了这个需要手势dismiss的VC,我们就需要保存一下,方便当前类在其他地方使用,所以我们新建一个属性来保存这个传入的VC。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@interface PanInteractiveTransition ()
@property (nonatomic, strong) UIViewController *presentVC;
@end
@implementation PanInteractiveTransition
-(void)panToDismiss:(UIViewController *)viewController {
self.presentVC = viewController;
UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
[self.presentVC.view addGestureRecognizer:panGestR];
}
-(void)panGestureAction:(UIPanGestureRecognizer *)pan {
CGPoint transition = [pan translationInView:self.presentVC.view];
NSLog(@"%.2f",transition.y);
switch (pan.state) {
case UIGestureRecognizerStateBegan:{
[self.presentVC dismissViewControllerAnimated:YES completion:nil];
}
break;
case UIGestureRecognizerStateChanged:{ //
CGFloat percent = MIN(1.0, transition.y/300);
[self updateInteractiveTransition:percent];
}
break;
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
if (pan.state == UIGestureRecognizerStateCancelled) { // 手势取消
[self cancelInteractiveTransition];
}else{
[self finishInteractiveTransition];
}
}
break;
default:
break;
}
}和创建
PresentAnimation
一样,我们创建一个一个DismissAnimation
类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
@implementation DismissAnimation
-(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
return 0.4f;
}
-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
UIView *contrainerView = [transitionContext containerView];
[contrainerView addSubview:toVC.view];
[contrainerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = finalRect;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end最后,我们在主控制器中添加一个手势驱动的对象,一个dismiss转场的对象,然后懒加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25-(PanInteractiveTransition *)paninterTransition {
if (!_paninterTransition) {
_paninterTransition = [[PanInteractiveTransition alloc] init];
}
return _paninterTransition;
}
-(DismissAnimation *)dismissAnimation {
if (!_dismissAnimation) {
_dismissAnimation = [[DismissAnimation alloc] init];
}
return _dismissAnimation;
}
-(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismissAnimation;
}
-(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return self.paninterTransition;
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"PresentSegue"]) {
// ...
[self.paninterTransition panToDismiss:presetVC];
}
}
完善
此时,我们运行程序,会发现以上代码尽管可以手势驱动了,但是点击按钮dismiss的功能无法使用了。这是因为如果只是返回self.paninterTransition,那么点击按钮dismiss的动画就会失效;如果只是返回nil,那么手势滑动的效果将会失效。综上所述,我们就得分情况考虑。
接下来我们就来完善一下。
给
PanInteractiveTransition
添加一个属性,表示是否处于切换过程中(用于判断使用的是点击按钮dismiss还是手势驱动来dismiss的)1
2// 是否处于切换过程中
@property (nonatomic, assign, getter=isInteracting) BOOL interacting;给
PanInteractiveTransition
添加一个属性,表示是否需要dismiss(用于当手势滑动到超过指定高度之后,就会dismiss,如果没有超过,就会还原)1
@property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
修改
PanInteractiveTransition
中的panGestureAction:
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29-(void)panGestureAction:(UIPanGestureRecognizer *)pan {
CGPoint transition = [pan translationInView:pan.view];
switch (pan.state) {
case UIGestureRecognizerStateBegan:{
self.interacting = YES;
[self.presentVC dismissViewControllerAnimated:YES completion:nil];
}
break;
case UIGestureRecognizerStateChanged:{ //
CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0);
self.shouldComplete = (percent > 0.5);
[self updateInteractiveTransition:percent];
}
break;
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
self.interacting = NO;
// 如果下移的距离小于300或者取消都当做取消
if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) { // 手势取消
[self cancelInteractiveTransition];
}else{
[self finishInteractiveTransition];
}
}
break;
default:
break;
}
}另外还有一点,就是需要修改
DismissAnimation
中的一处代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
UIView *contrainerView = [transitionContext containerView];
[contrainerView addSubview:toVC.view];
[contrainerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = finalRect;
} completion:^(BOOL finished) {
// 此处做了修改,由之前的[transitionContext completeTransition:YES];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
ok,到此为止,我们的一个自定义转场动画就算了完成了。
最后更新: 2023年12月02日 18:27:22
本文链接: http://example.com/post/a0e5cd1a.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!