在iOS7之前,View Controller的切换主要有4种:
- Push/Pop,NavigationViewController
- Present and dismis Modal
- UITabBarController
- addChildViewController(一般用于自定义的继承于 UIViewController 的容器子类)
iOS5,调用- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(5_0);
(1)前面3种方法这里就不多说了,很常见的系统方法.至于第四种,我在前面文章-剖析网易标签栏的效果中已经做了阐述,但是它提供的容器转场动画只可以实现一些简单的UIView动画,但是难以重用,耦合高.
(2)关键的API:
A.动画控制器 (Animation Controllers) 遵从 UIViewControllerAnimatedTransitioning 协议,并且负责实际执行动画。
B.交互控制器 (Interaction Controllers) 通过遵从 UIViewControllerInteractiveTransitioning 协议来控制可交互式的转场。
C.转场代理 (Transitioning Delegates) 根据不同的转场类型方便的提供需要的动画控制器和交互控制器。
其中UINavigationControllerDelegate delegate 中新增了2个方法给NavigationController
UIViewControllerTransitioningDelegate 新增transitioningDelegate 给Modal的Present和Dismis
UITabBarControllerDelegate delegate
- (id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
- (id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
animationControllerForTransitionFromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
D.转场上下文 (Transitioning Contexts) 定义了转场时需要的元数据,比如在转场过程中所参与的视图控制器和视图的相关属性。 转场上下文对象遵从 UIViewControllerContextTransitioning 协议,并且这是由系统负责生成和提供的。
E.转场协调器(Transition Coordinators) 可以在运行转场动画时,并行的运行其他动画。 转场协调器遵从 UIViewControllerTransitionCoordinator 协议。
(3)新的API主要提供了2种VC切换的方式:
A.非交互式切换,即定义一种从一个VC到另一个VC的动画效果,切换的时候自动播放,
B.交互式切换,这种方式同样需要定义动画效果,只是这个动画效果会根据跟随交互式手势来切换VC并同时播放动画效果。iOS7提供了一个默认的基于百分比的动画实现 UIPercentDrivenInteractiveTransition,而且根据WWDC的说明,最简单的实现交互式动画的方法就是通过继承 UIPercentDrivenInteractiveTransition。
苹果给我们开发者提供的是都是协议接口,所以我们能够很好的单独提出来写成一个个类,在里面实现我们各种自定义效果.
(4)来看看实现UIViewControllerAnimatedTransitioning的自定义动画类
/**
* 自定义的动画类
* 实现协议------>@protocol UIViewControllerAnimatedTransitioning
* 这个接口负责切换的具体内容,也即“切换中应该发生什么”
*/
@interface MTHCustomAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@end
@implementation MTHCustomAnimator
// 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 1.0;
}
// 完成容器转场动画的主要方法,我们对于切换时的UIView的设置和动画都在这个方法中完成
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 可以看做为destination ViewController
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 可以看做为source ViewController
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// 添加toView到容器上
// 如果是XCode6 就可以用这段
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
{
// iOS8 SDK 新API
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
//UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
[[transitionContext containerView] addSubview:toView];
}else{
// 添加toView到容器上
[[transitionContext containerView] addSubview:toViewController.view];
}
// 如果是XCode5 就是用这段
[[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]];
}];
}
PS:从协议中两个方法可以看出,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从UIViewControllerContextTransitioning 协议的对象。通常情况下,当我们使用系统的类时,系统框架为我们提供的转场代理(Transitioning Delegates),为我们创建了转场上下文对象,并把它传递给动画控制器。
// MainViewController
@interface MTHMainViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate>
@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; (貌似有BUG)换成modalTransitionStyle = 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<UIViewControllerAnimatedTransitioning>) 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 <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
/**
* 在非交互式动画效果中,该方法返回 nil
* 交互式转场,自我理解意思是,用户能通过自己的动作来(常见:手势)控制,不同于系统缺省给定的push或者pop(非交互式)
*/
return _interactionController;
}
#pragma mark - Transitioning Delegate (Modal)
// 前2个用于动画
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
self.minToMaxAnimator.animationType = AnimationTypePresent;
return _minToMaxAnimator;
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
self.minToMaxAnimator.animationType = AnimationTypeDismiss;
return _minToMaxAnimator;
}
// 后2个用于交互
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator
{
return _interactionController;
}
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
{
return nil;
}
以上实现的是非交互的转场,指的是完全按照系统指定的切换机制,用户无法中途取消或者控制进度切换.那怎么来实现交互转场呢:
UIPercentDrivenInteractiveTransition实现了UIViewControllerInteractiveTransitioning接口的类,,可以用一个百分比来控制交互式切换的过程。我们在手势识别中只需要告诉这个类的实例当前的状态百分比如何,系统便根据这个百分比和我们之前设定的迁移方式为我们计算当前应该的UI渲染,十分方便。具体的几个重要方法:
-(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通过手势识别的长度之类的来计算一个值,然后进行更新。之后的例子里会看到详细的用法
-(void)cancelInteractiveTransition 报告交互取消,返回切换前的状态
?C(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;
}
}
最后,给大家分享一个动画特效:类似于飞兔云传的发送ViewController切换
@implementation PDTransitionAnimator
#define Switch_Time 1.2
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return Switch_Time;
}
#define Button_Width 50.f
#define Button_Space 10.f
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView * toView = toViewController.view;
UIView * fromView = fromViewController.view;
if (self.animationType == AnimationTypeDismiss) {
// 这个方法能够高效的将当前显示的view截取成一个新的view.你可以用这个截取的view用来显示.例如,也许你只想用一张截图来做动画,毕竟用原始的view做动画代价太高.因为是截取了已经存在的内容,这个方法只能反应出这个被截取的view当前的状态信息,而不能反应这个被截取的view以后要显示的信息.然而,不管怎么样,调用这个方法都会比将view做成截图来加载效率更高.
UIView * snap = [toView snapshotViewAfterScreenUpdates:YES];
[transitionContext.containerView addSubview:snap];
[snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space, [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space, Button_Width, Button_Width)];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
[snap setFrame:[UIScreen mainScreen].bounds];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
[[transitionContext containerView] addSubview:toView];
snap.alpha = 0;
} completion:^(BOOL finished) {
[snap removeFromSuperview];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}];
} else {
UIView * snap2 = [toView snapshotViewAfterScreenUpdates:YES];
[transitionContext.containerView addSubview:snap2];
UIView * snap = [fromView snapshotViewAfterScreenUpdates:YES];
[transitionContext.containerView addSubview:snap];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
[snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space+ (Button_Width/2), [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space + (Button_Width/2), 0, 0)];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
//snap.alpha = 0;
} completion:^(BOOL finished) {
[snap removeFromSuperview];
[snap2 removeFromSuperview];
[[transitionContext containerView] addSubview:toView];
// 切记不要忘记了噢
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}];
}
}
其中,snapshotViewAfterScreenUpdates 方法的解释,我也不是很懂,反正初级来说会用就行,还可以参照下面的解析:
在iOS7 以前, 获取一个UIView的快照有以下步骤: 首先创建一个UIGraphics的图像上下文,然后将视图的layer渲染到该上下文中,从而取得一个图像,最后关闭图像上下文,并将图像显示在UIImageView中。现在我们只需要一行代码就可以完成上述步骤了:
[view snapshotViewAfterScreenUpdates:NO];
这个方法制作了一个UIView的副本,如果我们希望视图在执行动画之前保存现在的外观,以备之后使用(动画中视图可能会被子视图遮盖或者发生其他一些变化),该方法就特别方便。
afterUpdates参数表示是否在所有效果应用在视图上了以后再获取快照。例如,如果该参数为NO,则立马获取该视图现在状态的快照,反之,以下代码只能得到一个空白快照:
[view snapshotViewAfterScreenUpdates:YES];
[view setAlpha:0.0];
由于我们设置afterUpdates参数为YES,而视图的透明度值被设置成了0,所以方法将在该设置应用在视图上了之后才进行快照,于是乎屏幕空空如也。另外就是……你可以对快照再进行快照……继续快照……
继续前面的内容,这一章,主要介绍自定义ViewController容器上视图VC的切换.先来看看系统给我们提供的容器控制器 UINavigationController和UITabBarController 都有一个NSArray类型的属性viewControllers,很明显,存储的就是需要切换的视图VC.同理,我们定义一个ContainerViewController,是UIViewController的直接子类,用来作为容器依托,额,其他属性定义详见代码吧,这里不多说了.(PS:原先我进行多个自定义视图VC切换的方法,是放置一个UIScrollView,然后把所有childViewController的View的frame的X坐标,依此按320递增,大家可以自行想象下,这样不好的地方,我感觉就是所有的VC一经加载就全部实体化了,而且不会因为被切换变成暂不显示而释放掉)
偷懒下,用storyboard创建的5个childVC
// ContainerViewController
@interface FTContainerViewController ()
@property (strong, nonatomic) FTPhotoSenderViewController *photoSenderViewController;
@property (strong, nonatomic) FTVideoSenderViewController *videoSenderViewController;
@property (strong, nonatomic) FTFileSenderViewController *fileSenderViewController;
@property (strong, nonatomic) FTContactSenderViewController *contactSenderViewController;
@property (strong, nonatomic) FTClipboardSenderViewController *clipboardSenderViewController;
@property (strong, nonatomic) UIViewController *selectedViewController; // 当前选择的VC
@property (strong, nonatomic) NSArray *viewControllers; // childVC数组
@property (assign, nonatomic) NSInteger currentControllerIndex; // 当前选择的VC的数组下标号
@end
@implementation FTContainerViewController
#pragma mark - ViewLifecycle Methods
- (void)viewDidLoad
{
[super viewDidLoad];
// childVC
self.photoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTPhotoSenderViewController"];
self.videoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTVideoSenderViewController"];
self.fileSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTFileSenderViewController"];
self.contactSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTContactSenderViewController"];
self.clipboardSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTClipboardSenderViewController"];
// 存储childVC的数组
self.viewControllers = @[_photoSenderViewController,_videoSenderViewController,_fileSenderViewController,_contactSenderViewController,_clipboardSenderViewController];
// 缺省为下标为0的VC
self.selectedViewController = self.selectedViewController ?: self.viewControllers[0];
self.currentControllerIndex = 0;
}
依旧,实现UIViewControllerAnimatedTransitioning协议的Animator类,不过里面换个动画效果,利用iOS7新增的弹簧动画效果:
#import "FTMthTransitionAnimator.h"
@implementation FTMthTransitionAnimator
static CGFloat const kChildViewPadding = 16;
static CGFloat const kDamping = 0.5; // damping参数代表弹性阻尼,随着阻尼值越来越接近0.0,动画的弹性效果会越来越明显,而如果设置阻尼值为1.0,则视图动画不会有弹性效果
static CGFloat const kInitialSpringVelocity = 0.5; // 初始化弹簧速率
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 1.0;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
/**
* - viewControllerForKey:我们可以通过他访问过渡的两个 ViewController。
* - containerView:两个 ViewController 的 containerView。
* - initialFrameForViewController 和 finalFrameForViewController 是过渡开始和结束时每个 ViewController 的 frame。
*/
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
BOOL goingRight = ([transitionContext initialFrameForViewController:toViewController].origin.x < [transitionContext finalFrameForViewController:toViewController].origin.x);
CGFloat transDistance = [transitionContext containerView].bounds.size.width + kChildViewPadding;
CGAffineTransform transform = CGAffineTransformMakeTranslation(goingRight ? transDistance : -transDistance, 0);
// CGAffineTransformInvert 反转
toViewController.view.transform = CGAffineTransformInvert(transform);
// toViewController.view.transform = CGAffineTransformTranslate(toViewController.view.transform, (goingRight ? transDistance : -transDistance), 0);
/**
* ----------弹簧动画.....-------
* 使用由弹簧的运动描述的时序曲线` animations` 。当` dampingRatio`为1时,动画将平稳减速到其最终的模型值不会振荡。阻尼比小于1来完全停止前将振荡越来越多。可以使用弹簧的初始速度,以指定的速度在模拟弹簧的端部的物体被移动它附着之前。这是一个单元坐标系,其中1是指行驶总距离的动画在第二。所以,如果你改变一个物体的位置由200PT在这个动画,以及你想要的动画表现得好像物体在动,在100PT /秒的动画开始之前,你会通过0.5 。你通常会想通过0的速度。
*/
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:kDamping initialSpringVelocity:kInitialSpringVelocity options:0x00 animations:^{
fromViewController.view.transform = transform;
fromViewController.view.alpha = 0;
// CGAffineTransformIdentity 重置,初始化
toViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.alpha = 1;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
// 声明过渡结束-->记住,一定别忘了在过渡结束时调用 completeTransition: 这个方法。
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end
接下来的代码,就是实现自定义容器切换的关键了.通常情况下,当我们使用系统内建的类时,系统框架为我们创建了转场上下文对象,并把它传递给动画控制器。但是在我们这种情况下,我们需要自定义转场动画,所以我们需要承担系统框架的责任,自己去创建这个转场上下文对象。
@interface FTMthTransitionContext : NSObject <UIViewControllerContextTransitioning>
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight;
@property (nonatomic, copy) void (^completionBlock)(BOOL didComplete);
@property (nonatomic, assign, getter=isAnimated) BOOL animated;
@property (nonatomic, assign, getter=isInteractive) BOOL interactive; // 是否交互式
@property (nonatomic, strong) NSDictionary *privateViewControllers;
@property (nonatomic, assign) CGRect privateDisappearingFromRect;
@property (nonatomic, assign) CGRect privateAppearingFromRect;
@property (nonatomic, assign) CGRect privateDisappearingToRect;
@property (nonatomic, assign) CGRect privateAppearingToRect;
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, assign) UIModalPresentationStyle presentationStyle;
@end
@implementation FTMthTransitionContext
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight {
if ((self = [super init])) {
self.presentationStyle = UIModalPresentationCustom;
self.containerView = fromViewController.view.superview;
self.privateViewControllers = @{
UITransitionContextFromViewControllerKey:fromViewController,
UITransitionContextToViewControllerKey:toViewController,
};
// Set the view frame properties which make sense in our specialized ContainerViewController context. Views appear from and disappear to the sides, corresponding to where the icon buttons are positioned. So tapping a button to the right of the currently selected, makes the view disappear to the left and the new view appear from the right. The animator object can choose to use this to determine whether the transition should be going left to right, or right to left, for example.
CGFloat travelDistance = (goingRight ? -self.containerView.bounds.size.width : self.containerView.bounds.size.width);
self.privateDisappearingFromRect = self.privateAppearingToRect = self.containerView.bounds;
self.privateDisappearingToRect = CGRectOffset (self.containerView.bounds, travelDistance, 0);
self.privateAppearingFromRect = CGRectOffset (self.containerView.bounds, -travelDistance, 0);
}
return self;
}
- (CGRect)initialFrameForViewController:(UIViewController *)viewController {
if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) {
return self.privateDisappearingFromRect;
} else {
return self.privateAppearingFromRect;
}
}
- (CGRect)finalFrameForViewController:(UIViewController *)viewController {
if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) {
return self.privateDisappearingToRect;
} else {
return self.privateAppearingToRect;
}
}
- (UIViewController *)viewControllerForKey:(NSString *)key {
return self.privateViewControllers[key];
}
- (void)completeTransition:(BOOL)didComplete {
if (self.completionBlock) {
self.completionBlock (didComplete);
}
}
// 非交互式,直接返回NO,因为不允许交互当然也就无法操作进度取消
- (BOOL)transitionWasCancelled { return NO; }
// 非交互式,直接不进行操作,只有进行交互,下面3个协议方法才有意义,可参照系统给我们定义好的交互控制器
// @interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
- (void)updateInteractiveTransition:(CGFloat)percentComplete {}
- (void)finishInteractiveTransition {}
- (void)cancelInteractiveTransition {}
@end
OK,准备工作都做好了,为了仿照UIScrollView的滑动切换,但又因为现在展示的是非交互式,我们定义一个swip(轻扫)手势.
UISwipeGestureRecognizer *leftGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)];
[leftGesture setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.view addGestureRecognizer:leftGesture];
UISwipeGestureRecognizer *rightGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)];
[rightGesture setDirection:UISwipeGestureRecognizerDirectionRight];
[self.view addGestureRecognizer:rightGesture];
[objc] view plaincopy
// 响应手势的方法
- (void)swapViewControllers:(UISwipeGestureRecognizer *)swipeGestureRecognizer
{
if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
if (_currentControllerIndex < 4) {
_currentControllerIndex++;
}
NSLog(@"_currentControllerIndex = %ld",(long)_currentControllerIndex);
UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex];
NSLog(@"
%s__%d__|%@",__FUNCTION__,__LINE__,@"右边");
self.selectedViewController = selectedViewController;
} else if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionRight){
NSLog(@"
%s__%d__|%@",__FUNCTION__,__LINE__,@"左边");
if (_currentControllerIndex > 0) {
_currentControllerIndex--;
}
UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex];
self.selectedViewController = selectedViewController;
}
}
// 重写selectedViewController的setter
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
NSParameterAssert (selectedViewController);
[self _transitionToChildViewController:selectedViewController];
_selectedViewController = selectedViewController;
}
// 切换操作(自定义的,联想我在前面文章网易标签栏切换中,系统给的transitionFromViewController,是一个道理)
- (void)_transitionToChildViewController:(UIViewController *)toViewController
{
UIViewController *fromViewController = self.childViewControllers.count > 0 ? self.childViewControllers[0] : nil;
if (toViewController == fromViewController) {
return;
}
UIView *toView = toViewController.view;
[toView setTranslatesAutoresizingMaskIntoConstraints:YES];
toView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
toView.frame = self.view.bounds;
// 自定义容器的切换,addChildViewController是关键,它保证了你想要显示的VC能够加载到容器中
// 而所谓的动画和上下文,只是为了转场的动画效果
// 因此,就算用UIScrollView切换,也不能缺少addChildViewController,切记!切记!
[fromViewController willMoveToParentViewController:nil];
[self addChildViewController:toViewController];
if (!fromViewController) {
[self.view addSubview:toViewController.view];
[toViewController didMoveToParentViewController:self];
return;
}
// Animator
FTMthTransitionAnimator *transitionAnimator = [[FTMthTransitionAnimator alloc] init];
NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController];
NSUInteger toIndex = [self.viewControllers indexOfObject:toViewController];
// Context
FTMthTransitionContext *transitionContext = [[FTMthTransitionContext alloc] initWithFromViewController:fromViewController toViewController:toViewController goingRight:(toIndex > fromIndex)];
transitionContext.animated = YES;
transitionContext.interactive = NO;
transitionContext.completionBlock = ^(BOOL didComplete) {
// 因为是非交互式,所以fromVC可以直接直接remove出its parent's children controllers array
[fromViewController.view removeFromSuperview];
[fromViewController removeFromParentViewController];
[toViewController didMoveToParentViewController:self];
if ([transitionAnimator respondsToSelector:@selector (animationEnded:)]) {
[transitionAnimator animationEnded:didComplete];
}
};
// 转场动画需要以转场上下文为依托,因为我们是自定义的Context,所以要手动设置
[transitionAnimator animateTransition:transitionContext];
}
大功告成.
上面展示的就是一个基本的自定义容器的非交互式的转场切换.那交互式的呢?从上面我定义手势定义为swip而不是pan也可以看出,非交互转场,并不能完全实现UIScrollView那种分页式的效果,按照类似百分比的形式来进行fromVC和toVC的切换,因为我们缺少交互控制器.在自定义的容器中,系统是没有提供返回交互控制器的协议给我们的,查了蛮多资料,也没找到给出明确的方法,我认为,要跟实现转场上下文一样,仿照系统方法,自定义的去实现交互式的协议方法.我们就要去思考,系统是如何搭建起这个环境的.