天天看點

一個打開/關閉書籍動畫(轉場動畫)

​​​​​​        最近一直在寫小說閱讀器相關内容,看了當下熱門的幾款小說APP,發現打開/關閉書籍的時候都加了一個自定義轉場動畫,但是我們目前的項目是沒有的。作為一個對界面效果有獨特追求的開發者來說,絕對不能忍,加班也要加上去!

        大概看了一下動畫效果,發現難度其實不大,簡單歸納為下面幾步:

  1. 設定兩個視圖,一個為書籍封面截圖,另一個為打開書籍内容截圖,儲存書籍封面視圖;
  2. 打開書籍時:書籍封面沿書脊位置(左邊Y軸)逆時針旋轉90度并放大到書籍視圖正常(滿屏)大小。書籍内容視圖初始位置和書籍封面重合,和書籍封面同時做放大動畫;
  3. 關閉書籍時:第一步儲存的書籍封面視圖初始位置與書籍内容重合,并提前沿書脊位置(左邊Y軸)逆時針旋轉90度,做順時針旋轉和縮小動畫,直到恢複到書籍封面正常位置。 書籍内容視圖直接做縮小動畫,縮小到書籍封面正常大小;
  4. 動畫完成時移除兩個截圖即可。

我做好的效果如下

一個打開/關閉書籍動畫(轉場動畫)

主要代碼:

1、建立UIViewController的分類UIViewController+Transition,用于統一管理UINavigationControllerDelegate,在分類中新增屬性bookCoverView,用于儲存書籍封面視圖,當然你也可以直接在控制器中寫代碼,隻是新增了重複代碼而已。

UIViewController+Transition.h

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (Transition)

/// 書籍封面視圖
@property (nonatomic, strong, nullable) UIView *bookCoverView;

@end

NS_ASSUME_NONNULL_END
           

UIViewController+Transition.m

@interface UIViewController () <UINavigationControllerDelegate>

@end

@implementation UIViewController (Transition)

#pragma mark - Navigation controller delegate
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
    if (operation == UINavigationControllerOperationPush && [fromVC isMemberOfClass:[XPYBookStackViewController class]] && [toVC isMemberOfClass:[XPYReaderManagerController class]] && fromVC.bookCoverView) {
        // 當XPYBookStackViewController控制器push到XPYReaderManagerController控制器時需要執行自定義push動畫
        return [XPYOpenBookAnimation openBookAnimation];
    }
    if (operation == UINavigationControllerOperationPop && [fromVC isMemberOfClass:[XPYReaderManagerController class]] && [toVC isMemberOfClass:[XPYBookStackViewController class]] && fromVC.bookCoverView) {
        // 當XPYReaderManagerController控制器pop到XPYBookStackViewController控制器時需要執行自定義pop動畫
        return [XPYCloseBookAnimation closeBookAnimation];
    }
    return nil;
}

/// 使用者互動相關(這裡不需要)
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
    return nil;
}

/// 關聯屬性
#pragma mark - Getters
- (UIView *)bookCoverView {
    return objc_getAssociatedObject(self, @selector(bookCoverView));
}

#pragma mark - Setters
- (void)setBookCoverView:(UIView *)bookCoverView {
    objc_setAssociatedObject(self, @selector(bookCoverView), bookCoverView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
           

2、建立push動畫類(XPYOpenBookAnimation)和pop動畫類(XPYCloseBookAnimation),可以公用一個類也可以分開,部落客是分開建立了兩個類。自定義過轉場動畫的開發者都知道,轉場動畫最重要的是實作UIViewControllerAnimatedTransitioning協定中的兩個方法:

(1)- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext;

(2)- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext;

第一個方法實作具體的動畫效果,第二個方法傳回動畫執行時間

XPYOpenBookAnimation

@interface XPYOpenBookAnimation () <UIViewControllerAnimatedTransitioning>

@end

@implementation XPYOpenBookAnimation

+ (id<UIViewControllerAnimatedTransitioning>)openBookAnimation {
    XPYOpenBookAnimation *animation = [[XPYOpenBookAnimation alloc] init];
    return animation;
}

- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
    // 擷取目标視圖
    UIViewController *fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *tempFromView = nil;
    UIView *tempToView = nil;
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {    //iOS8
        tempToView = [transitionContext viewForKey:UITransitionContextToViewKey];
        tempFromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    } else {
        tempToView = toController.view;
        tempFromView = fromController.view;
    }
    // 截圖(afterScreenUpdates:是否所有效果應用在視圖以後再截圖)
    UIView *fromView = [fromController.bookCoverView snapshotViewAfterScreenUpdates:NO];
    UIView *toView = [tempToView snapshotViewAfterScreenUpdates:YES];
    
    //fromView和toView加入到containerView中
    [transitionContext.containerView addSubview:toView];
    [transitionContext.containerView addSubview:fromView];
    
    // 儲存frame
    CGRect fromFrame = fromController.bookCoverView.frame;
    CGRect toFrame = toView.frame;
    
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    
    // 修改anchorPoint為(0, 0.5)(Y軸中間位置)
    fromView.layer.anchorPoint = CGPointMake(0, 0.5);
    
    // 修改了anchorPoint之後需要重新設定frame
    fromView.frame = fromFrame;
    // toView初始位置與fromView一樣
    toView.frame = fromFrame;
    // 動畫
    [UIView animateWithDuration:duration animations:^{
        // 沿Y軸逆時針旋轉90度
        fromView.layer.transform = CATransform3DMakeRotation(- M_PI_2, 0, 1, 0);
        // frame變化
        fromView.frame = toFrame;
        toView.frame = toFrame;
    } completion:^(BOOL finished) {
        // 動畫結束移除截圖
        [fromView removeFromSuperview];
        [toView removeFromSuperview];
        
        // bookCoverView設為nil
        fromController.bookCoverView = nil;
        
        // containerView添加目标視圖
        [transitionContext.containerView addSubview:tempToView];
        // 還原子視圖
        [transitionContext.containerView.layer setSublayerTransform:CATransform3DIdentity];
        // 結束轉場(這裡使用!transitionContext.transitionWasCancelled,避免手勢取消時造成卡頓現象)
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    
}

/// 動畫時間
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.8;
}
           

XPYCloseBookAnimation

@interface XPYCloseBookAnimation () <UIViewControllerAnimatedTransitioning>

@property (nonatomic, strong) UIView *bookCoverView;

@end

@implementation XPYCloseBookAnimation

+ (id<UIViewControllerAnimatedTransitioning>)closeBookAnimation {
    XPYCloseBookAnimation *animation = [[XPYCloseBookAnimation alloc] init];
    return animation;
}

- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
    UIViewController *fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *tempFromView = nil;
    UIView *tempToView = nil;
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {    //iOS8
        tempFromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        tempToView = [transitionContext viewForKey:UITransitionContextToViewKey];
    } else {
        tempFromView = fromController.view;
        tempToView = toController.view;
    }
    UIView *toView = [fromController.bookCoverView snapshotViewAfterScreenUpdates:YES];
    UIView *fromView = [tempFromView snapshotViewAfterScreenUpdates:NO];
    [transitionContext.containerView addSubview:fromView];
    [transitionContext.containerView addSubview:toView];
    
    // 儲存frame
    CGRect fromFrame = fromView.frame;
    CGRect toFrame = fromController.bookCoverView.frame;
    
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    
    // 修改anchorPoint為(0, 0.5)(Y軸中間位置)
    toView.layer.anchorPoint = CGPointMake(0, 0.5);
    
    // 修改了anchorPoint之後需要重新設定frame
    fromView.frame = fromFrame;
    // toView初始位置與fromView一樣
    toView.frame = fromFrame;
    // 設定toViewY軸初始位置旋轉角度
    toView.layer.transform = CATransform3DMakeRotation(- M_PI_2, 0, 1, 0);
    
    // 添加toView并隐藏fromView
    [transitionContext.containerView insertSubview:tempToView atIndex:0];
    tempFromView.hidden = YES;
    
    [UIView animateWithDuration:duration animations:^{
        toView.layer.transform = CATransform3DIdentity;
        fromView.frame = toFrame;
        toView.frame = toFrame;
    } completion:^(BOOL finished) {
        // 動畫結束移除截圖
        [fromView removeFromSuperview];
        [toView removeFromSuperview];
        
        // bookCoverView設為nil
        fromController.bookCoverView = nil;
        
        // 還原子視圖
        [transitionContext.containerView.layer setSublayerTransform:CATransform3DIdentity];
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if ([transitionContext transitionWasCancelled]) {
            tempFromView.hidden = NO;
            [tempToView removeFromSuperview];
        }
    }];
}

- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.8;
}
           

3、最重要部分完成以後隻需要在自己需要執行動畫的控制器中(push為XPYBookStackViewController,pop為XPYReaderManagerController)設定UINavigationControllerDelegate,并在push時将書籍封面儲存到目标控制器中即可。

XPYBookStackViewController(push)

#import "UIViewController+Transition.h"


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    // 每次Appear重新設定NavigationController代理,實作自定義轉場動畫
    self.navigationController.delegate = self;
}

/// 因為部落客使用的是UICollectionView展示書籍封面,是以在didSelectItemAtIndexPath代理方法中push
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    // 擷取資料模型
    XPYBookModel *book = self.dataSource[indexPath.item];
    // XPYBookStackCollectionViewCell是部落客自定義的cell
    XPYBookStackCollectionViewCell *cell = (XPYBookStackCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
    // 對cell中的bookCoverImageView封面控件截圖
    UIView *snapshotView = [cell.bookCoverImageView snapshotViewAfterScreenUpdates:NO];
    // 截圖的frame設定為封面控件在keywindow上的frame
    snapshotView.frame = [cell.bookCoverImageView convertRect:cell.bookCoverImageView.frame toView:XPYKeyWindow];
    // 設定目前控制器的書籍封面視圖
    self.bookCoverView = snapshotView;
    // 建立閱讀器
    XPYReaderManagerController *reader = [[XPYReaderManagerController alloc] init];
    reader.book = book;
        
    // 閱讀器書籍封面視圖使用同一個截圖但是frame不同
    UIView *readerSnapshotView = [cell.bookCoverImageView snapshotViewAfterScreenUpdates:NO];
    // 進入閱讀器後預設将書籍排在最前位置,是以需要擷取第一本書籍的frame
    XPYBookStackCollectionViewCell *firstCell = (XPYBookStackCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    readerSnapshotView.frame = [firstCell.bookCoverImageView convertRect:firstCell.bookCoverImageView.frame toView:XPYKeyWindow];
    // 設定閱讀器的書籍封面視圖
    reader.bookCoverView = readerSnapshotView;
        
    [self.navigationController pushViewController:reader animated:YES];
}
           

XPYReaderManagerController(pop)

#import "UIViewController+Transition.h"

/// 隻需要在退出書籍方法中調用pop
- (void)readMenuDidExitReader {
     [self.navigationController popViewControllerAnimated:YES];
}
           

        主要的實作過程和代碼已經說完了,如果還有不明白的地方可以自己下載下傳項目研究,該小說項目内容還是比較豐富的,喜歡的朋友可以點個贊。項目位址:https://github.com/xiangxiaopenyou/XPYReader