我们今天做一个简单的贝塞尔曲线动画,做这个动画之前,我们要对UIBezierPath有简单的了解。
贝塞尔曲线基础知识,可以参考下面文章:
iOS-贝塞尔曲线(UIBezierPath)的使用
iOS-贝塞尔曲线(UIBezierPath)详解(CAShapeLayer)
效果图
我们先看效果图:
动画的几个关键点
我们的动画其实就是ABCDQ,这五个点画的图,其中Q点是关键点,就是贝塞尔曲线中的控制点。
其中ABCD是不动点,根据Q点的位置变化,改变图形,做出动画效果。
实现
创建必须用的属性
- 创建一个
navView
视图,承载动画layer,作为模拟导航视图用 - 创建一个
CAShapeLayer *shapeLayer
路径,画图用 - 创建一个
UIView *controlView
视图,记录控制点的实时视觉位置。 - 记录控制点的实时位置坐标
CGPoint controlPoint
- 创建一个定时器
CADisplayLink *displayLink
,拖拽结束后做动画使用。(为什么不用NSTimer呢?思考一下,评论区留言哟~) - 记录当前是否是在做动画
BOOL isAnimating
- 最后创建一个列表
tableView
实现思路
- 通过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];
- 创建定时器
_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:)];
/// 手势实现
- (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;
}
}];
}
}
}
- 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)