iOS 自定义转场动画实现小红书的push效果思路以及下雪碎屏等动画的实现

感觉好久没写会动的Demo了,前几天写了很久的Block源码分析,分析了几天整个人都不好了,都不知道block是什么了......,有需要的同学可以去看看,简直不要太简单Block是什么鬼毕竟也是做电商的,有时候会研究别人家的App实现,有写过Higo的页面,也看了小红书的push动画,之前一直不知道怎么做到的,偶然间看到了转场动画自定义,原来是这么玩的,OK,今天以非常简单的方式带大家入门转场动

老规矩,没图说个毛线,先看会儿动画在决定是否继续看。。。。。。

    

TNND,为什么只能上传2M的图!!!!!!





转场其实就是下一场景的VC视图替换当前场景VC视图以及控制器的过程,我们看到的系统自带的就是当前视图消失了,下个视图出现,这是非常基本的动画了转场动画不言而喻,最重要的其实是动画,学习了几天,简单分析下两种情况

1.就是最常见的UINavigationController中的push和pop

2.Modal转场,present以及dissmiss,支持的模式不多,UIModalPresentationCustom和   

   UIModalPresentationFullScreen



根据上面说的两种,也是最常用的,我们来自定义一下,看看能搞成什么样子。

Apple分离了很多协议出来支持转场动画,我们不深入研究,就用最简单的两种来实现,而且我只介绍我用到的代理,不用的就不BB了,不然看起来很乱
1.转场代理

<UINavigationControllerDelegate>  对应UINavigationController delegate属性遵守该协议
<UIViewControllerTransitioningDelegate> 对应UIViewcontroller的transitioningDelegate属性遵守该协议


2.动画控制器(最核心的就是他了)
<UIViewControllerAnimatedTransitioning>
我们在这里通过转场上下文transitionContext来获取我们需要的View,然后可以添加视图以及执行动画


3。转场环境(这个是系统已经带上了,我们知道就好)
<UIViewControllerContextTransitioning>就是上面动画控制器获取的对象,提供了转场需要的数据和交互


知道有这三个代理,我们来简单举个例子,这样看起来舒服点

我们先拿UINavigationController举个例子
首先我们需要有个类来实现转场代理,有两个方法
第一:我们用一个一个UINavigationController的子类,来实现转场代理。在代理方法里面返回对应的动画对象
第二:我们可以弄一个NSObject的类型,实现转场代理,代码写的话可以在外部弄个属性强引用封装的代理对象,用storyBoard的话直接在IB页面拖个NSObject
类型的东东,和拖View一样拖进去就强引用了,然后把类型设置为刚才封装的代理对象就好了

然后我们看到这个方法是需要一个实现<UIViewControllerAnimatedTransitioning>协议的动画对象,就创建一个NSObject实现这个代理方法
这里有两个,一个是转场时间,一个就是真正的转场动画的实现方法,这里你就需要转场环境transitionContext进行布局以及动画交互,然后把你需要的动画写出来就好了



再拿UIViewcontroller的present来举个例子
首先一样,我们需要实现转场代理,这里也有很多方法
第一:让self成为transitioningDelegate的代理,然后实现转场代理,同样里面返回对应的动画对象
第二:用UIPresentationController系统提供的类来全权保管需要的两个转场代理和动画代理,这类能管理view的各种属性和时间点,我们只需要present的时候
把代理只想继承于UIPresentationController对象的子类就好了,然后我们需要实现的动画就出来了


针对上面举得两个例子,我们等下分别介绍第一种方法




根据图片来看,push和present还是有结构上的区别的,有个点要特别注意,不然你的页面就黑了

push和present其实不只是上面代理的区别,我们在实现UIViewControllerAnimatedTransitioning这个协议的动画控制器里面通过转场环境来操作的时候就需要注意了

push操作的转场动画在动画结束之后,presenting(当前页面)是跟着消失了,但是present就蛋疼了,有两种情况,当在这个模式下的时候UIModalPresentationFullScreen和push一样,会先移除视图,然后dismiss的时候后再重新进来,当在UIModalPresentationCustom的时候,看上面的图来说就是presneting未参与转场,所以动画结束之后它未移出视图结构,在dismiss的时候,就会有差异。

差异就是,当我们push还是pop的时候都会从转场环境

(id<UIViewControllerContextTransitioning>)transitionContext拿出toView来,然后add到containerView里面

虽然我也不知道这里containerView是怎么工作的......如果是UIModalPresentationCustom这个模式下dismiss的时候,你再add进去,就黑了,他本身就不会参与转场,而且也不会从视图结构中移除,所以这个点还是要小心


知识点:

1.小红书push转场动画以及图片上的小动画

2.present自定义小窗口模式的动画

3.离子发射器做下雪动画

4.碎屏美女

5.还有一个非常简单的cell切换以及瀑布流


第一步:瀑布流搭建以及下雪动画

瀑布流的效果这里不详细介绍了需要的同学请戳我另一篇点击打开链接

下雪动画这个东西我用的是CAEmitterLayer和CAEmitterCell这套东西做的,本来这东西我是做成点赞之后爆炸的效


但是改着改着感觉能发射特效,还是蛮好玩的,就改了下属性,就能下雪了,只要记住cell是小颗粒,layer是容器,类似于tableView的cell,这样想去改属性就能得到自己想要的效果了

self.clipsToBounds = NO; // 让例子能弹出固定的框框,不然只有在框框内而已
    self.userInteractionEnabled = NO; // 取消交互,不然点不到
    CAEmitterCell *cell = [CAEmitterCell emitterCell];
    cell.contents = (id)[UIImage imageNamed:@"favorite_hl"].CGImage; // 这里要的是CGImage
    cell.name = @"snow";
    cell.lifetime = 10.0f;
    cell.lifetimeRange = 100.0;
    // 隐掉的时间
    // 初始化右多少个
    cell.birthRate = 10;
    cell.velocity = 10.0f; // 越大越往外 速度越快
    cell.velocityRange = 10.0f; // 越小就越接近圆形,越大越不规则
    cell.emissionRange = M_PI_2; // 发散的范围
    cell.yAcceleration = 2;
    cell.scale = 0.5f; // 栗子倍数
    cell.scaleRange = 0.02;
    
    
    _emitterLayer = [CAEmitterLayer layer];
    _emitterLayer.emitterShape = kCAEmitterLayerLine; // layer的形状
    _emitterLayer.emitterMode = kCAEmitterLayerSurface; // layer的弹出方式
    _emitterLayer.emitterSize = CGSizeMake(self.bounds.size.width * 2, 100);
    _emitterLayer.renderMode = kCAEmitterLayerOldestFirst;
    _emitterLayer.masksToBounds = NO;
    _emitterLayer.emitterCells = @[cell];
    _emitterLayer.frame = [UIScreen mainScreen].bounds;
    _emitterLayer.emitterPosition = CGPointMake(100, -40);
    
    [self.layer addSublayer:_emitterLayer];


第二步:实现push自定义转场动画前代理设置

1.自定义UINavigationController的子类,实现UINavigationDelegate

- (void)pushViewController:(UIViewController *)viewController
                 imageView:(UIImageView *)imageView
                    desRec:(CGRect)desRec
                  original:(CGRect)originalRec
                  deleagte:(id<MKJAnimatorDelegate>)delegate isRight:(BOOL)isRight;
根据属性也能看出,我们自定义的方法把原来的位置,目标位置,图片以及presentedVC传进去了,VC作为参数是之后设置代理用的


2.实现NavigationDelegate的方法 返回自定义动画控制器

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC
{
    // 普通push的时候就是小红书类型的
    MKJAnimator *animator = [[MKJAnimator alloc] init];
    animator.imageView = self.imageView;
    animator.destinationRec = self.destinationRec;
    animator.originalRec = self.originalRec;
    animator.isPush = self.isPush;
    animator.delegate = self.animationDelegate;
    
    // 左侧push的时候是屏幕碎裂类型的
    MKJSuperAnimation *superAnimator = [[MKJSuperAnimation alloc] init];
    
    if (self.isRight) {
        return animator;
    }
    else
    {
        return superAnimator;
    }
}


第三步:实现Push真正的自定义转场控制组件

1.创建一个对象遵循<UIViewcontrollAnimatedTransioning>代理,并生成一个动画的结束代理,让presented(push过去的VC)控制器来实现,让动画结束的时候做一系列操作

@protocol MKJAnimatorDelegate <NSObject>

- (void)animationFinish;

@end

@interface MKJAnimator : NSObject <UIViewControllerAnimatedTransitioning>

@property (nonatomic,strong) UIImageView *imageView;
@property (nonatomic,assign) CGRect destinationRec;
@property (nonatomic,assign) CGRect originalRec;
@property (nonatomic,assign) BOOL isPush;
@property (nonatomic,assign) id<MKJAnimatorDelegate>delegate;

@end

2.实现动画的核心代理方法持续时间和动画效果(核心代码)

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 获取上面那张图的containerVIew
    UIView* contentView = [transitionContext containerView];
    contentView.backgroundColor = [UIColor whiteColor];
    // toView就是push过去的View
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 加到容器View里面
    [contentView addSubview:toViewController.view];
    // 先设置为透明
    toViewController.view.alpha = 0;
    
    // 弄一个做动画的UIImageView
    __block UIImageView *imageView = [[UIImageView alloc] initWithImage:self.imageView.image];
    imageView.frame = self.isPush ? self.originalRec : self.destinationRec;
    [contentView addSubview:imageView];
    
    if (!self.isPush) {
        self.imageView.alpha = 0;
    }
    
    // 让临时的ImageView根据坐标动起来 然后把toView也渐渐显示
    [UIView animateWithDuration:0.5 animations:^{
       
        imageView.frame = self.isPush ? self.destinationRec : self.originalRec;
        toViewController.view.alpha = 1.0f;
        
    } completion:^(BOOL finished) {
        // 动画结束必定要告知这个对象结束了,不然问题
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        [imageView removeFromSuperview];
        if (!self.isPush) {
            self.imageView.alpha = 1;
        }
        else
        {
            // 告诉代理结束了
            if (self.delegate) {
                [self.delegate animationFinish];
            }
        }
        imageView = nil;
    }];
}


第四步:实现Present Modal效果的自定义动画小弹窗


1.这里接着刚才的效果,动画结束之后先给个图片,然后实现小小的动画

// <MKJAnimatorDelegate> 代理的实现,通知动画结束
- (void)animationFinish
{
    // 图片往上平铺
    [self.mainImageView sd_setImageWithURL:[NSURL URLWithString:self.redBookModel.img]];
    
    // 然后做个简单的小动画显示圆点,直线和文字
    [self showAnimationTag];
}
2.点击右上角实现present自定义小弹窗

还是两步走,先实现UIViewControllerTransioningDelegate,让自身成为代理,而且也可以看到,和刚才一样返回一个自定义的动画组件即可

- (void)click1:(UIButton *)button
{
    ThirdViewController *thirdVC = [[ThirdViewController alloc] init];
    thirdVC.modalPresentationStyle = UIModalPresentationCustom; // 貌似只支持几种模式,自己写还是直接用custom好了
    thirdVC.transitioningDelegate =  self; // 自己成为代理,返回动画
    [self presentViewController:thirdVC animated:YES completion:nil];
}


#pragma - present的时候需要的代理方法
#pragma mark - UIViewControllerTransitioningDelegate
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [MKJPresentAnimator new];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [MKJPresentAnimator new];
}
然后再进行关键动画的组装,这里就是刚才提到了push和present的区别,当modal属性是custom的时候,present 之后的动画是不会把presentingVC给移出的,那么当dismiss的时候千万别再把toView通过AddSubview到

ContainerView里面去了

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.45f;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 这个是present的动画
    UIView *containerView = transitionContext.containerView;
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toViewController.view;
    UIView *fromView = fromViewController.view;
    // 这里present不和push一样需要自己标记,这里直接有跟踪标记
    if (toViewController.beingPresented)
    {
        // toView加进来,设置居中的小框,宽度为1
        
        [containerView addSubview:toView];
        toView.bounds = CGRectMake(0, 0, 1, kScreenHeight *2/3);
        toView.center = containerView.center;
        // 然后再加个蒙层 大小也是很小
        UIView *dimmingView = [[UIView alloc] init];
        dimmingView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3];
        dimmingView.center = containerView.center;
        dimmingView.bounds = CGRectMake(0, 0, kScreenWidth * 3 / 5, kScreenHeight * 3 / 5);
        [containerView insertSubview:dimmingView belowSubview:toView];
        // 动画toView开始变宽  蒙层开始变大
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
           
            toView.bounds = CGRectMake(0, 0, kScreenWidth * 3 / 5, kScreenHeight *1/2);
            dimmingView.bounds = containerView.bounds;
            
        } completion:^(BOOL finished) {
            
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
        
    }
    else
    {
        // 返回的时候直接放fromView动画缩小就行了,提交之后自动消失了,不需要在进行Add了,不然就黑了
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            
            fromView.bounds = CGRectMake(0, 0, 1, kScreenHeight * 1 / 2);
            
        } completion:^(BOOL finished) {
           
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
}

3.小弹窗里面的用Masonry实现的小动画(按钮旋转缩小以及View扩大)

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    // 给背景蒙层加手势
    for (UIView *dimmingView in self.view.superview.subviews) {
        if (![dimmingView isEqual:self.view])
        {
            [dimmingView addGestureRecognizer:self.tap];
        }
    }
    self.TFWidthConstraint.constant = self.view.bounds.size.width * 2 /3;
    self.labelWidthConstraint.constant = self.view.bounds.size.width * 2 / 3;
    [UIView animateWithDuration:0.3 animations:^{
        self.closeButton.alpha = 1.0f;
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {
    }];
}

- (IBAction)close:(id)sender {
    self.TFWidthConstraint.constant = 0;
    self.labelWidthConstraint.constant = 0;
    
    CGAffineTransform form = CGAffineTransformMakeRotation(M_PI);
    
    form = CGAffineTransformScale(form, 0.1, 0.1);
    [UIView animateWithDuration:0.8 animations:^{
        self.closeButton.transform = form;
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {
        [self dismissViewControllerAnimated:YES completion:nil];
    }];
}


第五步:动画组件实现Push自定义的碎屏动画


依旧是两步走,实现UINavitgationController代理刚才已经介绍了,无非就是弄个代理方法,看了那么多,这个效果其实最关键的还是动画的实现,你脑洞大,那么动画必然就很酷炫,前提是你代码能力比脑洞还大,伪代码

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 获取两个VC和对应的View
    1.拿出需要的UIView
    // push是要判断的,present是有属性跟踪的
    2.BOOL isPush 是否push?
    // 截屏
    3.截屏
    UIGraphicsBeginImageContextWithOptions(containerView.bounds.size, YES, containerView.window.screen.scale);
    [xxx drawViewHierarchyInRect:containerView.bounds afterScreenUpdates:NO];
    
    // 加一层动画的View,这里会有layer一个个铺在上面,是原始切割数量的两倍,正面是from,背面是to,事先摆好位置
    4.[containerView addSubview:transitionContainerView];
    
    5.计算二位数组切割后
    CGFloat sliceSize = round(CGRectGetWidth(containerView.bounds) / 8.f);
    NSUInteger xSlices = ceil(CGRectGetWidth(containerView.bounds) / sliceSize);
    NSUInteger ySlices = ceil(CGRectGetHeight(containerView.bounds) / sliceSize);
    
    // 防止layer,layer是加到小格子View上面的,但是layer是全屏的,所以每个小格子要对应上必须进行负数偏移,不然图像都不完整了
    6.布局
    for (NSUInteger y = 0 ; y < ySlices; y++)
    {
        for (NSUInteger x = 0; x < xSlices; x++)
        {
            CALayer1
            CALayer2
            
            UIView *toCheckboardSquareView
            [toCheckboardSquareView.layer addSublayer:toContentLayer];
            
            UIView *fromCheckboardSquareView
            [fromCheckboardSquareView.layer addSublayer:fromContentLayer];

            [transitionContainerView addSubview:toCheckboardSquareView];
            [transitionContainerView addSubview:fromCheckboardSquareView];
        }
    }
    // 上面是to和from的View交错放置,所以这个时候需要*2来拿出各自的View
    __block NSUInteger sliceAnimationsPending = 0;
    for (NSUInteger y = 0 ; y < ySlices; y++)
    {
        for (NSUInteger x = 0; x < xSlices; x++)
        {
            7.计算时间........
            // 通过上面定的时间计算持续时间以及延迟时间
            NSTimeInterval startTime = projectionLength/(transitionVectorLength + transitionSpacing) * transitionDuration;
            NSTimeInterval duration = ( (projectionLength + transitionSpacing)/(transitionVectorLength + transitionSpacing) * transitionDuration ) - startTime;
            sliceAnimationsPending++;
            [UIView animateWithDuration:duration delay:startTime options:0 animations:^{
                8.开始动画。。。。。。
            } completion:^(BOOL finished) {
                // Finish the transition once the final animation completes.
                if (--sliceAnimationsPending == 0)
                    9. 结束动画。。。。。。
    }
}


总结下:

1.present和push动画是有所区别的,结构可以看上面的图,有的时候不区分是会出问题

2.自定义无论哪种,第一步就是实现该容易控制器代理的方法返回自定义动画组件(该组件必须实现指定代理)

第二步就是实现自定义动画控制器

3.CAEmitterLayer可以实现很多离子特效,这里简单介绍了两个

这个只是非常简单的两步走实现自定义转场动画,先有个基本了解,在进行更高层次的自定义效果就会更好,这些动画对性能来讲肯定会有所影响,有这样的想法必然就回去用GPUImage和OpenGL来实现图形动画,这些实现的效果等以后有空了在去研究分析下

简单的介绍了下流程,反正我看东西还是喜欢看Demo,需要的同学点下面的链接直接下载看吧

文章Demo链接:点击打开本文Demo链接

参考唐巧大神的理论知识文章:点击打开链接






  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值