動畫特效十七:粘性動畫

本人錄製技術視頻地址: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處的切線。

 

三階貝塞爾曲線:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章