利用UIBezierPath實現的橡皮筋動畫效果(OC版)

jopen 8年前發布 | 33K 次閱讀 iOS開發 移動開發

      本教程旨在說明第三方庫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];
        }
    }
}
          已經添加了彈簧效果,運行程序,將達到如下效果。



      原博客地址:傳送門  如有不足,還望指點!

      github:源碼


     






來自: http://blog.csdn.net/w_x_p/article/details/50482815

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!