之前看過王巍寫的ViewController切換來實現自定義轉場,之后又看了唐巧的一篇控制器轉場詳解的文章,篇幅很長,講的特別詳細。轉場有不少小細節,在這自己整理下,鞏固下知識。
為了表示方便,轉場前的控制器用FromVC表示,視圖用FromView表示;轉場后的控制器用ToVC表示,視圖用ToView表示。
轉場的基本步驟:
1.實現轉場代理
// 1.UIViewController 的 transitioningDelegate 遵循的協議。
UIViewControllerTransitioningDelegate
//2. UINavigationController 的 delegate 遵循的協議
UINavigationControllerDelegate。
//3. UITabBarController 的 delegate 遵循的協議。
UITabBarControllerDelegate
2.轉場的動畫控制器
//轉場動畫遵循的協議
UIViewControllerAnimatedTransitioning
該協議必須要實現的兩個方法:
- -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;轉場動畫時間
- -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;轉場動畫的具體實現(各種炫酷的動畫都是在這里實現的)
3.轉場上下文環境
//轉場上下文環境遵循的協議
UIViewControllerContextTransitioning
- 不需要開發者自己實現,該協議提供了轉場前后控制器的一寫信息。
- 其中幾個重要的方法:
- -(UIView *)containerView; 動畫發生的view容器。
- -(UIViewController *) viewControllerForKey:(NSString *)key; 通過key值,獲取轉場前后的控制器。
- key:
1 . UITransitionContextFromViewControllerKey 轉場前
2 . UITransitionContextToViewControllerKey 轉場后
- key:
- -(CGRect)initialFrameForViewController:(UIViewController *)vc; 獲取控制器初始frame。
- -(CGRect)finalFrameForViewController:(UIViewController *)vc;獲取控制器結束時的frame。
- -(void)completeTransition:(BOOL)didComplete; 轉場結束時必須要調用的方法。
4.手勢驅動視圖
系統提供了一個實現UIViewControllerInteractiveTransitioning協議的UIPercentDrivenInteractiveTransition類,所以我們只要繼承這個類,添加手勢并在手勢實現的方法中告知當前視圖的百分比,通過此邏輯來驅動視圖,在調用類中定義的一些方法就很容易實現視圖的交互。
其中幾個重要的方法:
- -(void)updateInteractiveTransition:(CGFloat)percentComplete;更新視圖的百分比。
- -(void)cancelInteractiveTransition;取消視圖的交互,返回交互前的狀態。
- -(void)finishInteractiveTransition;完成視圖交互,更新到交互后的狀態。
通過UINavigationController 實現轉場
通過UICollectionView簡單的實現神奇移動的效果。
設置好代理,并調用push方法
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PushedViewController *pushVC=[[PushedViewController alloc]init];
self.indexPath = indexPath;
self.navigationController.delegate = pushVC;
[self.navigationController pushViewController:pushVC animated:YES];
}
實現UINavigationControllerDelegate代理方法,此處返回實現UIViewControllerAnimatedTransitioning協議的動畫控制器對象,若返回nil,則調用系統默認方式。
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
// return nil;
return [PushPopTransfromAnimation transformWithType:(operation == UINavigationControllerOperationPush ? PushPopTransformPush :PushPopTransformPop)];
}
實現UIViewControllerAnimatedTransitioning協議的類:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.8;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
if (_type == PushPopTransformPush) {
[self pushAnimation:transitionContext];
}
else if (_type == PushPopTransformPop)
{
[self popAnimation:transitionContext];
}
}
#pragma mark -
-(void)pushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
//1.獲取控制器
CollectionViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
PushedViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//獲取FromVC中點擊的cell
MyCollectionViewCell *cell = (MyCollectionViewCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.indexPath];
UIView *containerView = [transitionContext containerView];
UIView *tempView = [cell.myImgView snapshotViewAfterScreenUpdates:NO];
tempView.frame =[cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
toVC.view.alpha = 0;
//2.添加ToView
[containerView addSubview:toVC.view];
[containerView addSubview:tempView];
toVC.imgView.hidden = YES;
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:0 animations:^{
tempView.frame = [toVC.imgView convertRect:toVC.imgView.bounds toView:containerView] ;
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
toVC.imgView.hidden = NO;
[tempView removeFromSuperview];
//3.轉場完成
[transitionContext completeTransition:YES];
}];
}
-(void)popAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
//1.
PushedViewController *fromVC = (PushedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
CollectionViewController *toVC = (CollectionViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//2.
UIView *containerView = [transitionContext containerView];
MyCollectionViewCell *cell = (MyCollectionViewCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.indexPath];
CGRect cellInitRect = [cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
UIView *temp = fromVC.imgView;
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
temp.frame = cellInitRect;
fromVC.view.alpha = 0;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES]];
}];
注意:1. 在轉場結束時,FromView會從視圖結構中主動移除 。2.push和pop動畫時都需要將ToView添加到ContainerView視圖中。
Model轉場
Model轉場與UINavigationController 和 UITabBarController轉場有些不同,需要注意。Model轉場屬性modalPresentationStyle可設置不同的模式,模式設置的不同,結果也不同,如:
UIModalPresentationFullScreen模式下:presentation后,FromView會被主動從視圖結構中移除,dismissal時,ToView可以自己手動添加到containerView中,也可以不用手動添加,系統會自己添加。
UIModalPresentationCustom模式下:presentation后,FromView不會被主動移除,這與FullScreen模式是不同的,dismissal時,切記也不用添加ToView視圖到containerView中,否則dismiss方法之后,presenting(將要顯示)的視圖不見了。
添加手勢驅動
Model方式實現轉場并添加手勢,在實現UIViewControllerTransitioningDelegate協議 和 UIViewControllerAnimatedTransitioning協議基礎上還要實現一個繼承UIPercentDrivenInteractiveTransition類的子類。
- UIViewControllerAnimatedTransitioning協議接口中有- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator方法,這個方法中就是用來返回UIPercentDrivenInteractiveTransition類的對象的。
- 該方法中還要判斷是否正在手勢驅動,手動驅動返回UIPercentDrivenInteractiveTransition類的對象,沒有手動驅動則返回nil,如果沒有判斷返回nil,則返回時沒有任何響應。
示例代碼:
PresentToViewController *presentToVC=[[PresentToViewController alloc]init];
presentToVC.transitioningDelegate=self; //轉場代理
presentToVC.delegate=self; //自定義代理,用于dismiss
[self.swipeVC handleDismissViewController:presentToVC];//用于手勢處理
//presentToVC.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController:presentToVC animated:YES completion:nil];
實現UIViewControllerTransitioningDelegate協議方法:
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return self.presentAnimation;
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return self.dismissAnimation;
}
//返回手勢對象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
{
return self.swipeVC.interacting ? self.swipeVC : nil;
}
實現UIViewControllerAnimatedTransitioning的類:
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
if (_type == YJPPresentAnimationPresent) {
[self presentVC:transitionContext];
}
else if (_type == YJPPresentAnimationDismiss)
{
[self dismissVC:transitionContext];
}
}
-(void)presentVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
PresentToViewController *toVC=(PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
toVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
toVC.view.center = containerView.center;
[containerView addSubview:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.frame = CGRectMake(0, 0, containerView.frame.size.width, containerView.frame.size.height);
toVC.view.center = containerView.center;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
-(void)dismissVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
PresentToViewController *fromVC = (PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
PresentFromViewController *toVC = (PresentFromViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//添加視圖
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
fromVC.view.center = containerView.center;
} completion:^(BOOL finished) {
//因添加了手勢驅動,這里要判斷是否取消
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
集成UIPercentDrivenInteractiveTransition類,實現子類:
- (void)handleDismissViewController:(PresentToViewController *)controller
{
self.dissmissVC = controller;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAction:)];
[controller.view addGestureRecognizer:pan];
}
-(CGFloat )completionSpeed
{
return 1 - self.persentCompleted;
}
-(void)panAction:(UIPanGestureRecognizer *)gesture
{
CGPoint point = [gesture translationInView:self.dissmissVC.view];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
self.interacting = YES;
[self.dissmissVC dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat persent = (point.y/500) <=1 ?(point.y/500):1;//百分比的程度
self.persentCompleted = persent;
[self updateInteractiveTransition:persent];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:
{
self.interacting = NO;
if (gesture.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
}else{
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
擴散效果
通過自定義了一個UINavigationController實現擴散效果的轉場。轉場的方法基本就那幾個方法,大同小異,不同的是動畫的實現方式。擴散本質其實就是設置了視圖的mask屬性,并添加了動畫,通過UIBezierPath、CAShapeLayer、CABasicAnimation就可以實現。
轉場動畫的核心代碼:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
if (_type == MaskTransitionPush) {
[self pushAnimaiton:transitionContext];
}
else if (_type == MaskTransitionPop)
{
[self popAnimaiton:transitionContext];
}
}
-(void)pushAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
//用于動畫結束時使用
self.transitionContext = transitionContext;
MaskedViewController *toVC = (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
//計算圓的半徑
UIButton *button = toVC.button;
CGFloat x =toVC.view.frame.size.width - button.center.x;
CGFloat y =toVC.view.frame.size.height - button.center.y;
CGFloat radius =sqrt(x*x+y*y);
UIBezierPath *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
UIBezierPath *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
//
CAShapeLayer *shaperLayer = [CAShapeLayer layer];
shaperLayer.path = finalPath.CGPath;
toVC.view.layer.mask = shaperLayer;
//添加動畫
CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
baseAnimation.fromValue = (id)initPath.CGPath;
baseAnimation.toValue = (id)finalPath.CGPath;
baseAnimation.duration = [self transitionDuration:transitionContext];
baseAnimation.delegate =self; //設置代理
[shaperLayer addAnimation:baseAnimation forKey:nil];
}
-(void)popAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
self.transitionContext = transitionContext;
MaskedViewController *fromVC= (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
SystemViewController *toVC = (SystemViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//添加視圖
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
//計算半徑
UIButton *button = fromVC.button;
CGFloat x =fromVC.view.frame.size.width - button.center.x;
CGFloat y =fromVC.view.frame.size.height - button.center.y;
CGFloat radius =sqrt(x*x+y*y);
UIBezierPath *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
UIBezierPath *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
//
CAShapeLayer *shaperLayer = [CAShapeLayer layer];
shaperLayer.path = initPath.CGPath;
fromVC.view.layer.mask = shaperLayer;
//
CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
baseAnimation.fromValue = (id)finalPath.CGPath;
baseAnimation.toValue =(id)initPath.CGPath;
baseAnimation.duration = [self transitionDuration:transitionContext];
baseAnimation.delegate =self;
[shaperLayer addAnimation:baseAnimation forKey:nil];
}
//CABasicAnimation動畫結束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self.transitionContext completeTransition:YES];
if (_type == MaskTransitionPush) {
[self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view.layer.mask = nil;
}
else if (_type == MaskTransitionPop)
{
[self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
}
}
參考文章:
https://onevcat.com/2013/10/vc-transition-in-ios7/
http://blog.devtang.com/2016/03/13/iOS-transition-guide/
http://www.lxweimin.com/p/45434f73019e