iOS自定義轉場動畫

通過這幾天的學習,我嘗試實現了四個效果,廢話不多說,先上效果圖:

DEMO ONE:一個神奇移動效果push動畫,支持手勢pop

神奇效果.gif

DEMO TWO:一個彈性的present動畫,支持手勢present和dismiss

彈性present.gif

DEMO THREE:一個翻頁push效果,支持手勢PUSH和POP

翻頁效果.gif

DEMO FOUR:一個小圓點擴散present效果,支持手勢dimiss

小圓點擴散.gif

準備

從iOS7開始,蘋果提供了自定義轉場的API,模態推送present和dismiss,導航控制器的push和pop,標簽控制器的控制器切換都可以自定義轉場了.
1.我們需要自定義一個遵循的<UIViewControllerAnimatedTransitioning>協議的動畫過渡管理對象,并實現兩個必須實現的方法:

 // 返回動畫事件
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; 
// 所有的過渡動畫事務都在這個方法里面完成 
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

2.我們還需要自定義一個繼承于UIPercentDrivenInteractiveTransition的手勢過渡管理對象,我把它成為百分比手勢過渡管理對象,因為動畫的過程是通過百分比控制的
3.成為相應的代理,實現相應的代理方法,返回我們前兩步自定義的對象就OK了 !
模態推送需要實現如下4個代理方法,iOS8新的那個方法我暫時還沒有發現它的用處,所以暫不討論.

//返回一個管理present動畫過渡的對象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
//返回一個管理dismiss動畫過渡的對象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
//返回一個管理present手勢過渡的對象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//返回一個管理dismiss動畫過渡的對象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

導航控制器實現如下2個代理方法

//返回轉場動畫過渡管理對象
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
//返回手勢過渡管理對象
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

標簽控制器也有相應的兩個方法

//返回轉場動畫過渡管理對象
 - (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
               interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
 //返回手勢過渡管理對象
 - (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
     animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                       toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

4.如果看著這些常常的代理方法名頭疼的話,沒關系,先在demo中用起來吧,慢慢就習慣了,其實哪種自定義轉場都只需要這3個步驟,如果不需要手勢控制,步驟2還可以取消,現在就讓我們動手來實現效果吧

Let's go!

DEMO ONE:神奇移動效果

1.我們首先創建2個控制器,為了方便我稱做present操作的為vc1、被present的為vc2,點擊一個控制器上的按鈕可以push出另一個控制器.
2.創建一個手勢過渡管理的類,我這里是PEInteractiveTransition,因為無論哪一種轉場,手勢控制的實質都是一樣的,我干脆就把這個手勢過渡管理的類封裝了一下,具體可以在.h文件里面查看,在接下來的三個轉場效果中我們都可以便捷的是使用它:
PEInteractiveTransition.h

typedef void(^GestureConfig)();

//手勢轉場類型
typedef NS_ENUM(NSUInteger,PEInteractiveTransitionType){
    PEInteractiveTransitionTypePresent = 0,
    PEInteractiveTransitionTypeDismiss,
    PEInteractiveTransitionTypePush,
    PEInteractiveTransitionTypePop
};

//手勢方向
typedef NS_ENUM(NSUInteger,PEInteractiveTransitionGestureDirection){
    PEInteractiveTransitionGestureDirectionLeft = 0,
    PEInteractiveTransitionGestureDirectionRight,
    PEInteractiveTransitionGestureDirectionUp,
    PEInteractiveTransitionGestureDirectionDown
};

@interface PEInteractiveTransition : UIPercentDrivenInteractiveTransition
/** 記錄是否開始手勢,判斷pop操作是手勢出發還是返回鍵出發 */
@property (nonatomic, assign)BOOL interation;
/** 觸發手勢present的時候config,在config中初始化并present需要彈出的控制器 */
@property (nonatomic, copy)GestureConfig presentConfig;
/** 觸發手勢push的時候config,在config中初始化并push需要彈出的控制器 */
@property (nonatomic, copy)GestureConfig pushConfig;

#pragma mark - ---初始化方法---
+ (instancetype)interactiveTransitionWithTransitionType:(PEInteractiveTransitionType)type GestureDirection:(PEInteractiveTransitionGestureDirection)direction;
- (instancetype)initWithTransitionType:(PEInteractiveTransitionType)type GestureDirection:(PEInteractiveTransitionGestureDirection)direction;

/** 給傳入控制器添加手勢*/
- (void)addPanGestureForViewController:(UIViewController *)viewController; 
@end

PEInteractiveTransition.m

#import "PEInteractiveTransition.h"

@interface PEInteractiveTransition()

/** 傳入的ViewController */
@property (nonatomic, weak)UIViewController *vc;
/** 手勢方向 */
@property (nonatomic, assign)PEInteractiveTransitionGestureDirection direction;
/** 手勢類型 */
@property (nonatomic, assign)PEInteractiveTransitionType type;

@end
@implementation PEInteractiveTransition

+ (instancetype)interactiveTransitionWithTransitionType:(PEInteractiveTransitionType)type GestureDirection:(PEInteractiveTransitionGestureDirection)direction
{
    return [[self alloc] initWithTransitionType:type GestureDirection:direction];
}

- (instancetype)initWithTransitionType:(PEInteractiveTransitionType)type GestureDirection:(PEInteractiveTransitionGestureDirection)direction{
    self = [super init];
    if (self) {
        _direction = direction;
        _type = type;
    }
    return self;
}

- (void)addPanGestureForViewController:(UIViewController *)viewController
{
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    self.vc = viewController;
    [viewController.view addGestureRecognizer:pan];
}
/**
 *  手勢過渡過程
 *
 *  @param pan 添加的手勢
 */
- (void)handleGesture:(UIPanGestureRecognizer *)pan
{
    //手勢百分比 初始化為0  向上和向右滑動 距離是負的 所以前面加負號 這樣負負得正
    CGFloat persent = 0;
    switch (_direction) {
        case PEInteractiveTransitionGestureDirectionUp:{
            CGFloat transitionY = -[pan translationInView:pan.view].y;
            persent = transitionY / pan.view.frame.size.width;
        }
            break;
        case PEInteractiveTransitionGestureDirectionRight:{
            CGFloat transitionR = [pan translationInView:pan.view].x;
            persent = transitionR / pan.view.frame.size.width;
        }
            break;
        case PEInteractiveTransitionGestureDirectionDown:{
            CGFloat transitionD = [pan translationInView:pan.view].y;
            persent = transitionD / pan.view.frame.size.width;
        }
            break;
        case PEInteractiveTransitionGestureDirectionLeft:{
            CGFloat transitionL = -[pan translationInView:pan.view].x;
            persent = transitionL / pan.view.frame.size.width;
        }
            break;
            
        default:
            break;
    }
    switch (pan.state) {
        case UIGestureRecognizerStateBegan:{
            //手勢開始的時候標記手勢狀態,并開始相應的事件
            self.interation = YES;
            [self startGesture];
        }
            break;
        case UIGestureRecognizerStateChanged:{
            //手勢過程中,通過updateInteractiveTransition設置pop過程進行的百分比
            [self updateInteractiveTransition:persent];
        }
            break;
        case UIGestureRecognizerStateEnded:{
            //手勢完成后結束標記并且判斷移動的距離是否過半,過則finishInteractiveTransition完成轉場操作,否則取消轉場操作
            self.interation = NO;
            if (persent > 0.5) {
                [self finishInteractiveTransition];
            }else {
                [self cancelInteractiveTransition];
            }
        }
            break;
            
        default:
            break;
    }
}

- (void)startGesture
{
    switch (_type) {
        case PEInteractiveTransitionTypePresent:{
            if (_presentConfig) {
                _presentConfig();
            }
        }
            break;
        case PEInteractiveTransitionTypeDismiss:{
            [_vc dismissViewControllerAnimated:YES completion:nil];
        }
            break;
        case PEInteractiveTransitionTypePush:{
            if (_pushConfig) {
                _pushConfig();
            }
        }
            break;
        case PEInteractiveTransitionTypePop:{
            [_vc.navigationController popViewControllerAnimated:YES];
        }
            break;
            
        default:
            break;
    }
}

@end

3.創建神奇效果的具體動畫代理
PENaviTransition.h

typedef NS_ENUM(NSUInteger, PENaviTransitionType){
    PENaviTransitionTypePush = 0,
    PENaviTransitionTypePop
};

@interface PENaviTransition : NSObject<UIViewControllerAnimatedTransitioning>


+ (instancetype)transitionWithType:(PENaviTransitionType)type;
- (instancetype)initWithTransitionType:(PENaviTransitionType)type;

@end

PENaviTransition.m主要實現一個動畫時長方法和一個push動畫和一個pop動畫

/**
 *  動畫時長
 */
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.75;
}
/**
 *  如何執行過渡動畫
 */
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    switch (_type) {
        case PENaviTransitionTypePush:
            [self doPushAnimation:transitionContext];
            break;
        case PENaviTransitionTypePop:
            [self doPopAnimation:transitionContext];
            break;
            
        default:
            break;
    }
}

/**
 *  執行push過渡動畫
 */
- (void)doPushAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    PEMagicMoveController *fromVC = (PEMagicMoveController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    PEMagicMovePushController *toVC = (PEMagicMovePushController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    //拿到當前點擊的cell的imageView
    PEMagicMoveCell *cell = (PEMagicMoveCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.clickIndex];
    UIView *containerView = [transitionContext containerView];
    UIView *tempView = [cell.imageV snapshotViewAfterScreenUpdates:NO];
    //將點擊的cell截圖作為臨時View的內容 并將坐標系轉化成push控制器種的坐標
    tempView.frame = [cell.imageV convertRect:cell.imageV.bounds toView:containerView];
    //設置動畫前的各個控件的狀態
    cell.imageV.hidden = YES;
    toVC.view.alpha = 0;
    toVC.imageView.hidden = YES;
    //tempView添加到containerView中保證在最前方,所以后添加
    [containerView addSubview:toVC.view];
    [containerView addSubview:tempView];
    //開始push動畫
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.55 initialSpringVelocity:1/0.55 options:0 animations:^{
        //臨時View(也就是截取cell大小)的frame變成和push出來view種的imageView大小一樣
        tempView.frame = [toVC.imageView convertRect:toVC.imageView.bounds toView:containerView];
        //push控制器由透明為0變為1
        toVC.view.alpha = 1;
    } completion:^(BOOL finished) {
        tempView.hidden = YES;
        toVC.imageView.hidden = NO;
        //如果動畫過渡取消了就標記不完成,否則標記完成,這里可以直接寫YES,如果有手勢過渡才需要判斷,必須標記,否則系統不會完成動畫,會出現無法交互等bug
        [transitionContext completeTransition:YES];
    }];
}
/**
 *  執行pop過渡動畫
 */
- (void)doPopAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    PEMagicMovePushController *fromVC = (PEMagicMovePushController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    PEMagicMoveController *toVC = (PEMagicMoveController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    PEMagicMoveCell *cell = (PEMagicMoveCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.clickIndex];
    UIView *containerView = [transitionContext containerView];
    //這里的lastObject就是push適合初始化的那個tempView
    UIView *tempView = containerView.subviews.lastObject;
    //設置動畫前狀態
    cell.imageV.hidden = YES;       //列表頁面的cell先隱藏
    fromVC.imageView.hidden = YES;  //當前頁面的imageView也進行隱藏
    tempView.hidden = NO;           //最上面的臨時View顯示
    [containerView insertSubview:toVC.view atIndex:0];
    //開始pop動畫
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:.55 initialSpringVelocity:1/0.55 options:0 animations:^{
        //將臨時View的坐標和大小變成新坐標系中列表cell的坐標和大小
        tempView.frame = [cell.imageV convertRect:cell.imageV.bounds toView:containerView];
        //同時詳情頁面隱藏
        fromVC.view.alpha = 0;
    } completion:^(BOOL finished) {
        //加入了手勢判斷
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if ([transitionContext transitionWasCancelled]) {
            //手勢取消了,原來隱藏的imageView要顯示出來
            tempView.hidden = YES;
            fromVC.imageView.hidden = NO;
        }else{
            //手勢成功
            cell.imageV.hidden = NO;        //列表點擊的cell要顯示
            [tempView removeFromSuperview]; //臨時的要去除 因為下一次會重新生成 不會產生冗余
        }
    }];
}

4.現在可以在具體進行跳轉的頁面vc1進行使用

#pragma mark <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    //記錄點擊的是列表的第幾個 這樣在返回動畫的時候能用上
    _clickIndex = indexPath;
    PEMagicMovePushController *vc = [[PEMagicMovePushController alloc] init];
    //設置導航控制器的代理為推出的控制器,可以達到自定義不同控制器推出效果的目的
    self.navigationController.delegate = vc;
    [self.navigationController pushViewController:vc animated:YES];
}

5.在push出的頁面vc2進行過渡動畫的監聽和實現

//返回手勢過渡管理對象
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    //分pop和push兩種情況分別返回動畫過渡代理相應不同的動畫操作
    return [PENaviTransition transitionWithType:operation == UINavigationControllerOperationPush ? PENaviTransitionTypePush : PENaviTransitionTypePop];
}

//返回轉場動畫過渡管理對象
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
    //如果是手動出發則返回我們的UIPercentDrivenInteractiveTransition對象
    return _interactiveTransition.interation ? _interactiveTransition : nil;
}

6.完成,就是下面的效果

神奇效果.gif

DEMO TWO: 彈性present動畫

1.創建兩個控制器,手勢管理類和DEMO ONE是同一個
2.自己具體的動畫過渡實現presentAnimationdismissAnimation兩個方法

/**
 *  presentAnimation
 */
- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContent
{
    UIViewController *toVC = [transitionContent viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromVC = [transitionContent viewControllerForKey:UITransitionContextFromViewControllerKey];
    //snapshotViewAfterScreenUpdates:可以對某個試圖截圖,我們采用對這個截圖做動畫代替直接對toVC做動畫,因為在手勢過渡中直接使用toVC動畫會和手勢有沖突,如果不需要實現手勢的話,就可以不是用截圖了
    UIView *tempView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
    tempView.frame = fromVC.view.frame;
    //因為對截圖做動畫,fromVC開始是隱藏的
    fromVC.view.hidden = YES;
    //containerView:如果要對視圖做轉場動畫,視圖就必須加入containerView中才能進行,可以理解為containerView管理所有做轉場動畫的視圖
    UIView *containerView = [transitionContent containerView];
    //將截圖視圖和toVC的view都加入containerView中 截圖視圖是fromVC所以先放 先放的在下面
    [containerView addSubview:tempView];
    [containerView addSubview:toVC.view];
    //設置toVC的frame,因為是present出來不是全屏,而且在底部,如果不設置默認是整個屏幕,這里的containerView的frame就是整個屏幕
    toVC.view.frame = CGRectMake(0, containerView.height, containerView.width, 500);
    //開始動畫,使用產生彈簧效果的方法
    [UIView animateWithDuration:[self transitionDuration:transitionContent] delay:0.0 usingSpringWithDamping:0.55 initialSpringVelocity:1.0 / 0.55 options:0 animations:^{
        //1.讓toVC向上移動 向上是負的
        toVC.view.transform = CGAffineTransformMakeTranslation(0, -500);
        //2.讓截圖視圖縮小即可
        tempView.transform = CGAffineTransformMakeScale(0.85, 0.85);
        
    } completion:^(BOOL finished) {
        //使用如下代碼標記整個轉場過程是否正常完成[transitionContext transitionWasCancelled]代表手勢是否取消了,如果取消了就傳NO表示轉場失敗,反之亦然,如果不是用手勢的話直接傳YES也是可以的,我們必須標記轉場的狀態,系統才知道處理轉場后的操作,否者認為你一直還在,會出現無法交互的情況
        [transitionContent completeTransition:![transitionContent transitionWasCancelled]];
        //轉場失敗后的處理
        if ([transitionContent transitionWasCancelled])
        {
            //失敗后復原動畫開始是的樣子
            //1.把fromVC顯示出來
            fromVC.view.hidden = NO;
            //2.移除截圖視圖,下次會重新生成
            [tempView removeFromSuperview];
        }
    }];
}

/**
 *  dismissAnimation
 */
- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContent
{
    UIViewController *fromVC = [transitionContent viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContent viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //參照present動畫的邏輯,present成功后,containerView的第一個子視圖就是截圖視圖,我們將其取出做動畫
    UIView *containerView = [transitionContent containerView];
    UIView *tempView = containerView.subviews[0];
    //開始動畫
    [UIView animateWithDuration:[self transitionDuration:transitionContent] delay:0.0 usingSpringWithDamping:0.55 initialSpringVelocity:1 / 0.55 options:0 animations:^{
        //present使用的是transform,只需要恢復就可以了
        fromVC.view.transform = CGAffineTransformIdentity;
        tempView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        if ([transitionContent transitionWasCancelled]) {
            //標記失敗
            [transitionContent completeTransition:NO];
        }else{
            //成功后,標記成功,讓toVC顯示出來,并移除截圖視圖
            [transitionContent completeTransition:YES];
            toVC.view.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];
}

3.vc1中進行調用

    PEElasticOneController *elasticVC = [PEElasticOneController new];
    elasticVC.delegate = self;
    [self presentViewController:elasticVC animated:YES completion:nil];

4.vc2中監聽

#pragma mark - ---UIViewControllerTransitioningDelegate---
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [PEElasticTransition transitionWithTransitionType:PEElasticTransitionTypePresent];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [PEElasticTransition transitionWithTransitionType:PEElasticTransitionTypeDismiss];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator
{
    PEInteractiveTransition *interactivePresent = [self.delegate interactiveTransitionForPresent];
    return interactivePresent.interation ? interactivePresent : nil;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
    return self.interactiveTransition.interation ? self.interactiveTransition : nil;
}

這樣就基本完成了

彈性present.gif

DEMO THREE:翻頁push效果

1.其它和上面的相似,直接來看核心的過渡動畫實現:

/**
 *  自定義push動畫
 */
- (void)doPushAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    //對當前view截圖 然后作為動畫對象
    UIView *tempV = [fromVC.view snapshotViewAfterScreenUpdates:NO];
    tempV.frame = fromVC.view.frame;
    UIView *containerV = [transitionContext containerView];
    //臨時的放最上面 做過渡
    [containerV addSubview:toVC.view];
    [containerV addSubview:tempV];
    fromVC.view.hidden = YES;
    //設置anchorPoint 選擇的支點
    [tempV setAnchorPoint:CGPointMake(0, 0.5)];
    CATransform3D transform3d = CATransform3DIdentity;
    //m34(透視效果,要操作的這個對象要有旋轉的角度,否則沒有效果。正直/負值都有意義);
    transform3d.m34 = -0.002;
    containerV.layer.sublayerTransform = transform3d;
    //增加陰影
    CAGradientLayer *fromGradient = [CAGradientLayer layer];
    fromGradient.frame = fromVC.view.bounds;
    fromGradient.colors = @[(id)[UIColor blackColor].CGColor,(id)[UIColor blackColor].CGColor];
    fromGradient.startPoint = CGPointMake(0.0, 0.5);
    fromGradient.endPoint = CGPointMake(0.8, 0.5);
    UIView *fromShadow = [[UIView alloc] initWithFrame:fromVC.view.bounds];
    fromShadow.backgroundColor = [UIColor clearColor];
    [fromShadow.layer insertSublayer:fromGradient atIndex:1];
    fromShadow.alpha = 0.0;
    [tempV addSubview:fromShadow];
    CAGradientLayer *toGradient = [CAGradientLayer layer];
    toGradient.frame = fromVC.view.bounds;
    toGradient.colors = @[(id)[UIColor blackColor].CGColor,(id)[UIColor blackColor].CGColor];
    toGradient.startPoint = CGPointMake(0.0, 0.5);
    toGradient.endPoint = CGPointMake(0.8, 0.5);
    UIView *toShadow = [[UIView alloc] initWithFrame:fromVC.view.bounds];
    toShadow.backgroundColor = [UIColor clearColor];
    [toShadow.layer insertSublayer:toGradient atIndex:1];
    toShadow.alpha = 1.0;
    [toVC.view addSubview:toShadow];
    //開始動畫
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        //沿Y軸旋轉 逆時針180°
        tempV.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1, 0);
        //當前的由亮變暗 push的VC由暗變亮
        fromShadow.alpha = 1.0;
        toShadow.alpha = 0.0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        //動畫取消的操作
        if ([transitionContext transitionWasCancelled]){
            [tempV removeFromSuperview];
            fromVC.view.hidden = NO;
        }
    }];
}

/**
 *  自定義pop動畫
 */
- (void)doPopAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerV = [transitionContext containerView];
    UIView *tempV = containerV.subviews.lastObject;
    [containerV addSubview:toVC.view];
    //開始動畫
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        //臨時圖層恢復原狀
        tempV.layer.transform = CATransform3DIdentity;
        fromVC.view.subviews.lastObject.alpha = 1.0;
        tempV.subviews.lastObject.alpha = 0.0;
    } completion:^(BOOL finished) {
        if ([transitionContext transitionWasCancelled]){
            [transitionContext completeTransition:NO];
        }else{
            [transitionContext completeTransition:YES];
            [tempV removeFromSuperview];
            toVC.view.hidden = NO;
        }
    }];
}

2.手勢加上的效果如下:

![Uploading 翻頁效果_161246.gif . . .]

DEMO FOUR:圓點擴散present效果

1.其它和上面相似,來看核心過渡的動畫實現

- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UINavigationController *fromVC = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *containerV = [transitionContext containerView];
    PECircleSpreadController *temp = fromVC.viewControllers.lastObject;
    [containerV addSubview:toVC.view];
    //畫兩個圓路徑
    UIBezierPath *startCircle = [UIBezierPath bezierPathWithOvalInRect:temp.buttonFrame];
    //始終用最大的x值和y值的平方根 來作為圓的半徑 這樣保證了圓點中心到最遠的邊的距離作為半徑
    CGFloat x = MAX(temp.buttonFrame.origin.x, containerV.frame.size.width - temp.buttonFrame.origin.x);
    CGFloat y = MAX(temp.buttonFrame.origin.y, containerV.frame.size.height - temp.buttonFrame.origin.y);
    CGFloat radius = sqrtf(pow(x, 2) + pow(y, 2));
    //圓心 半徑 開始角度 結束角度M_PI*2是360° 是否順時針
    UIBezierPath *endCircle = [UIBezierPath bezierPathWithArcCenter:containerV.center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
    
    //創建CAShapLayer進行遮蓋
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = endCircle.CGPath;
    //將maskLayer作為toVC的遮蓋
    toVC.view.layer.mask = maskLayer;
    //創建路徑動畫
    CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    //動畫是添加到layer上的,所以必須為CGPath,再將CGPath橋接為OC對象
    maskLayerAnimation.fromValue = (__bridge id)(startCircle.CGPath);
    maskLayerAnimation.toValue = (__bridge id)(endCircle.CGPath);
    maskLayerAnimation.duration = [self transitionDuration:transitionContext];
    maskLayerAnimation.delegate = self;
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];   //動畫速度: 先慢 后慢 中間快
    [maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];    
}

- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    PECircleSpreadPresentedController *fromVC = (PECircleSpreadPresentedController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UINavigationController *toVC = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    PECircleSpreadController *temp = toVC.viewControllers.lastObject;
    UIView *containerV = [transitionContext containerView];
    //畫兩個圓的路徑
    CGFloat radius = sqrtf(pow(containerV.frame.size.height, 2) + pow(containerV.frame.size.width, 2))/2.0;
    UIBezierPath *startCircle = [UIBezierPath bezierPathWithArcCenter:containerV.center radius:radius startAngle:0 endAngle:M_PI*2 clockwise:YES];
    temp.buttonFrame = fromVC.presentButtonFrame;
    UIBezierPath *endCircle = [UIBezierPath bezierPathWithOvalInRect:temp.buttonFrame];
    //創建CAShapeLayer進行覆蓋
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.fillColor = [UIColor greenColor].CGColor;
    maskLayer.path = endCircle.CGPath;
    fromVC.view.layer.mask = maskLayer;
    //創建路徑動畫
    CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    maskLayerAnimation.delegate = self;
    maskLayerAnimation.fromValue = (__bridge id)(startCircle.CGPath);
    maskLayerAnimation.toValue = (__bridge id)(endCircle.CGPath);
    maskLayerAnimation.duration = [self transitionDuration:transitionContext];
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
    
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{ 
    switch (_type) {
        case PECircleSpreadTransitionTypePresent:{
            id<UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:@"transitionContext"];
            [transitionContext completeTransition:YES];
        }
            break;
        case PECircleSpreadTransitionTypeDismiss:{
            id<UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:@"transitionContext"];
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            if ([transitionContext transitionWasCancelled]) {
                //去掉遮擋層
                [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
            }
        }
            break;
            
        default:
            break;
    }
}

注意:最后animationDidStop的代理方法中處理到動畫的完成邏輯.
2.最后的效果如下:

小圓點擴散.gif

總結
1、關于:self.modalPresentationStyle = UIModalPresentationCustom;我查看了視圖層級后發現,如果使用了Custom,在present動畫完成的時候,presentingView也就是demo one中的vc1的view會從containerView中移除,只是移除,并未銷毀,此時還被持有著(dismiss后還得回來呢!),如果設置custom,那么present完成后,它一直都在containerView中,只是在最后面,所以需不需要設置custom可以看動畫完成后的情況,是否還需要看見presentingViewController,但是記住如果沒有設置custom,在disMiss的動畫邏輯中,要把它加回containerView中,不然就不在咯~!
2、感覺寫了好多東西,其實只要弄懂了轉場的邏輯,其實就只需要寫動畫的邏輯就行了,其他東西都是固定的,而且蘋果提供的這種控制轉場的方式可充分解耦,除了寫的手勢過渡管理可以拿到任何地方使用,所有的動畫過渡管理者都可以很輕松的復用到其他轉場中,都不用分是何種轉場,demo沒有寫標簽控制器的轉場,實現方法也是完全類似的,大家可以嘗試一下,四個demo的github地址:自定義轉場動畫demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • 更新,更簡單的自定義轉場集成! 幾句代碼快速集成自定義轉場效果+ 全手勢驅動 寫在前面 這兩天閑下來好好的研究了一...
    wazrx閱讀 73,906評論 84 584
  • 路漫漫其修遠兮,吾將上下而求索 前記 想研究自定義轉場動畫很久了,時間就像海綿,擠一擠還是有的,花了差不多有10天...
    半笑半醉間閱讀 7,479評論 10 51
  • iOS7.0后蘋果提供了自定義轉場動畫的API,利用這些API我們可以改變 push和pop(navigation...
    薛定喵的鵝閱讀 17,837評論 1 37
  • iOS 7 以協議的方式開放了自定義轉場的 API,協議的好處是不再拘泥于具體的某個類,只要是遵守該協議的對象都能...
    iceMaple閱讀 1,980評論 0 13
  • 那是我第一次、也是至今唯一一次,見到父親的眼淚。 那年,由于一系列原因,我要從山東到東北去上大學,開學前幾天,父母...
    心有余響閱讀 185評論 0 3