目錄
- 前言(對UIBezierPath的介紹和描述)
- 基礎API (基礎使用)
- 手寫簽名 (項目中用過)
- 簡單動效
- 進度條動效(項目中用過)
- 冒泡泡動效(寫來玩的,一般也沒啥項目會用到)
- 加入購物車動效(可以看看,沒準能用到)
- 水波紋動效(以前見過支付寶有這個效果)
- 角標拖動消失動效(仿QQ效果)
前言
(這一部分是裝13的對UIBezierPath的介紹和描述,只想看用法的,可以跳過這部分或者直接下載demo查看。)
UIBezierPath位于UIKit框架中,它的核心是數學公式 -- 伯恩斯坦多項式,后來被法國數學家 Paul de Casteljau通過德卡斯特里奧(de Casteljau) 算法進行了圖形化,再后來被法國工程師皮埃爾·貝塞爾(Pierre Bézier)用來輔助汽車的車體工業(yè)設計,并廣為宣傳,從而被大眾熟知。
一階貝塞爾曲線

二階貝塞爾曲線

P0為起始點,P2為終點,P1為曲線弧度的控制點

三階貝塞爾曲線

P0為起始點,P3為終點,P1和P2為曲線弧度的控制點

在 P0P1,P1P2,P2P3這三段線(灰色線)上 ,連接之后形成線段(綠色線)上再取同樣的等比劃分點,連接之后形成線段(藍色線)上取同樣的等比劃分點,然后連接P0,形成圓滑曲線(即最終的紅色線)。
貝塞爾曲線可以有n階,原理都是這樣在形成的線段上不斷取等比劃分點形成曲線。
(n階是使用若干個二階和三階拼湊出來的)

其實就可以想象一根彈性磁性線,固定兩端,磁石牽引磁性線使之出現有弧度,磁石的位置就是控制點的位置,磁石可以有n個。
基礎API
使用UIBezierPath一般是重寫view的drawRect方法,在drawRect方法里創(chuàng)建圖形,如果你是在UIViewController里面直接寫,那么可以使用[self.view.layer addSublayer:shapeLayer]; 或者self.view.layer.mask = shapeLayer; 來顯示。
區(qū)別在于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創(chuàng)建并返回一個新的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;
//創(chuàng)建并返回一個與當前路徑相反的新的貝塞爾路徑對象
- (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;
//最大斜接長度 斜接長度指的是在兩條線交匯處內角和外角之間的距離。 只有l(wèi)ineJoin屬性為kCALineJoinMiter時miterLimit才有效; 邊角的角度越小,斜接長度就會越大;如果斜接長度超過 miterLimit的值,邊角會以 lineJoin的 "bevel"即kCALineJoinBevel類型來顯示
@property(nonatomic) CGFloat miterLimit; // Used when lineJoinStyle is kCGLineJoinMiter
//確定彎曲路徑短的繪制精度的因素
@property(nonatomic) CGFloat flatness;
//判斷奇偶數組的規(guī)則繪制圖像,圖形復雜時填充顏色的一種規(guī)則。類似棋盤
@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
//用指定的混合模式和透明度值來描繪受接收路徑所包圍的區(qū)域,內部填充
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
//用指定的混合模式和透明度值來描繪受接收路徑所包圍的區(qū)域,內部不填充
- (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) {
//創(chuàng)建貝塞爾路徑
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];
//創(chuàng)建運動軌跡
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{
/*
想有幾條波浪就創(chuàng)建幾條曲線,
更改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個關鍵點的確定。
:
LSBadgeViewxib和手寫代碼均可調用,但是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處于動畫結束時的狀態(tài)。
kCAFillModeBackwards:動畫開始前,layer處于動畫開始時的狀態(tài)。
kCAFillModeBoth:動畫開始前,layer處于動畫開始時的狀態(tài);動畫結束后,layer處于動畫結束時的狀態(tài)。
kCAFillModeRemoved:默認模式,動畫開始前和結束后,動畫對layer的狀態(tài)沒有影響。也就是說,動畫開始前和結束后,layer都會處于添加動畫前的狀態(tài),即在原點的位置。
注意:kCAFillModeRemoved 和 kCAFillModeBackwards 模式下,removedOnCompletion不管設置為YES還是NO,都會回到原始狀態(tài),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,完成一遍完整動畫)幀動畫keyTimes.png
values 是幀數組。
keyTimes 和 values 聯(lián)合起來用,但是需要注意,如果你設置了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:主體沿路徑切線方向,即如果你是方塊繞不規(guī)則路徑運動,那么那個方塊會根據路徑方向自動適當旋轉,可以想象小車爬山游戲。默認值是nil。
kCAAnimationRotateAuto:沿切線自動旋轉;
kCAAnimationRotateAutoReverse:翻轉180°后沿切線自動旋轉運動。停止幀動畫:
if ([bubbleView.layer animationForKey:@"movingAnimation"]) {//如果動畫進行中
[bubbleView.layer removeAnimationForKey:@"movingAnimation"];//停止動畫
}
- 幀動畫代理方法:<CAAnimationDelegate>
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 繪制路徑過程動畫
總結
貝塞爾曲線的核心就是確定路徑的關鍵點,如果路徑的關鍵點自己算不出來,就找個數學好的朋友,讓他/她幫你算吧......
