目录
- 前言(对UIBezierPath的介绍和描述)
- 基础API (基础使用)
- 手写签名 (项目中用过)
- 简单动效
- 进度条动效(项目中用过)
- 冒泡泡动效(写来玩的,一般也没啥项目会用到)
- 加入购物车动效(可以看看,没准能用到)
- 水波纹动效(以前见过支付宝有这个效果)
- 角标拖动消失动效(仿QQ效果)
前言
(这一部分是装13的对UIBezierPath的介绍和描述,只想看用法的,可以跳过这部分或者直接下载demo查看。)
UIBezierPath位于UIKit框架中,它的核心是数学公式 -- 伯恩斯坦多项式,后来被法国数学家 Paul de Casteljau通过德卡斯特里奥(de Casteljau) 算法进行了图形化,再后来被法国工程师皮埃尔·贝塞尔(Pierre Bézier)用来辅助汽车的车体工业设计,并广为宣传,从而被大众熟知。
一阶贝塞尔曲线
二阶贝塞尔曲线
P0为起始点,P2为终点,P1为曲线弧度的控制点
三阶贝塞尔曲线
P0为起始点,P3为终点,P1和P2为曲线弧度的控制点
在 P0P1,P1P2,P2P3这三段线(灰色线)上 ,连接之后形成线段(绿色线)上再取同样的等比划分点,连接之后形成线段(蓝色线)上取同样的等比划分点,然后连接P0,形成圆滑曲线(即最终的红色线)。
贝塞尔曲线可以有n阶,原理都是这样在形成的线段上不断取等比划分点形成曲线。
(n阶是使用若干个二阶和三阶拼凑出来的)
其实就可以想象一根弹性磁性线,固定两端,磁石牵引磁性线使之出现有弧度,磁石的位置就是控制点的位置,磁石可以有n个。
基础API
使用UIBezierPath一般是重写view的drawRect方法,在drawRect方法里创建图形,如果你是在UIViewController里面直接写,那么可以使用[self.view.layer addSublayer:shapeLayer]
; 或者self.view.layer.mask = shapeLayer
; 来显示。
区别在于addSublayer是直接在当前view上添加一个你绘制的图形,而self.view.layer.mask = shapeLayer;是对你的view进行遮罩。
+ (instancetype)bezierPath;
//画矩形
+ (instancetype)bezierPathWithRect:(CGRect)rect;
//画圆形或椭圆
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
//画带圆角的矩形
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius
/*可以自定义绘制某个角,带圆角的矩形
byRoundingCorners:设置需要圆角的角
cornerRadii:圆角角度 最大角度也就是长宽中小的那个值,超过那个值,你设置再大也就是那样
*/
+ (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创建并返回一个新的UIBezierPath对象
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
//将起始点移动到某个位置
- (void)moveToPoint:(CGPoint)point;
//添加一条线
- (void)addLineToPoint:(CGPoint)point;
//添加一条三阶贝塞尔曲线 controlPoint1、controlPoint2就是两个控制点的位置
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
//添加一条二阶贝塞尔曲线
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
//添加一条圆弧线 center:圆心位置 radius:半径 startAngle:起始弧度 endAngle:结束弧度 clockwise:是否顺时针画圆
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise API_AVAILABLE(ios(4.0));
//使用这个方法可以将起始点和终点连接起来,形成闭合路径,在画图的时候可以用这个方法来代替画最后一根闭合线,但注意是一条直线,如果要要画曲线,还是要手动画
- (void)closePath;
//移除所有点,即清屏
- (void)removeAllPoints;
//拼接一条线在当前线后面
- (void)appendPath:(UIBezierPath *)bezierPath;
//创建并返回一个与当前路径相反的新的贝塞尔路径对象
- (UIBezierPath *)bezierPathByReversingPath API_AVAILABLE(ios(6.0));
//用指定的仿射变换矩阵变换路径的所有点,即按照transform设置转换所有点之后重新形成一条新的路径
- (void)applyTransform:(CGAffineTransform)transform;
// Path info
@property(readonly,getter=isEmpty) BOOL empty;
@property(nonatomic,readonly) CGRect bounds;
@property(nonatomic,readonly) CGPoint currentPoint;
- (BOOL)containsPoint:(CGPoint)point;
// Drawing properties
@property(nonatomic) CGFloat lineWidth;
@property(nonatomic) CGLineCap lineCapStyle;
@property(nonatomic) CGLineJoin lineJoinStyle;
//最大斜接长度 斜接长度指的是在两条线交汇处内角和外角之间的距离。 只有lineJoin属性为kCALineJoinMiter时miterLimit才有效; 边角的角度越小,斜接长度就会越大;如果斜接长度超过 miterLimit的值,边角会以 lineJoin的 "bevel"即kCALineJoinBevel类型来显示
@property(nonatomic) CGFloat miterLimit; // Used when lineJoinStyle is kCGLineJoinMiter
//确定弯曲路径短的绘制精度的因素
@property(nonatomic) CGFloat flatness;
//判断奇偶数组的规则绘制图像,图形复杂时填充颜色的一种规则。类似棋盘
@property(nonatomic) BOOL usesEvenOddFillRule; // Default is NO. When YES, the even-odd fill rule is used for drawing, clipping, and hit testing.
//绘制虚线,dash数组存放各段虚线的长度(线长,空隙长,线长,空隙长这种排布),count是数组元素数量,phase是起始位置
- (void)setLineDash:(nullable const CGFloat *)pattern count:(NSInteger)count phase:(CGFloat)phase;
//检索线型
- (void)getLineDash:(nullable CGFloat *)pattern count:(nullable NSInteger *)count phase:(nullable CGFloat *)phase;
// Path operations on the current graphics context
- (void)fill;
- (void)stroke;
// These methods do not affect the blend mode or alpha of the current graphics context
//用指定的混合模式和透明度值来描绘受接收路径所包围的区域,内部填充
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
//用指定的混合模式和透明度值来描绘受接收路径所包围的区域,内部不填充
- (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
- (void)addClip;
其中绘制圆弧时的角度图如下:
手写签名
效果图如下:
:获取手指触碰屏幕的点,连接那些点。
撤销功能:在绘制路径的时候,把每条路径存到一个数组里,当点击撤销的时候,把数组中的最后一条路径删除,重绘路径。
在demo中有两种绘制方法,本质一样,只是第二种是使用二阶贝塞尔曲线来绘制,这样绘制出来的路径更加圆滑。
(这里只贴第二种方法的核心代码,完整代码请前往demo查看)
//这个方法,是用二阶贝塞尔曲线来绘制圆滑的路径
-(void)pan:(UIPanGestureRecognizer *)pan{
//获取当前点
CGPoint currentPoint = [pan locationInView:self];
CGPoint midPoint = [self getMidPoint:previousPoint withP2:currentPoint];
if (pan.state == UIGestureRecognizerStateBegan) {
//创建贝塞尔路径
self.path = [UIBezierPath bezierPath];
self.path.lineWidth = self.lineWidth;
//设置路径的起点
[self.path moveToPoint:currentPoint];
//保存画出的路径
[self.pathArray addObject:self.path];
}
if (pan.state == UIGestureRecognizerStateChanged) {
//圆滑曲线连接到当前触摸点
[self.path addQuadCurveToPoint:midPoint controlPoint:previousPoint];
}
previousPoint = currentPoint;
//重绘
[self setNeedsDisplay];
}
-(CGPoint)getMidPoint:(CGPoint)p1 withP2:(CGPoint)p2{
return CGPointMake((p1.x + p2.x)/2, (p1.y + p2.y)/2);
}
画进度条
效果图如下:
这里只做圆形的展示,直线的进度条就是把画圆路径换成画直线而已。
:画两段圆弧,一段作为基底,一段为进度条。用
strokeEnd
来设置进度条的比例。
如果需要渐变色,可以使用CAGradientLayer来设置颜色。
简单动效
贝塞尔曲线的动效可以跟定时器,UIViewAnimation,帧动画等等结合在一起,因为贝塞尔曲线的可塑性很强,适当结合可以做出多样的动画。
进度条动效
之前有个项目做过根据数据实时变化进度显示,就是使用上面的画进度条方法加上数据实时推送来实现,demo中采用定时器来实现实时变化效果:冒泡泡动效
:用贝塞尔曲线确定运动路径,使用CAKeyframeAnimation帧动画来实现动效。
//泡泡球
UIView * bubbleView = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 50, 50)];
bubbleView.backgroundColor = [UIColor colorWithHexString:@"#E61717" alpha:0.4];
bubbleView.clipsToBounds = YES;
bubbleView.layer.cornerRadius = 25;
[self.view addSubview:bubbleView];
//创建运动轨迹
UIBezierPath * path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(125, 425)];
[path addCurveToPoint:CGPointMake(50, 150) controlPoint1:CGPointMake(30, 300) controlPoint2:CGPointMake(175, 195)];
[path stroke];
CAShapeLayer * layer = [[CAShapeLayer alloc] init];
layer.path = path.CGPath;
layer.strokeColor = [UIColor blueColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
layer.lineWidth = 3;
[self.view.layer addSublayer:layer];
//帧动画
CAKeyframeAnimation * keyFrameAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
[keyFrameAnimation setDuration:2];
keyFrameAnimation.path = path.CGPath;
//设置为NO,则在动画结束后停在终点,YES则会回到原点
keyFrameAnimation.removedOnCompletion = YES;
keyFrameAnimation.fillMode = kCAFillModeForwards;
//重复次数
// keyFrameAnimation.repeatCount = MAXFLOAT;
//添加动画
[bubbleView.layer addAnimation:keyFrameAnimation forKey:@"movingAnimation"];
加上随机数生成,结束时泡泡炸开等效果,就可以做出好看的冒泡泡动效了:
加入购物车动效
在上面的基础上再加些旋转,缩放就可以做出加入购物车的动效:水波纹动效
:根据公式y = Asin(ωx+φ)+h
得到n个点,用贝塞尔曲线将这些点连接起来,形成一条波浪线,然后用CADisplayLink 不断改变x的值,来使曲线动起来,形成波浪的感觉。
k
是改变高度,
ω
是改变波浪往前偏移的位置,
φ
是改变速度,
A
是改变波浪的幅度。
关键代码:
- (void)drawRect:(CGRect)rect{
/*
想有几条波浪就创建几条曲线,
更改kappa(k)是改变高度,
更改omega(ω)是改变波浪往前偏移的位置,
更改phi(φ)是改变速度,
更改alpha(A)是改变波浪的幅度
*/
[self drawWaveWithColor:[UIColor colorWithHexString:@"#108EE8" alpha:1] withOmega:self.omega withPhi:self.phi withKappa:self.kappa];
[self drawWaveWithColor:[UIColor colorWithHexString:@"#108EE8" alpha:0.4] withOmega:self.omega * 1.5 withPhi:self.phi withKappa:self.kappa];
}
-(void)drawWaveWithColor:(UIColor *)color withOmega:(CGFloat)omega withPhi:(CGFloat)phi withKappa:(CGFloat)kappa{
[color set];
UIBezierPath * path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, self.bounds.size.height)];
//正弦曲线公式为:y=Asin(ωx+φ)+k;
for (CGFloat x = 0.0f; x <= self.bounds.size.width; x++) {
CGFloat y = self.alpha * sinf(omega * x + phi) + kappa;
[path addLineToPoint:CGPointMake(x, y)];
}
[path addLineToPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height)];
[path fill];
}
角标拖动消失动效
:5个关键点的确定。
: LSBadgeView
xib和手写代码均可调用,但是xib调用时,约束要用相对于view,而不是safeArea,或者你在LSBadgeView.m
中自己手动去修改center的赋值,将safeArea距离加上也可以。
核心代码:
-(void)getPoint{
//获取半径,根据拖动距离,缩小小圆半径
CGFloat rate = self.currentDistance / self.maxDistance > 1 ? 0 : 1 - (self.currentDistance / self.maxDistance);
self.beginR = self.currentR * rate;
//计算φ
CGFloat M = (self.currentPoint.x - self.beginPoint.x) / (self.currentPoint.y - self.beginPoint.y);
CGFloat angle = atanf(M);
//计算A
CGFloat Ax = self.beginPoint.x - cosf(angle) * self.beginR;
CGFloat Ay = self.beginPoint.y + self.beginR * sinf(angle);
self.pointA = CGPointMake(Ax, Ay);
//计算B
CGFloat Bx = self.beginPoint.x + cosf(angle) * self.beginR;
CGFloat By = self.beginPoint.y - self.beginR * sinf(angle);
self.pointB = CGPointMake(Bx, By);
//计算C
CGFloat Cx = self.currentPoint.x - cosf(angle) * self.currentR;
CGFloat Cy = self.currentR * sinf(angle) + self.currentPoint.y;
self.pointC = CGPointMake(Cx, Cy);
//计算D
CGFloat Dx = self.currentPoint.x + cosf(angle) * self.currentR;
CGFloat Dy = self.currentPoint.y - self.currentR * sinf(angle);
self.pointD = CGPointMake(Dx, Dy);
//计算O
CGFloat Ox = self.beginPoint.x + (self.currentPoint.x - self.beginPoint.x) / 2;
CGFloat Oy = self.beginPoint.y + (self.currentPoint.y - self.beginPoint.y) / 2;
self.pointO = CGPointMake(Ox, Oy);
}
-(void)drawPath{
UIBezierPath * path3 = [UIBezierPath bezierPath];
//小圆
[path3 addArcWithCenter:self.beginPoint radius:self.beginR startAngle:0 endAngle:360 clockwise:YES];
//两条圆弧 按照A -> B -> D -> C -> A 画图
[path3 moveToPoint:self.pointA];
[path3 addLineToPoint:self.pointB];
[path3 addQuadCurveToPoint:self.pointD controlPoint:self.pointO];
[path3 addLineToPoint:self.pointC];
[path3 addQuadCurveToPoint:self.pointA controlPoint:self.pointO];
self.pathLayer.path = path3.CGPath;
}
效果图:
另外
demo中r取的是角标view宽高中的较小值,如果你对demo圆角矩形时候的粘性路径不满意,可以自己去重新计算修改C、D的值,使其落在圆角矩形的边上。
-
fillMode
:
kCAFillModeForwards
:动画结束后,layer处于动画结束时的状态。
kCAFillModeBackwards
:动画开始前,layer处于动画开始时的状态。
kCAFillModeBoth
:动画开始前,layer处于动画开始时的状态;动画结束后,layer处于动画结束时的状态。
kCAFillModeRemoved
:默认模式,动画开始前和结束后,动画对layer的状态没有影响。也就是说,动画开始前和结束后,layer都会处于添加动画前的状态,即在原点的位置。
注意
:kCAFillModeRemoved 和 kCAFillModeBackwards 模式下,removedOnCompletion不管设置为YES还是NO,都会回到原始状态,kCAFillModeForwards 和 kCAFillModeBoth 模式下,removedOnCompletion的设置才有效。
timingFunctions
:动画的进场和出场效果
kCAMediaTimingFunctionLinear
(线性):匀速;
kCAMediaTimingFunctionEaseIn
(渐进):动画缓慢进入,然后加速离开;
kCAMediaTimingFunctionEaseOut
(渐出):动画全速进入,然后减速的到达目的地;
kCAMediaTimingFunctionEaseInEaseOut
(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地;
kCAMediaTimingFunctionDefault
(默认时间函数)。-
keyTimes
和values
:
keyTimes
给对应帧设置时间,值为0~1(表示的是进行到当前帧时所耗时间占),不设置表示匀速完成动画;
(如: @[@(0), @(0.2), @(0.5), @(1)]; 假设总时间为10s,那么到达p1,用时2s(即0.2),到达p2,用时3s(加起来是0.5),到达p3,用时5s,完成一遍完整动画)
values
是帧数组。
keyTimes 和 values 联合起来用,但是需要注意,如果你设置了keyFrameAnimation.path,那么这俩就不奏效(其实就是帧动画的两种实现方式,一种是Path实现,一种是Value方式实现)
//帧动画
CAKeyframeAnimation * keyFrameAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
[keyFrameAnimation setDuration:10];
//设置为NO,则在动画结束后停在终点,YES则会回到原点
keyFrameAnimation.removedOnCompletion = YES;
keyFrameAnimation.fillMode = kCAFillModeForwards;
//重复次数
keyFrameAnimation.repeatCount = MAXFLOAT;
NSValue * p1 = [NSValue valueWithCGPoint:CGPointMake(50, 120)];
NSValue * p2 = [NSValue valueWithCGPoint:CGPointMake(50, 200)];
NSValue * p3 = [NSValue valueWithCGPoint:CGPointMake(150, 150)];
keyFrameAnimation.values = @[p1, p2, p3];
keyFrameAnimation.keyTimes = @[@(0.1),@(0.8),@(1)];
keyFrameAnimation.autoreverses = YES;
[bubbleView.layer addAnimation:keyFrameAnimation forKey:@"movingAnimation"];
rotationMode
:主体沿路径切线方向,即如果你是方块绕不规则路径运动,那么那个方块会根据路径方向自动适当旋转,可以想象小车爬山游戏。默认值是nil。
kCAAnimationRotateAuto
:沿切线自动旋转;
kCAAnimationRotateAutoReverse
:翻转180°后沿切线自动旋转运动。停止帧动画:
if ([bubbleView.layer animationForKey:@"movingAnimation"]) {//如果动画进行中
[bubbleView.layer removeAnimationForKey:@"movingAnimation"];//停止动画
}
- 帧动画代理方法:
keyFrameAnimation.delegate = self;
//动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
}
//动画开始
-(void)animationDidStart:(CAAnimation *)anim{
}
- animationWithKeyPath的常用属性
//路径动画
CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//旋转动画
CABasicAnimation * rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
1)position 位置变化(运动路径动画)
2)transform.scale 缩放比例
transform.scale.x 缩放宽的比例
transform.scale.y 缩放高的比例
3)transform.rotation 旋转
transform.rotation.x 围绕x轴旋转
transform.rotation.y 围绕y轴旋转
transform.rotation.z 围绕z轴旋转
4)opacity 透明度
5)cornerRadius 圆角的设置
6)backgroundColor 背景颜色的变化
7)bounds 大小变化,中心不变
8)contentsRect.size.width 横向拉伸缩放
9)shadowColor 阴影色
10)strokeEnd 绘制路径过程动画
总结
贝塞尔曲线的核心就是确定路径的关键点,如果路径的关键点自己算不出来,就找个数学好的朋友,让他/她帮你算吧......
demo地址:https://github.com/cherrys94/UIBezierPathDemo