本人錄製技術視頻地址:https://edu.csdn.net/lecturer/1899 歡迎觀看。
注:本文內容是學習自 KittenYang 的《A GUIDE TO IOS ANIMATION》一書,如果大家感興趣的話,可以購買本書閱讀。但就個人而言,覺得作者的確思維清晰,但語言表達上面忽略了很多細節方面的說明,代碼書寫方面有許多需要優化及更正的地方。
粘性動畫效果圖如下:
其實這裏的動畫效果實現起來還是比較複雜的。我只進行分析,並講解粘性小球的部分。
思路分析
1. 我們可以自定義一個View來操作這個整體的效果。
2. 這個View上面有兩層
2.1 後面的灰色的背景與紅色的進度條,關於在動畫執行過程中,繪製具體的線條,我已經在 CALayer的needsDisplayForKey方法使用說明 進行了詳細的說明。
2.2 上面滾動的正方形或者滾動的小球(這一節主要講解怎麼實現上面滾動的小球)。
局部分析小球的形變效果,效果圖如下(就是上圖拖拽過程中小球的拉伸效果):
代碼分析
1. 主控制器代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
CGRect frame = CGRectMake((self.view.frame.size.width - kRoundWidth) * 0.5, (self.view.frame.size.height - kRoundWidth) * 0.5, kRoundWidth, kRoundWidth);
self.roundView = [[LFRoundView alloc] initWithFrame:frame];
self.roundView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.roundView];
// first load
self.roundView.roundLayer.progress = self.slider.value;
}
- (void)sliderChanged:(UISlider *)slider {
self.roundView.roundLayer.progress = slider.value;
}
代碼中的roundView就是黃色背景的View,是小球運動的載體。
2. LFRoundView的代碼如下:
@class LFRoundLayer;
@interface LFRoundView : UIView
@property (nonatomic, strong) LFRoundLayer *roundLayer;
@end
@implementation LFRoundView
// LFRoundView只是LFRoundLayer的載體
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.roundLayer = [LFRoundLayer layer];
self.roundLayer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
self.roundLayer.contentsScale = [UIScreen mainScreen].scale;
[self.layer addSublayer:self.roundLayer];
}
return self;
}
@end
在roundView的Layer上面添加了roundLayer的圖層,這個圖層就是用來繪製上面變動的小球。
3. LFRoundLayer代碼分析:
由於在主控制器中拖拽UISlider會一直促發小球的變形效果,所以LFRoundLayer應該定義一個progress屬性用來接受主控制器中傳遞過來的信息,並根據這個信息實時繪製變動的小球。
LFRoundLayer的 .h文件定義如下:
@interface LFRoundLayer : CALayer
@property (nonatomic, assign) CGFloat progress;
@end
然後在 .m文件中重寫progress屬性,用來實時監聽屬性值的變化,繪製變動的小球。
- (void)setProgress:(CGFloat)progress {
_progress = progress;
// 1. Prepare Positioning Square
// 1-1) squareX 的計算
CGFloat squareX = (self.frame.size.width - kOutsideWidth) * progress;
CGFloat squareY = self.position.y - kOutsideWidth * 0.5;
CGFloat squareW = kOutsideWidth;
CGFloat squareH = kOutsideWidth;
self.SquareRect = CGRectMake(squareX, squareY, squareW, squareH);
[self setNeedsDisplay];
}
需要說明的有以下幾點:
1. kOutsideWidth是自定義的一個宏,它是正圓的外接正方形的邊長。
#define kOutsideWidth 90
2. SquareRect 是外接正方形的frame, 雖然小球在運動過程中一直在發生變化,但是那個正方形一直保持不變,只是squareX一直在發生變化, SquareRect這個frame主要是用來定位計算,方便的計算出小球的相關位置信息,下面會做詳細的介紹。
3. 調用了[self setNeedsDisplay]方法,所以在重寫progress的過程中,會一直促發屏幕的重繪工作,即調用下面這個方法
- (void)drawInContext:(CGContextRef)ctx;
最後,我們在 drawInContext: 這個方法中完成小球的繪製工作。
- (void)drawInContext:(CGContextRef)ctx {
// 2. Prepare A,B,C,D
CGFloat movedDistance = kOutsideWidth / 6 * fabs(self.progress - 0.5);
self.direction = self.progress >= 0.5 ? MovedDirectionRight : MovedDirectionLeft;
CGFloat squareX = self.SquareRect.origin.x;
CGFloat squareY = self.SquareRect.origin.y;
CGFloat squareW = self.SquareRect.size.width;
CGPoint pointA = CGPointMake(squareX + kOutsideWidth * 0.5, squareY + movedDistance);
CGPoint pointB = CGPointMake(self.direction == MovedDirectionRight ? (squareX + squareW) : (squareX + squareW + 2 * movedDistance), self.position.y);
CGPoint pointC = CGPointMake(squareX + kOutsideWidth * 0.5, self.position.y + kOutsideWidth * 0.5 - movedDistance);
CGPoint pointD = CGPointMake(self.direction == MovedDirectionRight ? (squareX - 2 * movedDistance) : squareX, self.position.y);
// 3. Prepare C1~C8
CGPoint pointC1 = CGPointMake(pointA.x + kOffset, pointA.y);
CGPoint pointC2 = CGPointMake(pointB.x, pointB.y - kOffset);
CGPoint pointC3 = CGPointMake(pointB.x, pointB.y + kOffset);
CGPoint pointC4 = CGPointMake(pointC.x + kOffset, pointC.y);
CGPoint pointC5 = CGPointMake(pointC.x - kOffset, pointC.y);
CGPoint pointC6 = CGPointMake(pointD.x, pointD.y + kOffset);
CGPoint pointC7 = CGPointMake(pointD.x, pointD.y - kOffset);
CGPoint pointC8 = CGPointMake(pointA.x - kOffset, pointA.y);
NSArray *points = @[
[NSValue valueWithCGPoint:pointA], [NSValue valueWithCGPoint:pointB],
[NSValue valueWithCGPoint:pointC], [NSValue valueWithCGPoint:pointD],
[NSValue valueWithCGPoint:pointC1], [NSValue valueWithCGPoint:pointC2],
[NSValue valueWithCGPoint:pointC3], [NSValue valueWithCGPoint:pointC4],
[NSValue valueWithCGPoint:pointC5], [NSValue valueWithCGPoint:pointC6],
[NSValue valueWithCGPoint:pointC7], [NSValue valueWithCGPoint:pointC8]
];
// highlighted assistant points
[self assistantPointWithArray:points inContext:ctx];
// 1. Draw Positioning Square
UIBezierPath *squareBezierPath = [UIBezierPath bezierPathWithRect:self.SquareRect];
CGContextAddPath(ctx, squareBezierPath.CGPath);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetLineWidth(ctx, 1);
// how to use CGContextSetLineDash, refer to http://blog.csdn.net/zhangao0086/article/details/7234859
CGFloat squareDash[2] = {5, 5};
CGContextSetLineDash(ctx, 0, squareDash, 2);
CGContextStrokePath(ctx);
// 2. Draw Assistant Lines
UIBezierPath *assistantBezierPath = [UIBezierPath bezierPath];
[assistantBezierPath moveToPoint:pointA];
[assistantBezierPath addLineToPoint:pointC1];
[assistantBezierPath addLineToPoint:pointC2];
[assistantBezierPath addLineToPoint:pointB];
[assistantBezierPath addLineToPoint:pointC3];
[assistantBezierPath addLineToPoint:pointC4];
[assistantBezierPath addLineToPoint:pointC];
[assistantBezierPath addLineToPoint:pointC5];
[assistantBezierPath addLineToPoint:pointC6];
[assistantBezierPath addLineToPoint:pointD];
[assistantBezierPath addLineToPoint:pointC7];
[assistantBezierPath addLineToPoint:pointC8];
[assistantBezierPath closePath];
CGContextAddPath(ctx, assistantBezierPath.CGPath);
CGFloat lineDash[2] = {2, 2};
CGContextSetLineDash(ctx, 0, lineDash, 2);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextStrokePath(ctx);
// 3. Draw Entity
UIBezierPath *ovalBezierPath = [UIBezierPath bezierPath];
[ovalBezierPath moveToPoint:pointA];
[ovalBezierPath addCurveToPoint:pointB controlPoint1:pointC1 controlPoint2:pointC2];
[ovalBezierPath addCurveToPoint:pointC controlPoint1:pointC3 controlPoint2:pointC4];
[ovalBezierPath addCurveToPoint:pointD controlPoint1:pointC5 controlPoint2:pointC6];
[ovalBezierPath addCurveToPoint:pointA controlPoint1:pointC7 controlPoint2:pointC8];
[ovalBezierPath closePath];
CGContextAddPath(ctx, ovalBezierPath.CGPath);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineDash(ctx, 0, NULL, 0);
CGContextDrawPath(ctx, kCGPathFillStroke);
}
- (void)assistantPointWithArray:(NSArray *)points inContext:(CGContextRef)ctx {
CGFloat rectWidth = 4;
CGContextSetFillColorWithColor(ctx, [UIColor greenColor].CGColor);
for (NSValue *pointValue in points) {
CGPoint point = pointValue.CGPointValue;
CGContextFillRect(ctx, CGRectMake(point.x - rectWidth * 0.5, point.y - rectWidth * 0.5, rectWidth, rectWidth));
}
}
由於代碼量有點多,我會詳細的說明相關代碼:
1. movedDistance這個值的分析如下圖
2. 爲了標記小球是在向左移動還是在向右移動,定義一個枚舉來標示:
typedef enum {
MovedDirectionLeft,
MovedDirectionRight
}MovedDirection;
3. 後面的一系列代碼就是來繪製相關點(A,B,C,D, C1~C8)
先看下面這張關於各個點位置分佈介紹圖:
仔細觀察Demo中gif小球的運動,在運動過程中,線段AC1的長度始終是保持不變的,變化的只是B點和D點的位置。所以,在小球運動過程中,我們可以選擇特殊的位置來計算AC1的大小,而這個特殊位置就是self.progress爲0.5的時候,即小球處於圓形的時刻。
我們設置A點作爲起始點,B點作爲終止點,C1和C2作爲定位點,然後利用貝塞爾曲線進行計算。關於詳細的計算過程,見上圖的分析。
4. 在計算得到所有點的座標信息後,就可以繪製出小球及相關輔助線等信息了。
擴充,關於貝塞爾曲線的知識
Bézier curve(貝塞爾曲線)是應用於二維圖形應用程序的數學曲線。 曲線定義:起始點、終止點(也稱錨點)、控制點。通過調整控制點,貝塞爾曲線的形狀會發生變化。 1962年,法國數學家Pierre Bézier第一個研究了這種矢量繪製曲線的方法,並給出了詳細的計算公式,因此按照這樣的公式繪製出來的曲線就用他的姓氏來命名,稱爲貝塞爾曲線。
以下公式中:B(t)爲t時間下 點的座標;
P0爲起點,Pn爲終點,Pi爲控制點
一階貝塞爾曲線(線段):
意義:由 P0 至 P1 的連續點, 描述的一條線段
二階貝塞爾曲線(拋物線):
原理:由 P0 至 P1 的連續點 Q0,描述一條線段。
由 P1 至 P2 的連續點 Q1,描述一條線段。
由 Q0 至 Q1 的連續點 B(t),描述一條二次貝塞爾曲線。
經驗:P1-P0爲曲線在P0處的切線。
三階貝塞爾曲線: