iOS-UIBezierPath動畫之果凍動畫

我們今天做一個簡單的貝塞爾曲線動畫,做這個動畫之前,我們要對UIBezierPath有簡單的了解。
貝塞爾曲線基礎知識,可以參考下面文章:
iOS-貝塞爾曲線(UIBezierPath)的使用
iOS-貝塞爾曲線(UIBezierPath)詳解(CAShapeLayer)

效果圖

我們先看效果圖:


動畫效果圖

動畫的幾個關鍵點

ABCDQ點

我們的動畫其實就是ABCDQ,這五個點畫的圖,其中Q點是關鍵點,就是貝塞爾曲線中的控制點。

其中ABCD是不動點,根據(jù)Q點的位置變化,改變圖形,做出動畫效果。

實現(xiàn)

創(chuàng)建必須用的屬性
  1. 創(chuàng)建一個navView視圖,承載動畫layer,作為模擬導航視圖用
  2. 創(chuàng)建一個CAShapeLayer *shapeLayer路徑,畫圖用
  3. 創(chuàng)建一個UIView *controlView視圖,記錄控制點的實時視覺位置。
  4. 記錄控制點的實時位置坐標CGPoint controlPoint
  5. 創(chuàng)建一個定時器CADisplayLink *displayLink,拖拽結束后做動畫使用。(為什么不用NSTimer呢?思考一下,評論區(qū)留言喲~)
  6. 記錄當前是否是在做動畫BOOL isAnimating
  7. 最后創(chuàng)建一個列表tableView
實現(xiàn)思路
  1. 通過KVO觀察controlPoint的位置,因為松手后需要記錄實時的
    controlPoint
    static NSString *const kControlPoint = @"controlPoint";
    [self addObserver:self forKeyPath:kControlPoint options:NSKeyValueObservingOptionNew context:nil];
  1. 實例化CAShapeLayer
    self.navView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kControlMinHeight)];
    [self addSubview:self.navView];
    _shapeLayer = [CAShapeLayer layer];
    _shapeLayer.fillColor = [UIColor colorWithRed:57/255.0 green:67/255.0 blue:89/255.0 alpha:1.0].CGColor;
    [self.navView.layer addSublayer:_shapeLayer];
  1. 創(chuàng)建定時器
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(calculatePath)];
    _displayLink.paused = YES;
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  1. 記錄初始控制點信息
    // Q點坐標
    self.controlPoint = CGPointMake(kScreenWidth/2.0, kControlMinHeight);
    _controlView = [[UIView alloc] initWithFrame:CGRectMake(kScreenWidth/2.0, kControlMinHeight, 3, 3)];
    _controlView.backgroundColor = [UIColor redColor];
    [self addSubview:_controlView];
    _isAnimating = NO;
  1. 實例化tableView
    其中添加手勢是關鍵,代碼如下:
    [self addSubview:self.tableView];
    [self.tableView.panGestureRecognizer addTarget:self action:@selector(handlePanAction:)];

/// 手勢實現(xiàn)
- (void)handlePanAction:(UIPanGestureRecognizer *)pan{
    if (!_isAnimating) { //動畫過程中不處理事件
        if (pan.state == UIGestureRecognizerStateChanged){
            CGPoint point = [pan translationInView:self];
            // 這部分代碼使Q點跟著手勢走
            CGFloat controlHeight = point.y*0.7 + kControlMinHeight;
            CGFloat controlX = kScreenWidth/2.0 + point.x;
            CGFloat controlY = controlHeight > kControlMinHeight ? controlHeight : kControlMinHeight;
            self.controlPoint = CGPointMake(controlX, controlY);
            self.controlView.frame = CGRectMake(controlX, controlY, self.controlView.frame.size.width, self.controlView.frame.size.height);
        }else if (pan.state == UIGestureRecognizerStateCancelled ||
                  pan.state == UIGestureRecognizerStateEnded ||
                  pan.state == UIGestureRecognizerStateFailed){
            
            //手勢結束,_shapeLayer昌盛產(chǎn)生彈簧效果
            _isAnimating = YES;
            _displayLink.paused = NO;           //開啟displaylink,會執(zhí)行方法calculatePath.
            //彈簧
            [UIView animateWithDuration:1 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
                self.controlView.frame = CGRectMake(kScreenWidth/2.0, kControlMinHeight, 3, 3);
            } completion:^(BOOL finished) {
                if(finished){
                    self.displayLink.paused = YES;
                    self.isAnimating = NO;
                }
            }];
        }
    }
}
  1. KVO
//KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kControlPoint]) {
        [self updateShapeLayerPath];
    }
}
//更新貝塞爾曲線圖
- (void)updateShapeLayerPath {
    // 更新_shapeLayer形狀
    UIBezierPath *tPath = [UIBezierPath bezierPath];
    [tPath moveToPoint:CGPointMake(0, 0)];                              // A點
    [tPath addLineToPoint:CGPointMake(kScreenWidth, 0)];               // B點
    [tPath addLineToPoint:CGPointMake(kScreenWidth,  kControlMinHeight)];  // D點
    [tPath addQuadCurveToPoint:CGPointMake(0, kControlMinHeight) controlPoint:self.controlPoint]; // C,D,Q確定的一個弧線
    [tPath closePath];
    _shapeLayer.path = tPath.CGPath;
}

注意點:在拖拽手勢結束前,將定時器暫停掉。
拖拽手勢結束后,打開定時器。做阻尼動畫。
阻尼動畫可以使用系統(tǒng)的方法:

     //彈簧
       [UIView animateWithDuration:1 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
           self.controlView.frame = CGRectMake(kScreenWidth/2.0, kControlMinHeight, 3, 3);
       } completion:^(BOOL finished) {
           if(finished){
               self.displayLink.paused = YES;
               self.isAnimating = NO;
          }
       }];

另外:手勢結束相關代碼,也可以寫在這里

/// 接收拖動代碼也可以寫在這里
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    
}
外部調(diào)用方法
 JJCuteView *cuteView = [[JJCuteView alloc] initWithFrame:CGRectMake(0, 100, 320, kScreenHeight-100)];
 cuteView.backgroundColor = [UIColor whiteColor];
 [self.view addSubview:cuteView];
全部代碼:

JJCuteView.h

///  果凍動畫,QQ彈
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface JJCuteView : UIView

@end

NS_ASSUME_NONNULL_END

JJCuteView.m

//
//  JJCuteView.m
//  iOS_Tools
//
//  Created by 播唄網(wǎng)絡 on 2020/11/30.
//  Copyright ? 2020 播唄網(wǎng)絡. All rights reserved.
//

#import "JJCuteView.h"


#define kControlMinHeight 100
@interface JJCuteView ()<UITableViewDelegate,UITableViewDataSource>

/// 模擬導航視圖
@property (nonatomic, strong) UIView *navView;
/// 路徑
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
/// 曲線路徑控制點,為了更容易理解添加的. // 切點,用Q表示
@property (nonatomic, strong) UIView *controlView;
/// 切點位置
@property (nonatomic, assign) CGPoint controlPoint;
/// 定時器,為了做動畫用
@property (nonatomic, strong) CADisplayLink *displayLink;
/// 記錄當前是否在做動畫
@property (nonatomic, assign) BOOL isAnimating;
/// 列表
@property (nonatomic, strong) JJTableView *tableView;

@end

@implementation JJCuteView

static NSString *const kControlPoint = @"controlPoint";

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupUI];
    }
    return self;
}


#pragma mark - 初始化界面
- (void)setupUI{
    
    [self addObserver:self forKeyPath:kControlPoint options:NSKeyValueObservingOptionNew context:nil];
    
    // 手勢
    // UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanAction:)];
    // self.userInteractionEnabled = YES;
    // [self addGestureRecognizer:pan];
    
    [self addSubview:self.tableView];
    [self.tableView.panGestureRecognizer addTarget:self action:@selector(handlePanAction:)];
    
    self.navView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kControlMinHeight)];
    [self addSubview:self.navView];
    _shapeLayer = [CAShapeLayer layer];
    _shapeLayer.fillColor = [UIColor colorWithRed:57/255.0 green:67/255.0 blue:89/255.0 alpha:1.0].CGColor;
    [self.navView.layer addSublayer:_shapeLayer];
    
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(calculatePath)];
    _displayLink.paused = YES;
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    
    // Q點坐標
    self.controlPoint = CGPointMake(kScreenWidth/2.0, kControlMinHeight);
    _controlView = [[UIView alloc] initWithFrame:CGRectMake(kScreenWidth/2.0, kControlMinHeight, 3, 3)];
    _controlView.backgroundColor = [UIColor redColor];
    [self addSubview:_controlView];
    
    _isAnimating = NO;
}

- (void)handlePanAction:(UIPanGestureRecognizer *)pan{
    if (!_isAnimating) { //動畫過程中不處理事件
        if (pan.state == UIGestureRecognizerStateChanged){
            CGPoint point = [pan translationInView:self];
            // 這部分代碼使Q點跟著手勢走
            CGFloat controlHeight = point.y*0.7 + kControlMinHeight;
            CGFloat controlX = kScreenWidth/2.0 + point.x;
            CGFloat controlY = controlHeight > kControlMinHeight ? controlHeight : kControlMinHeight;
            self.controlPoint = CGPointMake(controlX, controlY);
            self.controlView.frame = CGRectMake(controlX, controlY, self.controlView.frame.size.width, self.controlView.frame.size.height);
        }else if (pan.state == UIGestureRecognizerStateCancelled ||
                  pan.state == UIGestureRecognizerStateEnded ||
                  pan.state == UIGestureRecognizerStateFailed){
            
            //手勢結束,_shapeLayer昌盛產(chǎn)生彈簧效果
            _isAnimating = YES;
            _displayLink.paused = NO;           //開啟displaylink,會執(zhí)行方法calculatePath.
            //彈簧
            [UIView animateWithDuration:1 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
                self.controlView.frame = CGRectMake(kScreenWidth/2.0, kControlMinHeight, 3, 3);
            } completion:^(BOOL finished) {
                if(finished){
                    self.displayLink.paused = YES;
                    self.isAnimating = NO;
                }
            }];
            
            
        }
    }
}

/// 接收拖動代碼也可以寫在這里
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    
}

//更新貝塞爾曲線圖
- (void)updateShapeLayerPath {
    // 更新_shapeLayer形狀
    UIBezierPath *tPath = [UIBezierPath bezierPath];
    [tPath moveToPoint:CGPointMake(0, 0)];                              // A點
    [tPath addLineToPoint:CGPointMake(kScreenWidth, 0)];               // B點
    [tPath addLineToPoint:CGPointMake(kScreenWidth,  kControlMinHeight)];  // D點
    [tPath addQuadCurveToPoint:CGPointMake(0, kControlMinHeight) controlPoint:self.controlPoint]; // C,D,Q確定的一個弧線
    [tPath closePath];
    _shapeLayer.path = tPath.CGPath;
}

//KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kControlPoint]) {
        [self updateShapeLayerPath];
    }
}

- (void)calculatePath{
    // 由于手勢結束時,Q執(zhí)行了一個UIView的彈簧動畫,把這個過程的坐標記錄下來,并相應的畫出_shapeLayer形狀
    CALayer *layer = self.controlView.layer.presentationLayer;
    self.controlPoint = CGPointMake(layer.position.x, layer.position.y);
}

#pragma mark -- TableView data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 6;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    return cell;
}

#pragma mark - lazy
- (JJTableView *)tableView{
    if (_tableView == nil) {
        _tableView = [[JJTableView alloc] initWithFrame:CGRectMake(0, kControlMinHeight, kScreenWidth, kScreenHeight-kControlMinHeight)];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        _tableView.backgroundColor = [UIColor whiteColor];
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
    }
    return _tableView;
}
@end

總結:

上面就是全部的代碼,注釋寫的也挺詳細的。
實現(xiàn)過程參考了文章iOS - 用UIBezierPath實現(xiàn)果凍效果

基本上貝塞爾曲線相關的知識點就到這里了。
其他文章:
iOS-貝塞爾曲線(UIBezierPath)的使用
iOS-貝塞爾曲線(UIBezierPath)詳解(CAShapeLayer)

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容