玩轉(zhuǎn)貝塞爾曲線
- 歷史:由法國(guó)雷諾汽車的工程師皮諾爾·貝塞爾發(fā)明,應(yīng)用于雷諾汽車設(shè)計(jì)
- 原理鋪墊:給定
n+1個(gè)數(shù)據(jù)點(diǎn),生產(chǎn)一條曲線,使得該曲線與這些點(diǎn)所連接的折線相近。第一個(gè)demo
- 以
KYAnimatedPageControl(粘性小球) 為例- 談?wù)刬OS中粘性動(dòng)畫以及果凍效果的實(shí)現(xiàn)
- 粘性小球會(huì)根據(jù)移動(dòng)距離的大小擁有不同的彈性程度。移動(dòng)距離越大,彈性效果越明顯。

思路:一個(gè)小球用四條貝塞爾曲線平分拼成,鏈接完成之后向內(nèi)填充顏色。然后,再單獨(dú)控制每條貝塞爾曲線的形狀,實(shí)時(shí)調(diào)用
layer的[self setNeedsDisplay]以重繪- (void)drawInContext:(CGContextRef)ctx方法。其中每條弧線都有兩個(gè)控制點(diǎn)(學(xué)過(guò)的應(yīng)該有印象)。為了方便傳達(dá)理念,已以下形式展示這一思路。


小球是由弧AB、弧BC、弧CD、弧DA 四段組成,其中每段弧都綁定兩個(gè)控制點(diǎn):弧AB 綁定的是 C1 、 C2;弧BC 綁定的是 C3 、 C4...
問(wèn)題:這些點(diǎn)應(yīng)該以什么樣的規(guī)律運(yùn)動(dòng)?
- 為了方便計(jì)算各個(gè)點(diǎn)的坐標(biāo),引入外接矩形(圖中虛線)
首先計(jì)算出這個(gè)外接矩形的位置:根據(jù)中心點(diǎn)的
(x,y)分別減去矩形(寬,高)的1/2獲得。//outsideRectSize 是外接矩形邊長(zhǎng) // 外接矩形 x CGFloat origin_x = self.position.x - outsideRectSize/2 + (pro gress - 0.5) * (self.frame.size.width - outsideRectSize); // 外接矩形 y CGFloat origin_y = self.position.y - outsideRectSize/2; // 設(shè)置外接矩形的frame self.outsideRect = CGRectMake(origin_x, origin_y, outsideRect Size, outsideRectSize);
- 個(gè)人理解:代碼里的
progress代表著上面視圖中滑塊0~1的值。計(jì)算外接矩形 X值時(shí),...+ (pro gress - 0.5) * (self.frame.size.width - outsideRectSize)這個(gè)部分代碼,代表著矩形在向左右移動(dòng)的過(guò)程中,向左移B點(diǎn),向右移D點(diǎn),這兩點(diǎn)在移動(dòng)過(guò)程中的緩沖區(qū)。其次還需要判斷當(dāng)前是向左移還是右移,左移的時(shí)候B動(dòng)D不動(dòng);右移的時(shí)候D動(dòng)B不動(dòng)。(動(dòng):指的是需不需要有緩沖區(qū))
//只要外接矩形在左側(cè),則改變B點(diǎn);在右邊,改變D點(diǎn) if (progress <= 0.5) { self.movePoint = POINT_B;//用枚舉代表 B, D 點(diǎn) NSLog(@"B點(diǎn)動(dòng)"); }else{ self.movePoint = POINT_D; NSLog(@"D點(diǎn)動(dòng)"); }有了矩形的位置,接下來(lái)計(jì)算關(guān)鍵點(diǎn)的坐標(biāo)
- (void)drawInContext:(CGContextRef)ctx{ //A-C1、B-C2... 的距離,當(dāng)設(shè)置為正方形邊長(zhǎng)的1/3.6倍時(shí),畫出來(lái)的圓弧完美貼合圓形 CGFloat offset = self.outsideRect.size.width / 3.6; //A.B.C.D實(shí)際需要移動(dòng)的距離.系數(shù)為滑塊偏離中點(diǎn)0.5的絕對(duì)值再乘以2.當(dāng)滑到兩端的時(shí)候,movedDistance為最大值:「外接矩形寬度的1/5」. CGFloat movedDistance = (self.outsideRect.size.width * 1 / 6) * fabs(self.progress-0.5)*2; //方便下方計(jì)算各點(diǎn)坐標(biāo),先算出外接矩形的中心點(diǎn)坐標(biāo) CGPoint rectCenter = CGPointMake(self.outsideRect.origin.x + self.outsideRect.size.width/2 , self.outsideRect.origin.y + self.outsideRect.size.height/2); CGPoint pointA = CGPointMake(rectCenter.x ,self.outsideRect.origin.y + movedDistance); CGPoint pointB = CGPointMake(self.movePoint == POINT_D ? rectCenter.x + self.outsideRect.size.width/2 : rectCenter.x + self.outsideRect.size.width/2 + movedDistance*2 ,rectCenter.y); CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.outsideRect.size.height/2 - movedDistance); CGPoint pointD = CGPointMake(self.movePoint == POINT_D ? self.outsideRect.origin.x - movedDistance*2 : self.outsideRect.origin.x, rectCenter.y); CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y); CGPoint c2 = CGPointMake(pointB.x, self.movePoint == POINT_D ? pointB.y - offset : pointB.y - offset + movedDistance); CGPoint c3 = CGPointMake(pointB.x, self.movePoint == POINT_D ? pointB.y + offset : pointB.y + offset - movedDistance); CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y); CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y); CGPoint c6 = CGPointMake(pointD.x, self.movePoint == POINT_D ? pointD.y + offset - movedDistance : pointD.y + offset); CGPoint c7 = CGPointMake(pointD.x, self.movePoint == POINT_D ? pointD.y - offset + movedDistance : pointD.y - offset); CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y); //外接虛線矩形 UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:self.outsideRect]; CGContextAddPath(ctx, rectPath.CGPath); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 1.0); CGFloat dash[] = {5.0, 5.0}; CGContextSetLineDash(ctx, 0.0, dash, 2); //1 CGContextStrokePath(ctx); //給線條填充顏色 //圓的邊界 UIBezierPath* ovalPath = [UIBezierPath bezierPath]; [ovalPath moveToPoint: pointA]; [ovalPath addCurveToPoint:pointB controlPoint1:c1 controlPoint2:c2]; [ovalPath addCurveToPoint:pointC controlPoint1:c3 controlPoint2:c4]; [ovalPath addCurveToPoint:pointD controlPoint1:c5 controlPoint2:c6]; [ovalPath addCurveToPoint:pointA controlPoint1:c7 controlPoint2:c8]; [ovalPath closePath]; CGContextAddPath(ctx, ovalPath.CGPath); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor); CGContextSetLineDash(ctx, 0, NULL, 0); //2 CGContextDrawPath(ctx, kCGPathFillStroke); //同時(shí)給線條和線條包圍的內(nèi)部區(qū)域填充顏色 //-------------- 注:以下代碼均為輔助觀察 ------------------- //標(biāo)記出每個(gè)點(diǎn)并連線,方便觀察,給所有關(guān)鍵點(diǎn)染色 -- 白色,輔助線顏色 -- 白色 //語(yǔ)法糖:字典@{},數(shù)組@[],基本數(shù)據(jù)類型封裝成對(duì)象@234,@12.0,@YES,@(234+12.0) CGContextSetFillColorWithColor(ctx, [UIColor yellowColor].CGColor); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); NSArray *points = @[[NSValue valueWithCGPoint:pointA],> >[NSValue valueWithCGPoint:pointB],[NSValue valueWithCGPoint:pointC],[NSValue valueWithCGPoint:pointD],[NSValue valueWithCGPoint:c1],[NSValue valueWithCGPoint:c2],[NSValue valueWithCGPoint:c3],[NSValue valueWithCGPoint:c4],[NSValue valueWithCGPoint:c5],[NSValue valueWithCGPoint:c6],[NSValue valueWithCGPoint:c7],[NSValue valueWithCGPoint:c8]];
[self drawPoint:points withContext:ctx];
//連接輔助線
UIBezierPath *helperline = [UIBezierPath bezierPath];
[helperline moveToPoint:pointA];
[helperline addLineToPoint:c1];
[helperline addLineToPoint:c2];
[helperline addLineToPoint:pointB];
[helperline addLineToPoint:c3];
[helperline addLineToPoint:c4];
[helperline addLineToPoint:pointC];
[helperline addLineToPoint:c5];
[helperline addLineToPoint:c6];
[helperline addLineToPoint:pointD];
[helperline addLineToPoint:c7];
[helperline addLineToPoint:c8];
[helperline closePath];
CGContextAddPath(ctx, helperline.CGPath);
CGFloat dash2[] = {2.0, 2.0};
CGContextSetLineDash(ctx, 0.0, dash2, 2);
CGContextStrokePath(ctx); //給輔助線條填充顏色
}
//在某個(gè)point位置畫一個(gè)點(diǎn),方便觀察運(yùn)動(dòng)情況
- (void)drawPoint:(NSArray *)points withContext:(CGContextRef)ctx{
for (NSValue *pointValue in points) {
CGPoint point = [pointValue CGPointValue];
CGContextFillRect(ctx, CGRectMake(point.x - 2,point.y - 2,4,4));
}
}
代碼中`ctx`字面意思是上下文,你可以理解為一塊全局的畫布。也就是說(shuō),一旦在某個(gè)地方改了畫布的一些屬性,其他任何使用畫布的屬性的時(shí)候都是改了之后的。比如上面在 `//1` 中把線條樣式改成了虛線,那么在下文 `//2` 中如果不恢復(fù)成連續(xù)的直線,那么畫出來(lái)的依然是`//1`中的虛線樣式。 >* **個(gè)人理解:** > >* **結(jié)合上面后兩張圖片分析**:假設(shè)向左移動(dòng),發(fā)現(xiàn)外接矩形以相同的速度向左移動(dòng),隨著移動(dòng),發(fā)現(xiàn)`c7,c6`兩控制點(diǎn)在`y`軸并不會(huì)改變;`c8,c5`在`x`軸不變的情況下,`y`軸向內(nèi)移動(dòng),`c1,c4`控制點(diǎn)與其保持平行,同時(shí)`x`軸相對(duì)不變;`c2,c3`以相同的比例向內(nèi)靠近。**猜測(cè)**該向內(nèi)靠近的多少與緩沖區(qū)有關(guān)連。向右移動(dòng)同理。相關(guān)閱讀
注意:該筆記內(nèi)容暫且待定,我覺得有必要加深一下內(nèi)功,務(wù)實(shí)基礎(chǔ)。先讀讀《iOS核心動(dòng)畫高級(jí)技巧》??。