談談iOS中粘性動畫以及果凍效果的實現

在最近作個一個自定義PageControl——KYAnimatedPageControl中,我實現了CALayer的形變更畫以及CALayer的彈性動畫,效果先過目:git

https://github.com/KittenYang/KYAnimatedPageControl

先作個提綱:github

第一個分享的主題是「如何讓CALayer發生形變」,這個技術在我以前一個項目 ———— KYCuteView 中有涉及,也寫了篇簡短的實現原理博文。今天再舉一個例子。

以前我也作過相似果凍效果的彈性動畫,好比這個項目—— KYGooeyMenu。用到的核心技術是CAKeyframeAnimation,而後設置幾個不一樣狀態的關鍵幀,就能初步達到這種彈性效果。可是,畢竟只有幾個關鍵幀,並且是須要手動計算,不精確不說,動畫也不夠細膩,畢竟你不可能手動建立60個關鍵幀。因此,今天的第二個主題是 —— 「如何用阻尼振動函數建立出60個關鍵幀」,從而實現CALayer產生相似[UIView animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion] 的彈性動畫。

正文。數組

如何讓CALayer發生形變?

關鍵技術很簡單:你須要用多條貝塞爾曲線 「拼」 出這個Layer。之因此這樣作的緣由不言而喻,由於這樣方便咱們發生形變。函數

好比 KYAnimatedPageControl 中的這個小球,其實它是這麼被畫出來的:字體

小球是由弧AB、弧BC、弧CD、弧DA 四段組成,其中每段弧都綁定兩個控制點:弧AB 綁定的是 C1 、 C2;弧BC 綁定的是 C3 、 C4 …..動畫

  • 如何表達各個點?網站

首先,A、B、C、D是四個動點,控制他們動的變量是ScrollView的contentOffset.x。咱們能夠在-(void)scrollViewDidScroll:(UIScrollView *)scrollView中實時獲取這個變量,並把它轉換成一個控制在 0~1 的係數,取名爲factor。spa

_factor = MIN(1, MAX(0, (ABS(scrollView.contentOffset.x - self.lastContentOffset) / scrollView.frame.size.width)));3d

假設A、B、C、D的最大變化距離爲小球直徑的2/5。那麼結合這個0~1的係數,咱們能夠得出A、B、C、D的真實變化距離 extra 爲:extra = (self.width 2 / 5) factor。當factor == 1時,達到最大形變狀態,此時四個點的變化距離均爲(self.width * 2 / 5)。rest

注意:根據滑動方向,咱們還要根據是B點移動仍是D點移動。

CGPoint pointA = CGPointMake(rectCenter.x ,self.currentRect.origin.y + extra);
CGPoint pointB = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? rectCenter.x + self.currentRect.size.width/2 : rectCenter.x + self.currentRect.size.width/2 + extra*2 ,rectCenter.y);
CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.currentRect.size.height/2 - extra);
CGPoint pointD = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? self.currentRect.origin.x - extra*2 : self.currentRect.origin.x, rectCenter.y);

 

而後是控制點:

關鍵是要知道上圖中A-C1 、B-C二、B-C三、C-C4….這些水平和垂直虛線的長度,命名爲offSet。通過屢次嘗試,我得出的結論是:

當offSet設置爲 直徑除以3.6 的時候,弧線能完美地貼合成圓弧。我隱約感受這個 3.6 是必然,貌似和360度有某種關係,或許經過演算能得出 3.6 這個值的必然性,但我沒有嘗試。

所以,各個控制點的座標:

CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y);  
CGPoint c2 = CGPointMake(pointB.x, pointB.y - offset);

CGPoint c3 = CGPointMake(pointB.x, pointB.y + offset);  
CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y);

CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y);  
CGPoint c6 = CGPointMake(pointD.x, pointD.y + offset);

CGPoint c7 = CGPointMake(pointD.x, pointD.y - offset);  
CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y);

 

有了終點和控制點,就能夠用UIBezierPath 中提供的方法 - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2; 畫線段了。

重載CALayer的- (void)drawInContext:(CGContextRef)ctx;方法,在裏面畫圖案:

- (void)drawInContext:(CGContextRef)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);
    CGContextSetFillColorWithColor(ctx, self.indicatorColor.CGColor);
    CGContextFillPath(ctx);

}

 

如今,當你滑動ScrollView的時候,小球就會形變了。
如何用阻尼振動函數建立出60個關鍵幀?

上面的例子中,有個很重要的因素,就是ScrollView中的contentOffset.x這個變量,沒有這個輸入,那接下來什麼都不會發生。但想要得到這個變量,是須要用戶觸摸、滑動去交互產生的。在某個動畫中用戶是沒有直接的交互輸入的,好比當手指離開以後,要讓這個小球以果凍效果彈回初始狀態,這個過程手指已經離開屏幕,也就沒有了輸入,那麼用上面的方法確定行不通,因此,咱們能夠用CAAnimation.

咱們知道,iOS7中蘋果在 UIView(UIViewAnimationWithBlocks) 加入了一個新的製做彈性動畫的工廠方法:

+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

 

可是沒有直接的關於彈性的 CAAnimation 子類,相似CABasicAnimation或CAKeyframeAnimation 來直接給CALayer添加動畫。好消息是iOS9中添加了公開的 CASpringAnimation。可是出於兼容低版本以及對知識探求的角度,咱們能夠了解一下如何手動給CALayer建立一個彈性動畫。

在開始以前須要複習一下高中物理知識 ———— 阻尼振動,你能夠點擊高亮字體的連接稍微複習一下。

根據維基百科,咱們能夠獲得以下振動函數通式:

固然這只是一個通式,咱們須要讓 圖像過(0,0),而且最後衰減到1 。咱們可讓原圖像先繞X軸翻轉180度,也就是加一個負號。而後沿y軸向上平移一個單位。因此稍加變形能夠獲得以下函數:

想看函數的圖像?沒問題,推薦一個在線查看函數圖象的網站 —— Desmos ,把這段公式 1-\left(e^{-5x}\cdot \cos (30x)\right) 複製粘帖進去就能夠看到圖像。

改進後的函數圖像是這樣的:

完美知足了咱們 圖形過(0,0),震盪衰減到1 的要求。其中式子中的 5 至關於阻尼係數,數值越小幅度越大;式子中的 30 至關於震盪頻率 ,數值越大震盪次數越多。

接下來就須要轉換成代碼。

整體思路是建立60幀關鍵幀(由於屏幕的最高刷新頻率就是60FPS),而後把這60幀數據賦值給 CAKeyframeAnimation 的 values 屬性。

用如下代碼生成60幀後保存到一個數組並返回它,其中//1就是利用剛纔的公式建立60個數值:

+(NSMutableArray *) animationValues:(id)fromValue toValue:(id)toValue usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity duration:(CGFloat)duration{

    //60個關鍵幀
    NSInteger numOfPoints  = duration * 60;
    NSMutableArray *values = [NSMutableArray arrayWithCapacity:numOfPoints];
    for (NSInteger i = 0; i < numOfPoints; i++) {
        [values addObject:@(0.0)];
    }

    //差值
    CGFloat d_value = [toValue floatValue] - [fromValue floatValue];

    for (NSInteger point = 0; point<numOfPoints; point++) {

        CGFloat x = (CGFloat)point / (CGFloat)numOfPoints;
        CGFloat value = [toValue floatValue] - d_value * (pow(M_E, -damping * x) * cos(velocity * x)); //1 y = 1-e^{-5x} * cos(30x)

        values[point] = @(value);
    }

    return values;

}

 

接下來建立一個對外的類方法,並返回一個 CAKeyframeAnimation :

+(CAKeyframeAnimation *)createSpring:(NSString *)keypath duration:(CFTimeInterval)duration usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity fromValue:(id)fromValue toValue:(id)toValue{

    CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keypath];
    NSMutableArray *values = [KYSpringLayerAnimation animationValues:fromValue toValue:toValue usingSpringWithDamping:damping * dampingFactor initialSpringVelocity:velocity * velocityFactor duration:duration];
    anim.values = values;
    anim.duration = duration;

    return anim;
}

 

另外一個關鍵

以上,咱們建立了 CAKeyframeAnimation 。可是這些values究竟是對誰起做用的呢?若是你熟悉CoreAnimation的話,沒錯,是對傳入的keypath起做用。而這些keypath其實就是CALayer中的屬性@property。好比,之因此當傳入的keypath爲transform.rotation.x時CAKeyframeAnimation會讓layer發生旋轉,就是由於CAKeyframeAnimation發現CALayer中有這麼個屬性叫transform,因而動畫就發生了。如今咱們須要改變的是主題一中的那個factor變量,因此,很天然地想到,咱們能夠給CALayer補充一個屬性名爲factor就好了,這樣CAKeyframeAnimation加到layer上時發現layer有這個factor屬性,就會把60幀不一樣的values賦值給factor。固然咱們要把fromValue和toValue控制在0~1:

CAKeyframeAnimation *anim = [KYSpringLayerAnimation createSpring:@"factor" duration:0.8 usingSpringWithDamping:0.5 initialSpringVelocity:3 fromValue:@(1) toValue:@(0)];
self.factor = 0;
[self addAnimation:anim forKey:@"restoreAnimation"];

 

最後一步,雖然CAKeyframeAnimation實時地去改變了咱們想要的factor,但咱們還得通知屏幕刷新,這樣才能看到動畫。

+(BOOL)needsDisplayForKey:(NSString *)key{
    if ([key isEqual:@"factor"]) {
        return  YES;
    }
    return  [super needsDisplayForKey:key];
}

 

上面的代碼通知屏幕當factor發生變化時,實時刷新屏幕。

最後的最後,你須要重載CALayer中的-(id)initWithLayer:(GooeyCircle *)layer方法,爲了保證動畫能連貫起來,你須要拷貝前一個狀態的layer及其全部屬性。

-(id)initWithLayer:(GooeyCircle *)layer{
    self = [super initWithLayer:layer];
    if (self) {

        self.indicatorSize  = layer.indicatorSize;
        self.indicatorColor = layer.indicatorColor;
        self.currentRect = layer.currentRect;
        self.lastContentOffset = layer.lastContentOffset;
        self.scrollDirection = layer.scrollDirection;
        self.factor = layer.factor;
    }
    return self;
}

 

總結:

作自定義的動畫最關鍵的就是要有變量,要有輸入。像滑動ScrollView的時候,滑動的距離就是動畫的輸入,能夠做爲動畫的變量;當沒有交互的時候,能夠用CAAnimation。其實CAAnimation底層就有個定時器,而定時器的做用就是能夠產生變量,時間就是變量,就能夠產生變化的輸入,就能看到變化的狀態,連起來就是動畫了。

相關文章
相關標籤/搜索