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童本,拖拽結(jié)束后做動畫使用。(為什么不用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){
            
            //手勢結(jié)束,_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;
}

注意點:在拖拽手勢結(jié)束前泵额,將定時器暫停掉。
拖拽手勢結(jié)束后携添,打開定時器嫁盲。做阻尼動畫。
阻尼動畫可以使用系統(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;
          }
       }];

另外:手勢結(jié)束相關代碼薪寓,也可以寫在這里

/// 接收拖動代碼也可以寫在這里
- (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){
            
            //手勢結(jié)束,_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{
    // 由于手勢結(jié)束時,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

總結(jié):

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

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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锥腻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子母谎,更是在濱河造成了極大的恐慌瘦黑,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奇唤,死亡現(xiàn)場離奇詭異幸斥,居然都是意外死亡,警方通過查閱死者的電腦和手機咬扇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門甲葬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人懈贺,你說我怎么就攤上這事经窖。” “怎么了梭灿?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵画侣,是天一觀的道長。 經(jīng)常有香客問我堡妒,道長配乱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮搬泥,結(jié)果婚禮上桑寨,老公的妹妹穿的比我還像新娘。我一直安慰自己忿檩,他們只是感情好西疤,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著休溶,像睡著了一般代赁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兽掰,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天芭碍,我揣著相機與錄音,去河邊找鬼孽尽。 笑死窖壕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的杉女。 我是一名探鬼主播瞻讽,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熏挎!你這毒婦竟也來了速勇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤坎拐,失蹤者是張志新(化名)和其女友劉穎烦磁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哼勇,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡都伪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了积担。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陨晶。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖帝璧,靈堂內(nèi)的尸體忽然破棺而出先誉,到底是詐尸還是另有隱情,我是刑警寧澤聋溜,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布谆膳,位于F島的核電站叭爱,受9級特大地震影響撮躁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜买雾,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一把曼、第九天 我趴在偏房一處隱蔽的房頂上張望杨帽。 院中可真熱鬧,春花似錦嗤军、人聲如沸注盈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽老客。三九已至,卻和暖如春震叮,著一層夾襖步出監(jiān)牢的瞬間胧砰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工苇瓣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尉间,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓击罪,卻偏偏與公主長得像哲嘲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子媳禁,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容