关键帧动画实现围绕路径移动汽车
如果我们想让一个物体沿着一个轨迹移动,就需要用到UIBezierPath
。使用UIBezierPath
的两个关键点
-
数据点
(起点终点)、控制点
- 为了显示贝塞尔曲线的路径,需要用到专门的图层
CAShapeLayer
下面使用UIBezierPath
、CAShapeLayer
实现一个汽车围绕路径移动的动画
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test{
//1.定义贝塞尔曲线
UIBezierPath *path = [UIBezierPath bezierPath];
//使用方法moveToPoint:去设置初始线段的起点
[path moveToPoint:CGPointMake(20, 200)];
//设置EndPoint & Control Point(终点和控制点)
[path addCurveToPoint:CGPointMake(300, 200) controlPoint1:CGPointMake(100, 100) controlPoint2:CGPointMake(200, 300)];
//2.为了显示贝塞尔曲线的路径 -> CAShapeLayer专门用来显示UIBezierPath图层
//CAShapeLayer 使用shapeLayer 可以更高效的渲染图形.并且不使用drawRect方法
CAShapeLayer *shapeLyaer = [CAShapeLayer layer];
//路径
shapeLyaer.path = path.CGPath;
//填充颜色
//shapeLyaer.fillColor = [UIColor blueColor].CGColor;
shapeLyaer.fillColor = nil;
//线段颜色
shapeLyaer.strokeColor = [UIColor redColor].CGColor;
//为子图层添加贝塞尔曲线图
[self.view.layer addSublayer:shapeLyaer];
//3.添加🚗图层
CALayer *carLayer = [CALayer layer];
carLayer.frame = CGRectMake(15, 200-18, 36, 36);
//寄宿图
carLayer.contents = (id)[UIImage imageNamed:@"car"].CGImage;
carLayer.anchorPoint = CGPointMake(0.5, 0.8);
[self.view.layer addSublayer:carLayer];
//4.创建关键帧动画,让汽车沿着路径移动
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
//路径
anim.keyPath = @"position";
//path
anim.path = path.CGPath;
//时长
anim.duration = 4.0;
//rotationMode
anim.rotationMode = kCAAnimationRotateAuto;
//为汽车图层添加动画
[carLayer addAnimation:anim forKey:nil];
}
@end
物理引擎的简单使用
物理引擎是由UIKit
提供的,负责了动画和交互的体系。UIDynamic
引擎是iOS7.o引入的的技术,其目的是解放开发者,可以远离物理公式
。
UIDynamic
物理引擎重要的类
- 重力行为:
UIGravityBehavior
- 碰撞行为:
UICollisionBehavior
- 捕捉行为:
UISnapBehavior
- 推动行为:
UIPushBehavior
- 附着行为:
UIAttchmentBehavior
- 动力元素行为:
UIDynamicItemBehavior
使用2D物理引擎
的两个步骤
- 添加行为,需要绑定
view
- 将行为添加到容器中
下面我们来使用一些常用的物理引擎,代码如下
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)UIDynamicAnimator * animator;
@property(nonatomic,strong)UIDynamicAnimator * animator2;
@property(nonatomic,strong)UIAttachmentBehavior * attachmentBehavior;
@property(nonatomic,strong)UIImageView * redView;
@property(nonatomic,strong)UIImageView * greenView;
@property(nonatomic,strong)UIImageView * yellowView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//UI实现
self.view.backgroundColor = [UIColor whiteColor];
//红色球
_redView =[[UIImageView alloc]initWithFrame:CGRectMake(100, 200, 50, 50)];
_redView.image = [UIImage imageNamed:@"ball.png"];
_redView.userInteractionEnabled = YES;
_redView.backgroundColor =[UIColor redColor];
_redView.layer.masksToBounds = YES;
_redView.layer.cornerRadius = 25;
[self.view addSubview:_redView];
//绿色球
_greenView =[[UIImageView alloc]initWithFrame:CGRectMake(100, 400, 50, 50)];
_greenView.backgroundColor =[UIColor greenColor];
_greenView.image = [UIImage imageNamed:@"ball2.png"];
_greenView.userInteractionEnabled = YES;
_greenView.layer.masksToBounds = YES;
_greenView.layer.cornerRadius = 25;
[self.view addSubview:_greenView];
//黄色球
_yellowView =[[UIImageView alloc]initWithFrame:CGRectMake(200, 500, 50, 50)];
_yellowView.backgroundColor =[UIColor yellowColor];
_yellowView.image = [UIImage imageNamed:@"ball3.png"];
_yellowView.userInteractionEnabled = YES;
_yellowView.layer.masksToBounds = YES;
_yellowView.layer.cornerRadius = 25;
[self.view addSubview:_yellowView];
//物理引擎
[self animator];
//创建自由落体行为-重力,初始化哪几个view要接收这样的重力行为
UIGravityBehavior *gravity = [[UIGravityBehavior alloc]initWithItems:@[_redView,_yellowView,_greenView]];
//重力行为有一个属性是重力加速度,设置越大速度增长越快。默认是1
gravity.magnitude = 2;
//添加到容器
[_animator addBehavior:gravity];
//碰撞行为
UICollisionBehavior *collision =[[UICollisionBehavior alloc]initWithItems:@[_redView,_yellowView,_greenView]];
//设置边缘(父View的bounds)
collision.translatesReferenceBoundsIntoBoundary = YES;
//可以利用贝塞尔曲线限制边界
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0,150, self.view.frame.size.width, self.view.frame.size.width)];
CAShapeLayer * shapeLayer =[CAShapeLayer layer];
shapeLayer.path =path.CGPath;
//画笔颜色
shapeLayer.strokeColor =[UIColor redColor].CGColor;
shapeLayer.lineWidth = 5;
//填充颜色
shapeLayer.fillColor = nil;
[self.view.layer addSublayer:shapeLayer];
[collision addBoundaryWithIdentifier:@"circle" forPath:path];
[_animator addBehavior:collision];
//模拟捕捉行为
//捕捉行为需要在创建时就给与一个点
//捕捉行为有一个防震系数属性,设置的越大,振幅就越小
CGPoint point = CGPointMake(10, 400);
UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:_greenView snapToPoint:point];
snap.damping = 1;
[_animator addBehavior:snap];
//其他行为的拓展
UIDynamicItemBehavior *itemBehavior =[[UIDynamicItemBehavior alloc]initWithItems:@[_redView]];
/*
elasticity 弹性系数
friction 摩擦系数
density 密度
resistance 抵抗性
angularResistance 角度阻力
charge 冲击
anchored 锚定
allowsRotation 允许旋转
*/
itemBehavior.elasticity =.6;//弹性系数
[_animator addBehavior:itemBehavior];
//添加手势
UIPanGestureRecognizer *pan =[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAuction:)];
[_redView addGestureRecognizer:pan];
}
-(void)panAuction:(UIPanGestureRecognizer *)ges{
if (ges.state == UIGestureRecognizerStateBegan) {
UIOffset offset = UIOffsetMake(-10, -10);
/*
offsetFromCenter:偏离中心幅度
attachedToAnchor:附加到锚点 手势点击的位置
*/
//UIAttachmentBehavior 附着行为
_attachmentBehavior =[[UIAttachmentBehavior alloc]initWithItem:_redView offsetFromCenter:offset attachedToAnchor:[ges locationInView:self.view]];
[_animator addBehavior:_attachmentBehavior];
}else if (ges.state == UIGestureRecognizerStateChanged){
//设置锚点
[_attachmentBehavior setAnchorPoint:[ges locationInView:self.view]];
}else if (ges.state ==UIGestureRecognizerStateEnded || ges.state == UIGestureRecognizerStateFailed || ges.state == UIGestureRecognizerStateCancelled){
[_animator removeBehavior:_attachmentBehavior];
}
}
//懒加载
- (UIDynamicAnimator *)animator
{
if (!_animator) {
// 创建一个物理仿真器,关联到一个view上
//容器(里面放一些行为)
/*
ReferenceView:关联的view
*/
_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
return _animator;
}
@end
足球
从上面掉落下来把篮球
撞击上去,同时我们给_redView
足球添加了一个拖拽手势,可以把足球
拖上去使其掉下来再次撞击篮球
。
下面我们给篮球
添加一个捕捉行为,点击屏幕随意更改其位置
- (void)viewDidLoad {
[super viewDidLoad];
_animator2 = [[UIDynamicAnimator alloc]initWithReferenceView:self.view];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 1.获得手指对应的触摸对象
UITouch *touch = [touches anyObject];
// 2.获得触摸点
CGPoint point = [touch locationInView:self.view];
// 3.创建捕捉行为
UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:_yellowView snapToPoint:point];
// 防震系数,damping越大,振幅越小
snap.damping = 1;
// 4.清空之前的并再次开始
[_animator2 removeAllBehaviors];
[_animator2 addBehavior:snap];
}
贝塞尔曲线
贝塞尔曲线
(Bézier curve),又称⻉兹曲线
或贝济埃曲线
,是应⽤于⼆维图形应用程序的数学曲线。一 般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成
,节点是可拖动的支点,线段像可伸缩的⽪筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在⼀些比较成熟的位图软件中也有贝塞尔曲线工具,如PhotoShop
等。
贝塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用⻉塞尔曲线来为汽⻋的主体进行设计。⻉塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau演算法开发,以稳定数值的方法求出贝兹曲线。
UIBezierPath
属性与方法
- 初始化方法
初始化方法,需要用实例方法添加线条。使用比较多,可以根据需要任意定制样式,画任何我们想画的图形。
+ (instancetype)bezierPath;
返回一个矩形path
+ (instancetype)bezierPathWithRect:(CGRect)rect;
返回一个圆形或者椭圆形path
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
返回一个带圆角的矩形path,矩形的四个角都是圆角
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
返回一个带圆角的矩形path,UIRectCorner 枚举值可以设置只绘制某个圆角
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
返回一段圆弧,参数说明:center:弧线中心点的坐标 radius:弧线所在圆的半径 startAngle:弧线开始的角度值 endAngle:弧线结束的角度值 clockwise:是否顺时针画弧线
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
用一条 CGpath 初始化
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
返回一个反转当前路径的路径对象(反方向绘制path)
- (UIBezierPath *)bezierPathByReversingPath API_AVAILABLE(ios(6.0));
属性
CGPath:将UIBezierPath类转换成CGPath
currentPoint:当前path的位置,可以理解为path的终点
lineWidth:线条宽度
lineCapStyle:端点样式
lineJoinStyle:连接类型
flatness:绘线的精细程度,默认为0.6,数值越大,需要处理的时间越长
usesEvenOddFillRule:判断奇偶数组的规则绘制图像,图形复杂时填充颜色的一种规则。类似棋盘
miterLimit:最大斜接长度 (只有在使用kCGLineJoinMiter时才有效,最大限制为10) ,边角的角度越小,斜接长度就会越大,为了避免斜接长度过长,使用lineLimit属性限制,如果斜接长度超过miterLimit,边角就会以KCALineJoinBevel类型来显示
- setLineDash: count: phase: 为path绘制虚线,dash数组存放各段虚线的长度,count是数组元素数量,phase是起始位置
lineCapStyle - 端点类型
kCGLineCapButt:无端点
kCGLineCapRound:圆形端点
kCGLineCapSquare:方形端点(样式上和kCGLineCapButt是一样的,但是比kCGLineCapButt长一点)
lineJoinStyle - 交叉点的类型
kCGLineJoinMiter:尖角衔接
kCGLineJoinRound:圆角衔接
kCGLineJoinBevel:斜角衔接
UIBezierPath构建Path
以point点开始作为起点,一般用 + (instancetype)bezierPath 创建的贝塞尔曲线,先用该方法标注一个起点,再调用其他的创建线条的方法来绘制曲线
- (void)moveToPoint:(CGPoint)point;
绘制二次贝塞尔曲线的关键方法,即从path的最后一点开始添加一条线到point点
- (void)addLineToPoint:(CGPoint)point;
绘制二次贝塞尔曲线的关键方法,和 -moveToPoint: 配合使用。endPoint为终止点,controlPoint为控制点
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
绘制三次贝塞尔曲线的关键方法,以三个点画一段曲线。一般和 moveToPoint: 配合使用。
其中起始点由 -moveToPoint: 设置,终止点为 endPoint:,控制点1的坐标controlPoint1,控制点2的坐标是controlPoint2。
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
绘制一段圆弧,center: 原点坐标 radius: 半径 startAngle: 起始角度 endAngle: 终止角度 clockwise 顺时针/逆时针方向绘制
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
闭合线
- (void)closePath;
移除所有的点,从而有效的删除所有子路径
- (void)removeAllPoints;
追加指定的bezierPath到路径上
- (void)appendPath:(UIBezierPath *)bezierPath;
用仿射变换矩阵变换路径的所有点
- (void)applyTransform:(CGAffineTransform)transform;
图形上下文中的路径操作
填充路径
- (void)fill;
各个点连线
- (void)stroke;
填充模式,alpha 设置
blendMode : https://onevcat.com/2013/04/using-blending-in-ios/
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
链接模式,alpha设置
- (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
图形绘制超出当前路径范围,则不可见
- (void)addClip;
QQ消息提示按钮实现
下面我们使用UIBezierPath
实现一个QQ消息提示按钮的拖拽效果
拆分动画:
- 2个圆(一个固定圆,一个拖拽圆)
- 贝塞尔曲线,求得关键点.
- 固定圆比例缩小
- 拖拽到一定距离的时候需要断开
- 断开之后有个圆的反弹效果bgg
实现思路拆分步骤(根据两个圆求得关键点)
注意:计算A点坐标的时候,由于屏幕左上角是原点(0, 0)
,所以要用(x1, y1)
坐标分别减去对应的间距。
#import "ViewController.h"
@interface ViewController ()
//圆1
@property (nonatomic, strong) UIView *view1;
//圆2
@property (nonatomic, strong) UIView *view2;
//shapeLayer图层
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
//坐标记录
@property (nonatomic, assign) CGPoint oldViewCenter;
@property (nonatomic, assign) CGRect oldViewFrame;
@property (nonatomic, assign) CGFloat r1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1.UI.
[self setUp];
}
-(void)setUp
{
//添加view1
_view1 = [[UIView alloc] initWithFrame:CGRectMake(36, CGRectGetHeight(self.view.bounds)-66, 40, 40)];
_view1.layer.cornerRadius = 20;
_view1.backgroundColor = [UIColor redColor];
[self.view addSubview:_view1];
//添加view2
_view2 = [[UIView alloc] initWithFrame:_view1.frame];
_view2.layer.cornerRadius = 20;
_view2.backgroundColor = [UIColor redColor];
[self.view addSubview:_view2];
//添加label
UILabel *numL = [[UILabel alloc] initWithFrame:_view2.bounds];
numL.text = @"99";
numL.textAlignment = NSTextAlignmentCenter;
numL.textColor = [UIColor whiteColor];
[_view2 addSubview:numL];
//添加手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[_view2 addGestureRecognizer:pan];
//初始化layer
_shapeLayer = [CAShapeLayer layer];
_oldViewFrame = _view1.frame;
_oldViewCenter = _view1.center;
_r1 = CGRectGetWidth(_view1.frame)/2;
}
-(void)panAction:(UIPanGestureRecognizer *)ges{
if (ges.state == UIGestureRecognizerStateChanged) {
//1.view2跟着手指移动 --执行案例
_view2.center = [ges locationInView:self.view];
//当拖拽到一定距离之后,则移出来
//那么就是r1半径缩减到一定距离
//在caculPoint 方法中,更新r1值
//再拖拽if中判断,如果低于9
if (_r1 < 9) {
_view1.hidden = YES;
[_shapeLayer removeFromSuperlayer];
}
//2.计算6个关键点,并画出贝塞尔曲线
[self caculPoint];
}else if(ges.state == UIGestureRecognizerStateEnded
|| ges.state == UIGestureRecognizerStateFailed
|| ges.state == UIGestureRecognizerStateCancelled)
{
//思考: 回弹时,那些属性/组件要做调整
//view2 位置恢复,shaperLayer消失,
// [_shapeLayer removeFromSuperlayer];
// _view2.center= _oldViewCenter;
[_shapeLayer removeFromSuperlayer];
//加上弹跳效果动画
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
weakSelf.view2.center = weakSelf.oldViewCenter;
} completion:^(BOOL finished) {
weakSelf.view1.hidden = NO;
weakSelf.view1.frame = weakSelf.oldViewFrame;
weakSelf.r1 = weakSelf.oldViewFrame.size.width/2;
weakSelf.view1.layer.cornerRadius = weakSelf.r1;
}];
}
}
-(void)caculPoint{
//1.初始化已知顶点
CGPoint center1 = _view1.center;
CGPoint center2 = _view2.center;
//2.计算出斜边d的长度(根据勾股定理)
//d= √((x2-x1)•(x2-x1) + (y1-y2)•(y1-y2));
CGFloat dis = sqrtf(pow((center2.x-center1.x), 2)+pow(center1.y-center2.y, 2));
//3.计算sin(正弦),cos(余弦)数据
CGFloat sinValue = (center2.x - center1.x)/dis;
CGFloat cosValue = (center1.y - center2.y)/dis;
//4.半径
CGFloat r1 = CGRectGetWidth(_oldViewFrame)/2 - dis/20;
CGFloat r2 = CGRectGetHeight(_view2.bounds)/2;
//更新_r1值
_r1 = r1;
//5.计算6个关键点
CGPoint pA = CGPointMake(center1.x - r1 * cosValue, center1.y - r1 * sinValue);
CGPoint pB = CGPointMake(center1.x + r1 * cosValue, center1.y + r1 * sinValue);
CGPoint pC = CGPointMake(center2.x + r2 * cosValue, center2.y + r2 * sinValue);
CGPoint pD = CGPointMake(center2.x - r2 * cosValue, center2.y - r2 * sinValue);
CGPoint pO = CGPointMake(pA.x + dis/2*sinValue, pA.y - dis/2*cosValue);
CGPoint pP = CGPointMake(pB.x + dis/2*sinValue, pB.y - dis/2*cosValue);
//6.绘制贝塞尔曲线
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:pA];
[path addQuadCurveToPoint:pD controlPoint:pO];
[path addLineToPoint:pC];
[path addQuadCurveToPoint:pB controlPoint:pP];
[path closePath];
//7.把路径添加layer
//这些写不可以,因为每一次拖拽就会新建一个CAShapeLayer
/*
CAShapeLayer *sh = [[CAShapeLayer alloc]init];
sh.path = path.CGPath;
sh.fillColor = [UIColor redColor].CGColor;
//8.将layer添加到view上--执行代码
[self.view.layer insertSublayer:sh above:_view2.layer];
*/
if (_view1.hidden) {
return;
}
_shapeLayer.path = path.CGPath;
_shapeLayer.fillColor = [UIColor redColor].CGColor;
[self.view.layer insertSublayer:_shapeLayer below:_view2.layer];
//8.重新计算view1的位置---执行代码.
_view1.center = _oldViewCenter;
_view1.bounds = CGRectMake(0, 0, r1*2, r1*2);
_view1.layer.cornerRadius = r1;
//发生错误! 修改r1的计算才能正常显示
//CGFloat r1 = CGRectGetWidth(_oldViewFrame)/2 - dis/20;
}
@end