在iOS7之前,View Controller的切換主要有4種:
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 報告交互取消,返回切換前的狀態
–(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的切換,因為我們缺少交互控制器.在自定義的容器中,系統是沒有提供返回交互控制器的協議給我們的,查了蠻多資料,也沒找到給出明確的方法,我認為,要跟實現轉場上下文一樣,仿照系統方法,自定義的去實現交互式的協議方法.我們就要去思考,系統是如何搭建起這個環境的.