美文网首页
一个简单的抽屉侧边栏的实现.

一个简单的抽屉侧边栏的实现.

作者: yearwen | 来源:发表于2016-07-26 13:40 被阅读0次

    前段时间看了一本书《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 动画学习手册 》 在里面看到一个很酷的侧边栏弹出效果.

    最近打算重写一个我自己个私人项目,就想到了用这个效果来作为界面跳转的方式,下面就来介绍一下

    我们首先来看一下完成的效果

    特效1.gif

    整个菜单栏的弹出效果可以分为两个部分,下面会一一分析.其实我之前很少接触动画效果,但是(对,这里有但是),遇到动画不能害怕去写,只要学会分析动画的效果,其实并不难.

    第一部分动画: 先来看看效果

    动画1.gif
    这部分可以看做是菜单栏唤醒之前和菜单栏没有唤醒的动画效果.
    这一部分我参考了iOS - 用 UIBezierPath 实现果冻效果这篇文章实现的方式

    好了,回到正题,首先,用KVO的方式监听一个 view的坐标点(上图中红色的小点点),通过这个坐标点来更新CAShapeLayer的形状,进而达到动画的效果
    下面是观察的方法与设置的方法(具体可以看代码或者参照上面那个链接)

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:kX] || [keyPath isEqualToString:kY]) {
                [self updateShapeLayerPathWithEnd];
        }
    }
    - (void)updateShapeLayerPath
    {
        // 更新_shapeLayer形状
        UIBezierPath *tPath = [UIBezierPath bezierPath];
        [tPath moveToPoint:CGPointMake(0, 0)];                              // 1点
        [tPath addLineToPoint:CGPointMake(0,  SYS_DEVICE_HEIGHT)];  //2点
        [tPath addQuadCurveToPoint:CGPointMake(0, 0)
                      controlPoint:CGPointMake(_curveX, _curveY)]; // 确定一个弧线
        [tPath closePath];
        _shapeLayer.path = tPath.CGPath;
    }
    

    下面重点来了.怎么实现弹簧的效果呢
    先来介绍一个东东 : CADisplayLink
    CADisplayLink默认每秒运行60次calculatePath是算出在运行期间_curveView的坐标,从而确定_shapeLayer的形状

        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(calculatePath)];
        [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        _displayLink.paused = YES;//这里先暂停
    
    //CADisplayLink 绑定的方法
    - (void)calculatePath
    {
        // 由于手势结束时,view会执行一个弹簧动画,把这个过程的坐标记录下来,并相应的画出_shapeLayer形状
        CALayer *layer = _curveView.layer.presentationLayer;
        self.curveX = layer.position.x;
        self.curveY = layer.position.y;
    }
    
    

    iOS7之后苹果更加注重用户交互,所以添加了一部分动画效果的接口.其中有一个就是实现弹簧效果的函数

    [UIView animateWithDuration:(NSTimeInterval)//动画持续时间
    delay:(NSTimeInterval)//动画延迟时间
    usingSpringWithDamping:(CGFloat)//类似弹簧振动效果0~1
    initialSpringVelocity:(CGFloat)//初始速度
    options:(UIViewAnimationOptions)//动画过渡效果
    animations:^{
    // code
    } completion:^(BOOL finished) {
    // code
    }]
    usingSpringWithDamping:它的范围为0.0f到1.0f,数值越小「弹簧」的振动效果越明显。
    initialSpringVelocity:初始的速度,数值越大一开始移动越快。值得注意的是,初始速度取值较高而时间较短时,也会出现反弹情况。
    Spring Animation是线性动画或ease-out动画的理想替代品。由于iOS本身大量使用的就是Spring Animation,用户已经习惯了这种动画效果,因此使用它能使App让人感觉更加自然,用Apple的话说就是「instantly familiar」。此外,Spring Animation不只能对位置使用,它适用于所有可被添加动画效果的属性。
    

    上面说到用的是一个view的坐标点来更新CAShapeLayer层来实现效果的,所以我们只要在手势的绑定的方法中实现以下代码即可

     if(pan.state == UIGestureRecognizerStateChanged)
            {
                // 手势移动时,_shapeLayer跟着手势扩大区域
                CGPoint point = [pan translationInView:self];
    
                // 这部分代码使view与手指的坐标绑定并更新view的坐标,通过kvo修改坐标的值
                _mHeight = point.y*0.7 + MIN_HEIGHT;
                self.curveX = point.x;
                self.curveY =SYS_DEVICE_HEIGHT/2;
                _curveView.frame = CGRectMake(_curveX,
                                              _curveY,
                                              _curveView.frame.size.width,
                                              _curveView.frame.size.height);
                
                
            }else if (pan.state == UIGestureRecognizerStateCancelled ||
                     pan.state == UIGestureRecognizerStateEnded ||
                     pan.state == UIGestureRecognizerStateFailed)
            {
                // 手势结束时,_shapeLayer返回原状并产生弹簧动效
                _displayLink.paused = NO;   //开启displaylink,会执行方法calculatePath.
                _curveX  = _curveView.frame.origin.x;
                // 弹簧动效
                [UIView animateWithDuration:1.0
                                      delay:0.0
                     usingSpringWithDamping:0.3
                      initialSpringVelocity:0
                                    options:UIViewAnimationOptionCurveEaseInOut
                                 animations:^{
                                     // 曲线点(r5点)是一个view.所以在block中有弹簧效果.然后根据他的动效路径,在calculatePath中计算弹性图形的形状
                                     _curveView.frame = CGRectMake(0, self.window.frame.size.height/2, 3, 3);
                                 } completion:^(BOOL finished) {
                                     if(finished)
                                     {
                                         _displayLink.paused = YES;
                                         _isAnimating = NO;
                                     }
                                 }];
            }
    

    至此,第一部分的动画效果就已经实现.
    第二部分效果就是弹出菜单的动画效果了
    第二部分我是参考 《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 动画学习手册 》这本书中提到的动画实现效果,书中大神说到他做动画的心得: “「善于拆解」。即把一个复杂的动画分解为几个分动画,然后再把这些分动画逐一解决。”

    废话不多说,看看大神是怎么实现的吧
    我们先把整个第二部分拆解成一个 View 从屏幕左侧移入

    self.frame = CGRectMake(-keyWindow.frame.size.width/2-EXTRAAREA, 0, keyWindow.frame.size.width/2+EXTRAAREA, keyWindow.frame.size.height);
    self.backgroundColor = [UIColor clearColor];
    [self.view insertSubview:self belowSubview:helperSideView];
    //“右侧还留出了 30px(即代码中的 EXTRAAREA ) 的透明区域。理由很简单,因为如果不这么做,发生弹性时向右突出的边界就看不到了。”
    

    这个View创建之后我们可以加上动画效果,使它从屏幕外移到屏幕上显示,或者菜单栏消失的时候使它移出屏幕
    接下来就是实现动画了.思路跟上面那个差不多一样,也是用贝塞尔曲线画出范围,然后用CADisplayLink来实现.
    但是(对,还是一个但是),到这的时候我发现一个问题,实现弹簧震动效果的地方不止一处,可我只有一个CADisplayLink的对象,该怎么实现呢.
    有一个办法,就是每次需要实现动画效果的时候,实现CADisplayLink绑定一个方法,动画完成之后,再把CADisplayLink对象绑定的方法给解掉.这样一个界面就能反复使用同一个CADisplayLink对象实现不同的动画效果.

    这里跑偏一下说一下CADisplayLink这个东东的其他的一些属性

    @property(readonly, nonatomic) CFTimeInterval duration;//每帧之间的时间
    @property(nonatomic) NSInteger frameInterval;//间隔多少帧调用一次 selector 方法,默认值是 1 ,即每帧都调用一次。如果每帧都调用一次的话,对于 iOS 设备来说那刷新频率就是 60HZ 也就是每秒 60 次,如果将 frameInterval 设为 2 那么就会两帧调用一次,也就是变成了每秒刷新 30 次。
    @property(getter=isPaused, nonatomic) BOOL paused;//是否暂停当前的定时器,控制 CADisplayLink 的运行。
    
    

    好,回到正题,现在我们要实现的动画效果式菜单栏弹出的过程中,实现侧边凸起,和菜单栏返回的时候,侧边凹进去的效果,即:
    “ 如何产生一组变化的数值,x 增加到某个正数,再从这个正数(也就是最大值 P 点)递减到一个负数,最后从这个负数(也就是最小值 Q 点)递增到 x ?” (x是view 中心点的x坐标)

    我在书中看到作者提供了一个技巧,就是创建两个辅助视图(代码中的helperSideView与helperCenterView),然后设置两个辅助视图动画移动的起点与终点一样,只是把两个动画效果的初始速度设置一个时间差,这样就会起到一个获取到一个变化的差值,这个差值就可以用来刷新动画,形成回弹效果了
    额....代码Show起来:

            [self beforeAnimation];
            [UIView animateWithDuration:0.7
                                  delay:0.0f
                 usingSpringWithDamping:0.3f
                  initialSpringVelocity:0.9f     //辅助视图1的动画起始时间设为0.9
                                options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
                
                helperSideView.center = CGPointMake(keyWindow.center.x, helperSideView.frame.size.height/2);
                
            } completion:^(BOOL finished) {
                [self finishAnimation];
            }];
        
            [UIView animateWithDuration:0.3 animations:^{
                blurView.alpha = 1.0f;//这个视图为最下层的淡灰色半透明毛玻璃视图
            }];
            
            
            [self beforeAnimation];
            [UIView animateWithDuration:0.7
                                  delay:0.0f
                 usingSpringWithDamping:0.8f
                  initialSpringVelocity:2.0f  //辅助视图2的动画起始时间设为2.0
                                options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
                
                helperCenterView.center = keyWindow.center;
                
            } completion:^(BOOL finished) {
                if (finished) {
          //动画2 完成之后要给灰色区域添加点击手势,用来取消菜单栏.
                    UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapToUntrigger:)];
                    [blurView addGestureRecognizer:tapGes];
                    
                    [self finishAnimation];
                }
            }];
    
    

    其中的方法:[self beforeAnimation] 和 [self finishAnimation] 用于控制 CADisplayLink 什么时候应该移除。用到了动画的累加计数:每开始一个动画时计数器加 1,每停止一个动画时计数器减 1,当两个动画都完成时,计数器为 0,此时移除 CADisplayLink,就是上文提到的用一个CADisplayLink对象完成多个动画,并在不用的时候移除CADisplayLink对象的方法

    //动画开始之前调用
    -(void)beforeAnimation{
      if (self.displayLink == nil) {
            self.displayLink = [CADisplayLink displayLinkWithTarget:self 
            selector:@selector(displayLinkAction:)];
     
            [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        }
        self.animationCount ++;
    }
     
    //动画完成之后调用
    -(void)finishAnimation{
        self.animationCount --;
        if (self.animationCount == 0) {
            [self.displayLink invalidate];
            self.displayLink = nil;
        }
    }
    

    上面实现了菜单栏的动画的计算.接下来就是怎么让这个动画显示出来,在第一阶段的时候,我们是用的KVO的方式,只要是标记view的坐标有改变,就会刷新layer层的动画效果.现在这个动画是有两个标记view,怎么搞?

    还是一样的,在beforeAnimation的方法中,displayLink绑定了一个方法displayLinkAction:, 在这个方法里,我们除了要获取到前面提到的那个差值,还需要另外一个方法,区别于第一种KVO更新动画效果的方式,调用[self setNeedsDisplay]方法,调用这个方法的时候就会触发UIView的- (void)drawRect:(CGRect)rect;方法或 CALayer 的 drawRectInContext方法

    -(void)displayLinkAction:(CADisplayLink *)dis{
        CALayer *sideHelperPresentationLayer   =  (CALayer *)[helperSideView.layer presentationLayer];
        CALayer *centerHelperPresentationLayer =  (CALayer *)[helperCenterView.layer presentationLayer];
        CGRect centerRect = [[centerHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];
        CGRect sideRect = [[sideHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];
    
        diff = sideRect.origin.x - centerRect.origin.x; //注意,这个diff值就是计算出来的那个差值
    
        [self setNeedsDisplay];
    }
    

    因为[self setNeedsDisplay] 处于CADisplayLink 的方法里面,所以会连续调用UIView的- (void)drawRect:(CGRect)rect;方法,下面是drawRect: 的实现

    - (void)drawRect:(CGRect)rect {
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:CGPointMake(0, 0)];
        [path addLineToPoint:CGPointMake(self.frame.size.width-EXTRAAREA, 0)];
    
        [path addQuadCurveToPoint:CGPointMake(self.frame.size.width-EXTRAAREA, self.frame.size.height) 
        controlPoint:CGPointMake(keyWindow.frame.size.width/2+diff, keyWindow.frame.size.height/2)];
            //注意,这里要加上之前计算出来的diff值
    
        [path addLineToPoint:CGPointMake(0, self.frame.size.height)];
        [path closePath];
        
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextAddPath(context, path.CGPath);
        [_menuColor set];
        CGContextFillPath(context);
    }
    

    好了,到现在为止基本就可以实现动画效果了,最后就只剩下按钮的动画效果了
    其实按钮的动画效果也是回弹效果,只不过开始的起始时间依次增加,所以,我们只要在菜单视图完全弹出之后再调用按钮的动画效果就可以实现.

    -(void)animateButtons{
        for (NSInteger i = 0; i < self.subviews.count; i++) {
            UIView *menuButton = self.subviews[i];
            menuButton.transform = CGAffineTransformMakeTranslation(-90, 0);
            [UIView animateWithDuration:0.7
                                  delay:i*(0.3/self.subviews.count) //动画开始的时间依次延迟0.3秒.
                 usingSpringWithDamping:0.4f 
                  initialSpringVelocity:0.0f
                                options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
                menuButton.transform =  CGAffineTransformIdentity;
            } completion:NULL];
        }
    }
    

    然后,我们来看看在displayLinkAction 的方法中 CALayer *sideHelperPresentationLayer = (CALayer *)[helperSideView.layer presentationLayer]; CALayer的presentationLayer的作用:
    当你给一个 CALayer 添加动画的时候,动画其实并没有改变这个 layer 的实际属性。取而代之的,系统会创建一个原始 layer 的拷贝。在文档中,苹果称这个原始 layer 为 Model Layer ,而这个复制的 layer 则被称为 Presentation Layer 。 Presentation Layer 的属性会随着动画的进度实时改变,而 Model Layer 中对应的属性则并不会改变。在这里我们只是用到了layer的x坐标用来计算.其他的应用,我也在学习中.

    上文中的工程地址: 一个简单的抽屉侧边栏

    写在最后: 这是我第一次写这类的文章,思路不是很清晰,敬请谅解.这个效果库我会趁着有空慢慢优化,现在还只是一个开始,有什么好的建议可以给我说下.另外欢迎大神帮忙优化代码.敬表感谢. 对了,还有,《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 动画学习手册 》这本书真的很好,有心学习动画效果的朋友一定要看看.

    相关文章

      网友评论

          本文标题:一个简单的抽屉侧边栏的实现.

          本文链接:https://www.haomeiwen.com/subject/jyjnjttx.html