利用UIBezierPath实现的橡皮筋动画效果(OC版)

      本教程旨在说明第三方库DGElasticPullToRefresh的橡皮筋动画效果的实现原理,不过它使用swift实现的,平时用OC最多,顾抽出时间翻译成OC版,供大家参考。

       前提:

       在讲这篇教程之前,如果你对UIBezierPath和CAShapeLayer还不熟悉的话,那需要对这两个方面要做了解,这样才能够看懂代码部分。UIBezierPath可以绘制贝塞尔曲线,那么就需要设置控制点,这里分为三段,其中L3和L2、R1和R2、c和R1,效果如下图所示:

       1、初始状态效果

       2、拉伸后的效果

        拉伸后会有个回弹效果,为了达到这种效果,可以通过CADisplayLink来实现,它将在主run loop中运行,并且每帧调用一次所需的功能。

        实现:

        one part

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _minimalHeight = 50.0;   //原始状态的高度
    _maxWaveHeight = 100.0;  //最高状态的高度
    
    _animating = NO;
    self.view.userInteractionEnabled = YES;
    
    //矩形框
    _shapeLayer = [CAShapeLayer layer];
    _shapeLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, _minimalHeight);
    _shapeLayer.backgroundColor = [UIColor colorWithRed:91/255.0 green:144/255.0 blue:212/255.0 alpha:1.0].CGColor;
    [self.view.layer addSublayer:_shapeLayer];
    
    //添加手势,可以改变frame的高度
    UIPanGestureRecognizer *panG = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureDidMove:)];
    [self.view addGestureRecognizer:panG];
}

- (void) panGestureDidMove:(UIPanGestureRecognizer *)pan
{
    if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateFailed || pan.state == UIGestureRecognizerStateCancelled) {
        
        
    }
    else
    {
        CGFloat testHeight = _minimalHeight + MAX([pan translationInView:self.view].y, 0);
        CGRect rect = _shapeLayer.frame;
        rect.size.height = testHeight;
        _shapeLayer.frame = rect;
    }
}

              解释:这里很简单,先初始化一个CAShapeLayer矩形框图层,然后给视图添加拖动的手势,利用当前手指拖动时视图中point中的y坐标来改变CAShapeLayer的高度。


       仔细观察,会发现当改变图层的高度时,会有一点点延时出现,那这样效果不好,又该怎么去解决呢?那么就需要添加下面一行代码来防止这种现象的出现。

    _shapeLayer.actions = @{@"position":[NSNull null],@"bounds":[NSNull null],@"path":[NSNull null]};  //关闭隐式动画,防止带来延迟效果

        添加到addSubLayer:方法前面,再次运行程序,发现之前延时的效果已经没有了。

        two part

        这部分就是添加之前提及的7个控制点,l1、l2、l3、c、r1、r2、r3,这7个点其实是一个3x3的view,代码如下:

    _l3ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _l3ControlPointView.backgroundColor = [UIColor redColor];
    [self.view addSubview:_l3ControlPointView];
    _l2ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _l2ControlPointView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:_l2ControlPointView];
    _l1ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _l1ControlPointView.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:_l1ControlPointView];
    _cControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _cControlPointView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:_cControlPointView];
    _r1ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _r1ControlPointView.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:_r1ControlPointView];
    _r2ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _r2ControlPointView.backgroundColor = [UIColor purpleColor];
    [self.view addSubview:_r2ControlPointView];
    _r3ControlPointView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 3, 3)];
    _r3ControlPointView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:_r3ControlPointView];

       接下来需要做一个UIView extension,当动画从一个frame到另外一个frame时,需要获取UIView.frame和UIView.center来代替当前的动画以达到最终的动画效果;所以我们就需要获取到UIView.layer.presentationLayer的position;这里涉及到一个presentationLayer,做动画时presentationLayer里面的值就是当前屏幕的值,这里就不一一赘述。代码如下:

- (CGPoint) usePresentationLayerIfPossible:(BOOL)theBool
{
    if (theBool == YES) {
        
        CALayer *theLayer = self.layer;
        return [[theLayer presentationLayer] position];
    }
    
        return self.center;
}

 

       three part

       创建当前路径的方法,就是绘制贝塞尔曲线,其中两个点是控制点,其中l3和l2、c和r1、r1和r2,这里涉及到UIBezierPath绘制贝塞尔曲线知识,返回当前shapeLayer的CGPath,代码如下

- (CGPathRef )currentPath
{
    CGFloat width = self.view.bounds.size.width;
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:CGPointMake(0, 0)];
    [bezierPath addLineToPoint:CGPointMake(0, [_l3ControlPointView usePresentationLayerIfPossible:_animating].y)];
    [bezierPath addCurveToPoint:[_l1ControlPointView usePresentationLayerIfPossible:_animating] controlPoint1:[_l3ControlPointView usePresentationLayerIfPossible:_animating] controlPoint2:[_l2ControlPointView usePresentationLayerIfPossible:_animating]];
    [bezierPath addCurveToPoint:[_r1ControlPointView usePresentationLayerIfPossible:_animating] controlPoint1:[_cControlPointView usePresentationLayerIfPossible:_animating] controlPoint2:[_r1ControlPointView usePresentationLayerIfPossible:_animating]];
    [bezierPath addCurveToPoint:[_r3ControlPointView usePresentationLayerIfPossible:_animating] controlPoint1:[_r1ControlPointView usePresentationLayerIfPossible:_animating] controlPoint2:[_r2ControlPointView usePresentationLayerIfPossible:_animating]];
    [bezierPath addLineToPoint:CGPointMake(width, 0.0)];
    [bezierPath closePath];
    
    return bezierPath.CGPath;
}

        由于接下来需要使用CADisplayLink的selector(),那么就需要shapelayer被更新,更新shapelayer的路径(path)。

- (void) updateShapeLayer
{
    _shapeLayer.path = [self currentPath];
}

        four part

       之前创建了多个控制点,那么当拖动的时候这些控制点位置也是改变的,就需要去布局拖动后各个控制点。代码如下:

- (void) layoutControlPoints:(CGFloat)baseHeight waveHeight:(CGFloat)waveHeight locationX:(CGFloat)locationX
{
    CGFloat width = self.view.bounds.size.width;
    CGFloat minLeftX = MIN((locationX - width / 2.0) * 0.28, 0);
    CGFloat maxRightX = MAX(width + (locationX - width / 2.0) * 0.28, width);
    
    CGFloat leftPartWidth = locationX - minLeftX;
    CGFloat rightPartWidth = maxRightX - locationX;
    
    _l3ControlPointView.center = CGPointMake(minLeftX, baseHeight);
    _l2ControlPointView.center = CGPointMake(minLeftX + leftPartWidth*0.44, baseHeight);
    _l1ControlPointView.center = CGPointMake(minLeftX + leftPartWidth*0.71, baseHeight + waveHeight*0.64);
    _cControlPointView.center = CGPointMake(locationX, baseHeight + waveHeight*1.36);
    _r1ControlPointView.center = CGPointMake(maxRightX - rightPartWidth*0.71, baseHeight + waveHeight*0.64);
    _r2ControlPointView.center = CGPointMake(maxRightX - (rightPartWidth*0.44), baseHeight);
    _r3ControlPointView.center = CGPointMake(maxRightX, baseHeight);
}

        解释:

      baseHeight:baseHeight + waveHeight = 全高

      waveHeight:曲线的高度

      locationX:手指滑动点的X坐标

      width:视图的宽度

      minLeftX:定义l3视图的position坐标的X最小值,这个值可以小于0

      maxRigthX:定义r3视图的position坐标的X最小值

      leftPartWith:locationX和minLeftX的距离

      rightPartWidth:maxRightX和locationX的距离
      这里涉及参数比较多。


      five part

      更新panGestureDidMove方法,当拖动时需要更新各个点的位置,代码如下:

- (void) panGestureDidMove:(UIPanGestureRecognizer *)pan
{
    if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateFailed || pan.state == UIGestureRecognizerStateCancelled) {
        
        CGFloat centerY = _minimalHeight;
        _animating = YES;
        
    }
    else
    {
        CGFloat maxF = [pan translationInView:self.view].y;
        CGFloat additionalHeight = MAX(maxF, 0);
        NSLog(@"=========%f",maxF);
        
        CGFloat waveHeight = MIN(additionalHeight * 0.6, _maxWaveHeight);
        CGFloat baseHeight = _minimalHeight + additionalHeight - waveHeight;
        
        CGFloat locationX = [pan translationInView:self.view].x + pan.view.center.x;  //视图上手指滑动的x坐标,也就是波浪的顶部
        NSLog(@"+++++++%f",locationX);
        
        if (maxF > 150) {
            
        }
        else
        {
            [self layoutControlPoints:baseHeight waveHeight:waveHeight locationX:locationX];
            [self updateShapeLayer];
        }
    }
}

       six part

       接下来需要计算初始的高度、波浪的高度、手指移动的坐标,那么就需要调用之前创建的方法;一个是布局控制点的layoutControlPoints方法和更新shapeLayer图层路径的updateShapeLayer方法。在viewDidLoad:方法最下面添加如下方法:

    [self layoutControlPoints:_minimalHeight waveHeight:0.0 locationX:self.view.bounds.size.width/2];
    [self updateShapeLayer];

        再将shapeLayer.backgroundColor替换成shapeLayer.fillColor 

    _shapeLayer.fillColor = [UIColor colorWithRed:91/255.0 green:144/255.0 blue:212/255.0 alpha:1.0].CGColor;

        如果一切没有问题,运行程序,有点效果出来了,可是当你松开手指的时候,并没有回弹,那么就需要之前提到的displayLink了。初始化displayLink,将其运行在mainRunLoop中。

_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateShapeLayer)];
    [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

        更改panGestureDidMove:方法,产生弹簧效果

- (void) panGestureDidMove:(UIPanGestureRecognizer *)pan
{
    if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateFailed || pan.state == UIGestureRecognizerStateCancelled) {
        
        CGFloat centerY = _minimalHeight;
        _animating = YES;
        
        [UIView animateWithDuration:0.9 delay:0.0 usingSpringWithDamping:0.57 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
            CGPoint pointl3 = _l3ControlPointView.center;
            pointl3.y = centerY;
            _l3ControlPointView.center = pointl3;
            CGPoint pointl2 = _l2ControlPointView.center;
            pointl2.y = centerY;
            _l2ControlPointView.center = pointl2;
            CGPoint pointl1 = _l1ControlPointView.center;
            pointl1.y = centerY;
            _l1ControlPointView.center = pointl1;
            CGPoint pointc = _cControlPointView.center;
            pointc.y = centerY;
            _cControlPointView.center = pointc;
            CGPoint pointr1 = _r1ControlPointView.center;
            pointr1.y = centerY;
            _r1ControlPointView.center = pointr1;
            CGPoint pointr2 = _r2ControlPointView.center;
            pointr2.y = centerY;
            _r2ControlPointView.center = pointr2;
            CGPoint pointr3 = _r3ControlPointView.center;
            pointr3.y = centerY;
            _r3ControlPointView.center = pointr3;
            
        } completion:^(BOOL finished) {
            _animating = NO;
        }];
    }
    else
    {
        CGFloat maxF = [pan translationInView:self.view].y;
        CGFloat additionalHeight = MAX(maxF, 0);
        NSLog(@"=========%f",maxF);
        
        CGFloat waveHeight = MIN(additionalHeight * 0.6, _maxWaveHeight);
        CGFloat baseHeight = _minimalHeight + additionalHeight - waveHeight;
        
        CGFloat locationX = [pan translationInView:self.view].x + pan.view.center.x;  //视图上手指滑动的x坐标,也就是波浪的顶部
        NSLog(@"+++++++%f",locationX);
        
        if (maxF > 150) {
            
        }
        else
        {
            [self layoutControlPoints:baseHeight waveHeight:waveHeight locationX:locationX];
            [self updateShapeLayer];
        }
    }
}

          已经添加了弹簧效果,运行程序,将达到如下效果。



      原博客地址:传送门  如有不足,还望指点!

     

 

     

 

 


 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值