FaceBook POP原始碼解析一

醬了裡個醬發表於2019-03-11

FaceBook POP原始碼解析二

FaceBook POP原始碼解析三

FaceBook POP原始碼解析四

一、前言

在解析pop原始碼之前,我們先通過程式導向的方式來實現系統動畫,來理解動畫的本質。因為如果從物件導向的方式直接去解析,很容易就繞到物件與物件之間的關聯中。

二、系統動畫

假設我們需要將一個view向下移動600px,那麼可以使用UIView Animation來實現

- (void)startSystemAnimation {
    [UIView animateWithDuration:1.0 delay:0.f options:UIViewAnimationOptionCurveEaseIn animations:^{
        self.animatedView.layer.transform = CATransform3DMakeTranslation(0, 300, 0);
    } completion:nil];
}
複製程式碼

對於這個簡單的系統動畫,我們需要分別瞭解以下內容:

  • duration:動畫的執行週期,一般我們可以通過設定來控制動畫的執行時間
  • options: 動畫的執行效果,我們可以通過選擇不同的UIViewAnimationOptions來實現不同的動畫效果
  • transform:動畫的執行物件,CATransform3D是一個使用者處理3D形變的類,可以改變控制元件的平移、縮放、旋轉等。

三、相關知識

簡單來說,檢視的動畫就是在一定時間內,動畫的屬性(位置、顏色或大小)發生線性或非線性的變化。假設我們要實現上面的系統動畫,那麼我們要明確時間、變化效果和變化物件三者。

  • 時間:動畫開始時間和動畫結束時間
  • 變化效果:動畫是勻速變化還是先加速後減速
  • 變化物件:需要修改何種屬性
1. CADisplayLink

CADisplayLink是一個可以讓我們以和螢幕重新整理率相同的頻率將內容渲染到螢幕上的定時器。與NSTimer最大的區別就是時間誤差小且呼叫時機與螢幕重新整理率相同,能夠保證動畫的流暢性。

2. UIViewAnimationOptions

該屬性定義了一系列的動畫效果,具體可以參考UIViewAnimationOptions,這次我們主要探討動畫速度控制的效果。

UIViewAnimationOptionCurveEaseInOut:動畫先加速、後減速[預設]。
UIViewAnimationOptionCurveEaseIn :動畫開始後逐漸加速。
UIViewAnimationOptionCurveEaseOut:動畫快結束時進行減速。
UIViewAnimationOptionCurveLinear :動畫勻速執行,預設值。
複製程式碼
3. CAMediaTimingFunction

該類主要定義了動畫的緩衝效果,上面UIViewAnimationOptions的四種動畫效果實際上是通過CAMediaTimingFunction來實現的,具體如下:

kCAMediaTimingFunctionLinear//勻速的線性計時函式
kCAMediaTimingFunctionEaseIn//緩慢加速,然後突然停止
kCAMediaTimingFunctionEaseOut//全速開始,慢慢減速
kCAMediaTimingFunctionEaseInEaseOut//慢慢加速再慢慢減速
kCAMediaTimingFunctionDefault//也是慢慢加速再慢慢減速,但是它加速減速速度略慢
複製程式碼
4. CATransform3D

在使用系統動畫時,我們通過CATransform3DMakeTranslation方法來修改檢視的位置,而CATransform3D本身是一個矩陣,它矩陣中對應的屬性如下:

struct CATransform3D{  
  CGFloat     m11(x縮放),   m12(y切變),   m13(旋轉),   m14();
  CGFloat     m21(x切變),   m22(y縮放),   m23,        m24;
  CGFloat     m31(旋轉),    m32,         m33,         m34(透視效果,要有旋轉角度才能看出效果);
  CGFloat     m41(x平移),   m42(y平移),   m43(z平移),  m44;
};
複製程式碼

如上所示,矩陣中對應的m42即為檢視y軸方向上的平移,通過修改該屬性我們就可以達到修改檢視y軸方向的位置了。

四、自定義動畫實現

1. 虛擬碼:簡單來說就是計算每一小段時間對應檢視的偏移,然後更新檢視的偏移。
t0: 開始時間
t1: 當前時間
t2: 結束時間
t1 = currentTime();
t2 = t0 + duration;
while (t1 < t2) {
    offset = caculateOffset(); //計算偏移量
    obj.offset = offset;
    t1 = currentTime(); //更新當前時間
}
複製程式碼
2.線性變化
- (void)startCustomAnimation {
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

static bool isStarted = NO;
static CFTimeInterval beginTime = 0;
static CFTimeInterval endTime = 0;

- (void)render:(CADisplayLink *)displayLink { //正常情況下,每秒呼叫60次,即每16ms呼叫一次
    CGFloat translationY = 300;
    CFTimeInterval duration = 1.0;
    if (!isStarted) { //記錄動畫開始時間和結束時間
        beginTime = CACurrentMediaTime();
        endTime = CACurrentMediaTime() + duration;
        isStarted = YES;
    }
    CFTimeInterval curTime = CACurrentMediaTime();
    if (curTime < endTime) {
        CFTimeInterval interval = curTime - beginTime;
        CGFloat offset = [self linearWithInterval:interval duration:duration translation:translationY]; //獲取當前時間對應的位置
        CATransform3D transform = CATransform3DIdentity;
        transform.m42 = offset; //更新檢視對應的位置
        self.animatedView.layer.transform = transform; 
    } else {
        displayLink.paused = YES; //動畫結束,停止計時器
        [displayLink invalidate];
    }
}
//線性計算檢視對應的偏移:勻速變化
- (CGFloat)linearWithInterval:(CFTimeInterval)interval duration:(CFTimeInterval)duration translation:(CGFloat)transation {
    return interval / duration * transation;
}
複製程式碼
3.非線性變化
  • CAMediaTimingFunction緩衝動畫的本質是使用三次貝塞爾曲線,通過曲線來描述動畫的變化速率,具體貝塞爾曲線相關內容可參考: 貝塞爾曲線

  • 求解三次貝塞爾曲線方程

三次貝塞爾曲線方程
其中P0和P3表示為曲線的起始點,P1和P2表示曲線的控制點,我們假設起始點分別為(0,0)和(1,1),那麼可以化簡公式為:B(t) = (3*P1-3*P2+1)*t^3 + (3*P2-6*P1)*t^2 + 3*P1*t

我們需要根據x(時間點)計算出f(x)偏移量,那麼需要先解出係數t來,係數t的求解一般採用牛頓法或二分法.下面先採用牛頓法進行求解,若沒有得到合適的解,再通過二分法來求解。

- (CGFloat)curveEaseInWithInterval:(CFTimeInterval)interval duration:(CFTimeInterval)duration translation:(CGFloat)transation {
    CAMediaTimingFunction *timingFunc = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    float timingControls[4];
    [timingFunc getControlPointAtIndex:1 values:&timingControls[0]];
    [timingFunc getControlPointAtIndex:2 values:&timingControls[2]];
    CGPoint controlPoint1 = CGPointMake(timingControls[0], timingControls[1]);
    CGPoint controlPoint2 = CGPointMake(timingControls[2], timingControls[3]); //獲取控制點位置
    
    CGFloat p1x = controlPoint1.x, p1y = controlPoint1.y;
    CGFloat p2x = controlPoint2.x, p2y = controlPoint2.y;
    CGFloat ax,bx,cx,ay,by,cy; //分別表示t,t^2和t^3前的係數
    cx = 3.0 * p1x;
    bx = 3.0 * (p2x - p1x) - cx;
    ax = 1.0 - cx - bx;
    
    cy = 3.0 * p1y;
    by = 3.0 * (p2y - p1y) - cy;
    ay = 1.0 - cy - by;
    
    CGFloat (^sampleCurveX)(CGFloat t) = ^CGFloat (CGFloat t) { //座標x中的三次貝塞爾曲線公式
        return ((ax * t + bx) * t + cx) * t; //秦九韶演算法
    };
    CGFloat (^sampleCurveY)(CGFloat t) = ^CGFloat (CGFloat t) { //座標y中的三次貝塞爾曲線公式
        return ((ay * t + by) * t + cy) * t;
    };
    CGFloat (^sampleCurveDerivativeX)(CGFloat t) = ^CGFloat (CGFloat t) { //求導函式
        return (3 * ax * t + 2.0 * bx) * t + cx;
    };
    
    CGFloat (^solveCurveX)(CGFloat x, CGFloat epsilon) = ^(CGFloat x, CGFloat epsilon) { //求解公式中的因子t
        CGFloat t0,t1,t2,x2,d2;
        int i;
        //牛頓法求解
        for (t2 = x, i = 0; i < 8; i ++) {
            x2 = sampleCurveX(t2) - x;
            if (fabs(x2) < epsilon) {
                return t2;
            }
            d2 = sampleCurveDerivativeX(t2);
            if (fabs(d2) < 1e-6) {
                break;
            }
            t2 = t2 - x2 / d2;
        }
        
        t0 = 0.0;
        t1 = 1.0;
        t2 = x;
        if (t2 < t0) {
            return t0;
        }
        if (t2 > t1) {
            return t1;
        }
        //二分法求解
        while (t0 < t1) {
            x2 = sampleCurveX(t2);
            if (fabs(x2 - x) < epsilon) {
                return t2;
            }
            if (x > x2) {
                t0 = t2;
            } else {
                t1 = t2;
            }
            t2 = (t1 - t0) * 0.5 + t0;
        }
        
        return t2;
    };
    
    CGFloat epsilon = 1.0 / (1000 * duration); //誤差允許範圍
    CGFloat t = solveCurveX(interval / duration, epsilon); //計算出係數t
    return sampleCurveY(t) * transation; //根據公式計算出時間點對應的偏移量,然後再乘以總偏移量
}
複製程式碼

五、總結

本文主要使用一種程式導向的方式來實現簡單的動畫,但其實它裡面已經包括了pop中的一些關鍵程式碼,只是pop是使用物件導向的方式來實現動畫,以保證動畫更靈活、更高效,關於pop的具體實現方式,後面會逐一介紹。

相關文章