iOS-UIBezierPath动画之果冻动画

我们今天做一个简单的贝塞尔曲线动画,做这个动画之前,我们要对UIBezierPath有简单的了解。
贝塞尔曲线基础知识,可以参考下面文章:
iOS-贝塞尔曲线(UIBezierPath)的使用
iOS-贝塞尔曲线(UIBezierPath)详解(CAShapeLayer)

效果图

我们先看效果图:


动画效果图

动画的几个关键点

ABCDQ点

我们的动画其实就是ABCDQ,这五个点画的图,其中Q点是关键点,就是贝塞尔曲线中的控制点。

其中ABCD是不动点,根据Q点的位置变化,改变图形,做出动画效果。

实现

创建必须用的属性
  1. 创建一个navView视图,承载动画layer,作为模拟导航视图用
  2. 创建一个CAShapeLayer *shapeLayer路径,画图用
  3. 创建一个UIView *controlView视图,记录控制点的实时视觉位置。
  4. 记录控制点的实时位置坐标CGPoint controlPoint
  5. 创建一个定时器CADisplayLink *displayLink,拖拽结束后做动画使用。(为什么不用NSTimer呢?思考一下,评论区留言哟~)
  6. 记录当前是否是在做动画BOOL isAnimating
  7. 最后创建一个列表tableView
实现思路
  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. 创建定时器
    _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:)];

/// 手势实现
- (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昌盛产生弹簧效果
            _isAnimating = YES;
            _displayLink.paused = NO;           //开启displaylink,会执行方法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 *)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;
}

注意点:在拖拽手势结束前,将定时器暂停掉。
拖拽手势结束后,打开定时器。做阻尼动画。
阻尼动画可以使用系统的方法:

     //弹簧
       [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 {
    
}
外部调用方法
 JJCuteView *cuteView = [[JJCuteView alloc] initWithFrame:CGRectMake(0, 100, 320, kScreenHeight-100)];
 cuteView.backgroundColor = [UIColor whiteColor];
 [self.view addSubview:cuteView];
全部代码:

JJCuteView.h

///  果冻动画,QQ弹
#import 

NS_ASSUME_NONNULL_BEGIN

@interface JJCuteView : UIView

@end

NS_ASSUME_NONNULL_END

JJCuteView.m

//
//  JJCuteView.m
//  iOS_Tools
//
//  Created by 播呗网络 on 2020/11/30.
//  Copyright © 2020 播呗网络. All rights reserved.
//

#import "JJCuteView.h"


#define kControlMinHeight 100
@interface JJCuteView ()

/// 模拟导航视图
@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昌盛产生弹簧效果
            _isAnimating = YES;
            _displayLink.paused = NO;           //开启displaylink,会执行方法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 *)change context:(void *)context {
    if ([keyPath isEqualToString:kControlPoint]) {
        [self updateShapeLayerPath];
    }
}

- (void)calculatePath{
    // 由于手势结束时,Q执行了一个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

总结:

上面就是全部的代码,注释写的也挺详细的。
实现过程参考了文章iOS - 用UIBezierPath实现果冻效果

基本上贝塞尔曲线相关的知识点就到这里了。
其他文章:
iOS-贝塞尔曲线(UIBezierPath)的使用
iOS-贝塞尔曲线(UIBezierPath)详解(CAShapeLayer)

你可能感兴趣的:(iOS-UIBezierPath动画之果冻动画)