我們今天做一個簡單的貝塞爾曲線動畫碉熄,做這個動畫之前腹忽,我們要對UIBezierPath有簡單的了解。
貝塞爾曲線基礎知識,可以參考下面文章:
iOS-貝塞爾曲線(UIBezierPath)的使用
iOS-貝塞爾曲線(UIBezierPath)詳解(CAShapeLayer)
效果圖
我們先看效果圖:
動畫的幾個關鍵點
我們的動畫其實就是ABCDQ届宠,這五個點畫的圖,其中Q點是關鍵點乘粒,就是貝塞爾曲線中的控制點豌注。
其中ABCD是不動點,根據(jù)Q點的位置變化灯萍,改變圖形轧铁,做出動畫效果。
實現(xiàn)
創(chuàng)建必須用的屬性
- 創(chuàng)建一個
navView
視圖旦棉,承載動畫layer齿风,作為模擬導航視圖用 - 創(chuàng)建一個
CAShapeLayer *shapeLayer
路徑药薯,畫圖用 - 創(chuàng)建一個
UIView *controlView
視圖,記錄控制點的實時視覺位置救斑。 - 記錄控制點的實時位置坐標
CGPoint controlPoint
- 創(chuàng)建一個定時器
CADisplayLink *displayLink
童本,拖拽結(jié)束后做動畫使用。(為什么不用NSTimer呢脸候?思考一下穷娱,評論區(qū)留言喲~) - 記錄當前是否是在做動畫
BOOL isAnimating
- 最后創(chuàng)建一個列表
tableView
實現(xiàn)思路
- 通過KVO觀察controlPoint的位置,因為松手后需要記錄實時的
controlPoint
static NSString *const kControlPoint = @"controlPoint";
[self addObserver:self forKeyPath:kControlPoint options:NSKeyValueObservingOptionNew context:nil];
- 實例化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];
- 創(chuàng)建定時器
_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;
- 實例化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;
}
}];
}
}
}
- 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)