天天看点

一个打开/关闭书籍动画(转场动画)

​​​​​​        最近一直在写小说阅读器相关内容,看了当下热门的几款小说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