從CoreAnimation到Pop

pop是Facebook在開源的一款動畫引擎,看下其官方的介紹:

Pop是一款在iOS、tvOS和OS X平臺通用的可擴展動畫引擎。它在基本靜態動畫的基礎上,增加了彈性以及衰減動畫,這在創建真實有物裏性的交互很有用。其API能夠快速的整合進已有的Objective-C工程,可以對任意對象的任意屬性做動畫。這是一個成熟且經過測試的框架,在Paper這款優秀的app中有廣泛的應用。(iOS7之後蘋果也提供了Spring動畫(不過CASpringAnimation iOS9才提供)以及UIDynamic物理引擎(比如碰撞以及重力等物理效果不錯,有興趣可以玩玩))

那Pop動畫引擎跟CoreAnimation有啥區別?我們先來簡單瞭解一下蘋果的CoreAnimation:

CoreAnimation

先看下CoreAnimation在框架中所處的位置:

CoreAnimation.png

可以看出視圖的渲染以及動畫都是基於CoreAnimation框架(看名字容易以爲只是動畫相關),其地位還是相當重要。我們來看下iOS在視圖的渲染以及動畫的各個階段都發生了蝦米,這其中涉及到應用內部以及應用外部:

應用內部4個階段:

  • 佈局
    這個階段是用戶在程序內部設置組織視圖或圖層的關係,比如設置view的backgroundColor、frame等屬性;

  • 顯示
    這是圖層的寄宿圖片被繪製的階段,比如實現了-drawRect:或-drawLayer:inContext:方法,這些方法會這這個階段執行,這些繪製方法是由CPU在應用內部同步地完成,屬於離屏渲染。

  • 準備
    這個階段,CoreAnimation框架會將渲染視圖的各種屬性以及動畫的參數等數據準備好;同時這個階段還會解壓需要渲染的image。

  • 提交
    這是在應用內部發生的最後階段,CoreAnimation打包準備好的所有視圖/圖層以及動畫的屬性,然後通過IPC(進程間通信)發送到render server進行顯示,可以看到其實視圖的渲染以及動畫是在另外一個進程處理的。在iOS5和之前的版本是SpringBoard進程(同時管理着iOS的主屏),在iOS6之後的版本中叫做BackBoard。

應用外部2個階段:
一旦這些打包好的數據到達render server,這些數據會被反序列化成另一個叫做渲染樹的圖層樹,根據這個樹狀結構,render server做如下工作:

  • 根據layer的屬性值,如果圖層包含動畫,則計算其屬性的中間插值,然後設置OpenGL幾何形狀(紋理化的三角形)來執行渲染
  • 在屏幕上渲染可見的三角形

所以整個階段包含六個階段,如果有動畫,最後兩個階段會重複的執行。前五個階段都是通過CPU處理的,只有最後一個階段使用GPU。而且你能控制的只有前面兩個階段:佈局和顯示,剩下都是CoreAnimation框架在內部進行處理。

簡單瞭解完CoreAnimaton的工作方式之後,我們在來看看pop實現動畫的方式。

pop

CADisplayLink是一個和屏幕刷新率(每秒60幀)相同的定時器,pop實現的動畫就是基於該定時器,它在每一幀計根據指定的time function計算出動畫的中間值,然後將計算好的值賦給視圖或圖層(可以是任意對象)的屬性(比如透明度、frame等),當屬性發生變化之後,我們知道Core Animation會通過IPC把這些變化通知render server進行渲染,因此整個動畫過程變成是你的應用內部驅動的,render server則被動接受數據進行渲染,跟上面提到的Core Animation動畫方式有所不同;另一個不同是pop在動畫過程中改變的是model layer的狀態,不像Core Animation作用的是渲染樹的圖層樹,Core Animation動畫會在動畫結束後回到起始位置, model layer, presentation layer 和 render layer的區別有興趣可以去了解。

core_animation_basics_sublayer_hierarchies.png
Animate View

pop提供了幾種動畫,包括basic、Spring(彈簧)、Deacy(衰減)以及自定義的動畫


pop animation

其API跟Core Animation提供的API類似,我們來看看如何使用pop,包括以下幾個步驟:

// 1 選擇動畫類型 (POPBasicAnimation  POPSpringAnimation POPDecayAnimation)
POPSpringAnimation *springAnimation = [POPSpringAnimation animation];
springAnimation.springBounciness=16;
springAnimation.springSpeed=6;

// 2 選擇要對視圖或者圖層的屬性做動畫,比如我們想要縮放動畫,我們可以選擇:kPOPViewScaleXY。
//pop提供了一些屬性,包括視圖屬性:kPOPViewAlpha kPOPViewBackgroundColor kPOPViewBounds kPOPViewCenter kPOPViewFrame等,
//圖層屬性:kPOPLayerBackgroundColor kPOPLayerBounds kPOPLayerScaleXY kPOPLayerSize kPOPLayerOpacity kPOPLayerPosition等,具體可以查看POPAnimatableProperty.m文件
springAnimation.property = [POPAnimatableProperty propertyWithName:kPOPViewScaleXY];

// 3 設置動畫的終點值
springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.3, 1.3)];

// 4 爲動畫指定代理POPAnimatorDelegate(可選),
springAnimation.delegate = self;

// 5 將動畫添加到視圖或圖層中,開始做動畫
[_testView pop_addAnimation:springAnimation forKey:@"springAnimation"];

可以看到API與Core Animation的基本類似,熟悉的同學應該能很快使用上,具體的使用方式可以嘗試,比如Spring動畫的幾個參數的效果,實踐出真知~

Animate NSObject

pop除了可以對view或着layer做動畫之外,還可以對任意NSObject對象的屬性做動畫,其實動畫本質上也是離散的,當每秒內離散的數據足夠多的時候對於人眼來說就是連續的。因此對NSObject對象屬性做動畫本質上也是計算出一系列的離散值,比如對下面的對象做動畫,然後我們可以根據這些離散值來觀察pop的動畫曲線:

@interface AnimatableObject : NSObject
@property (nonatomic,assign) CGFloat propertyValue;
@end

@implementation AnimatableObject

- (void)setPropertyValue:(CGFloat)newValue{
    _propertyValue = newValue;
}

@end

上面的對象包含一個float類型的屬性,由於這個對象的屬性並不是pop提供的內建屬性(POPAnimatableProperty.mm中定義的),因此我們需要創建一個新的動畫屬性POPAnimatableProperty:

POPAnimatableProperty *valueProperty = [POPAnimatableProperty propertyWithName:@"value" initializer:^(POPMutableAnimatableProperty *prop) {
    prop.writeBlock=^(id obj, const CGFloat values[]) {
        [obj setPropertyValue:values[0]];
        [_values addObject:@(values[0])]; //收集值用於後面繪製觀察曲線
    };
    prop.readBlock = ^(id obj, CGFloat values[]) {
        values[0] = [obj propertyValue];
    };
}];

我們需要爲這個動畫屬性提供名稱以及writeBlock跟readBlock,block裏面定義如何將數值與對象屬性關聯,現在我們對這個對象做動畫並繪製相關的動畫曲線。
我們對object做basic動畫,採用easeInOut的時間函數:

POPBasicAnimation *animation = [POPBasicAnimation animation];
animation.property = valueProperty;
animation.fromValue = [NSNumber numberWithFloat:0];
animation.toValue = [NSNumber numberWithFloat:100];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.duration = 1.5;
animation.completionBlock = ^(POPAnimation *anim, BOOL finished){
    [self drawCurl:_values];
};
_animateObject = [[AnimatableObject alloc] init];
[_animateObject pop_addAnimation:animation forKey:@"easeInEaseOut"];

//根據獲取到的值來繪製曲線
-(void)drawCurl:(NSArray*)values
{
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(100, 350)];
    
    for (int i=0; i<[values count]; i++) {
        NSNumber *value = values[i];
        CGPoint point = CGPointZero;
        point.x = 100+i*(100/values.count);
        point.y = 350 - [value floatValue];
        [path addLineToPoint:point];
    }
    
    _layer.path = path.CGPath;
    [self.view.layer addSublayer:_layer];
}

可以看到繪製出如下的曲線:

easeInEaseOut

假如使用PopSpringAnimation做動畫:

POPSpringAnimation *springAni = [POPSpringAnimation animation];
springAni.property = valueProperty;
springAni.fromValue = [NSNumber numberWithFloat:0];
springAni.toValue = [NSNumber numberWithFloat:100];
springAni.dynamicsMass = 5;
springAni.completionBlock = ^(POPAnimation *anim, BOOL finished){
    [self drawCurl:_values];
};
_animateObject = [[AnimatableObject alloc] init];
[_animateObject pop_addAnimation:springAni forKey:@"springAnimation"];

可以看到是如下曲線,有興趣可以自己是試試其它曲線。

spring

實現原理

簡單瞭解完pop的使用方式,我們來繼續聊一聊pop的實現方式,爲了方便說明簡單分析下面的pop動畫,移動view的x位置:

POPBasicAnimation *basicAnimation = [POPBasicAnimation animation];
basicAnimation.property = [POPAnimatableProperty propertyWithName:kPOPLayerPositionX];
basicAnimation.toValue = @(200);
[_testView pop_addAnimation:basicAnimation forKey:nil];
  • pop內建屬性

kPOPLayerPositionX是pop內建的屬性,pop內置了常見的屬性動畫,保存在全局的靜態數組_staticStates[]中,對每個屬性定義好了讀取屬性值readBlock以及寫入屬性值的writeBlock(如果是自定義的屬性,則需要自己實現readBlock和writeBlock,如之前所示),

static POPStaticAnimatablePropertyState _staticStates[] =
{
    ...
    {kPOPLayerPositionX,
        ^(CALayer *obj, CGFloat values[]) {
          values[0] = [(CALayer *)obj position].x;
        },
        ^(CALayer *obj, const CGFloat values[]) {
          CGPoint p = [(CALayer *)obj position];
          p.x = values[0];
          [obj setPosition:p];
        },
        kPOPThresholdPoint
    },
    ...
 }
  • POPAnimator
    pop的動畫都是交給POPAnimator執行的,POPAnimator是一個負責執行動畫單例對象,這個對象會開啓一個CADisplayLink定時器,該定時器會在每幀執行動畫:
//POPAnimator.mm
- (id)init
{
    ...
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)];
    _displayLink.paused = YES;
    [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    ...
}

可以發現定時器是加到runloop的NSRunLoopCommonModes模式中的,這樣即便是UI滑動的時候也不會影響動畫的執行。

當我們使用pop_addAnimation把定義好的動畫加到POPAnimator對象時:

- (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key
{
  ...
  //POPAnimator會先判斷該動畫對象是否存在(所有動畫會保存在內部的一個字典對象中)了,如果存在就不重複添加執行動畫
  NSMutableDictionary *keyAnimationDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj);
  if (nil == keyAnimationDict) {
    keyAnimationDict = [NSMutableDictionary dictionary];
    CFDictionarySetValue(_dict, (__bridge void *)obj, (__bridge void *)keyAnimationDict);
  } else {
    POPAnimation *existingAnim = keyAnimationDict[key];
    if (existingAnim) {
      if (existingAnim == anim) {
        return;
      }
      [self removeAnimationForObject:obj key:key cleanupDict:NO];
    }
  }
  keyAnimationDict[key] = anim

  // 將動畫保存在_pendingList數組中
  _pendingList.push_back(item);

  // 開啓CADisplayLink定時器
  updateDisplayLink(self);

  //執行_pendingList數組中的動畫
  [self _scheduleProcessPendingList];
}

  • 基於NSRunLoop的動畫更新機制
    當我們有動畫需要被執行時,pop會在主線層的runloop中添加觀察者,監聽kCFAllocatorDefault、kCFRunLoopBeforeWaiting和kCFRunLoopExit事件,並在回調的時候處理執行_pendingList裏的動畫
- (void)_scheduleProcessPendingList
{
  ...

  if (!_pendingListObserver) {
    __weak POPAnimator *weakSelf = self;
    _pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      ...
      //在回調中執行_pendingList中的動畫
      CFTimeInterval time = [self _currentRenderTime];
      [self _renderTime:(0 != _beginTime) ? _beginTime : time items:_pendingList];
      ...
    });

    if (_pendingListObserver) {
      CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver,  kCFRunLoopCommonModes);
    }
  }
  ...
}
  • 渲染 pending 動畫
    當runloop觀察者的回調被執行時,POPAnimator會根據當前時間(需要這個時間去做插值)一個一個執行_pendingList裏的動畫:
- (void)_renderTime:(CFTimeInterval)time item:(POPAnimatorItemRef)item
{
    ...
    // 只執行有效的動畫
    if (state->active && !state->paused) {
      
      //根據當前時間執行動畫
      applyAnimationTime(obj, state, time);

      //如果動畫執行完畢
      if (state->isDone()) {

        //將計算好的值設給視圖或圖層對象
        applyAnimationToValue(obj, state);
        }
    }
    ...
}

static void applyAnimationTime(id obj, POPAnimationState *state, CFTimeInterval time)
{
    //根據當前時間計算推倒出新的值大小
    if (!state->advanceTime(time, obj)) {
        return;
    }
    POPPropertyAnimationState *ps = dynamic_cast<POPPropertyAnimationState*>(state);
    if (NULL != ps) {

        //將推倒出的新值作用到視圖或圖層對象
        updateAnimatable(obj, ps);
    }
}

pop會根據動畫類型做不同的插值算法,如下所示可以看到有四種不同的插值方式

bool advanceTime(CFTimeInterval time, id obj) {
    ...
    switch (type) {
      case kPOPAnimationSpring:
        advanced = advance(time, dt, obj);
        break;
      case kPOPAnimationDecay:
        advanced = advance(time, dt, obj);
        break;
      case kPOPAnimationBasic: {
        advanced = advance(time, dt, obj);
        computedProgress = true;
        break;
      }
      case kPOPAnimationCustom: {
        customFinished = [self _advance:obj currentTime:time elapsedTime:dt] ? false : true;
        advanced = true;
        break;
      }
     ...
}

我們以kPOPAnimationBasic方式爲例,

bool advance(CFTimeInterval time, CFTimeInterval dt, id obj) {
    
    //默認採用kCAMediaTimingFunctionDefault時間函數
    ((POPBasicAnimation *)self).timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];

    // 將時間歸一化到[0-1]
    CGFloat p = 1.0f;
    if (duration > 0.0f) {
        // cap local time to duration
        CFTimeInterval t = MIN(time - startTime, duration) / duration;
        p = POPTimingFunctionSolve(timingControlPoints, t, SOLVE_EPS(duration));
        timeProgress = t;
    } else {
        timeProgress = 1.;
    }

    //根據當前的時間,以及from和to的值計算出新的當前值
    interpolate(valueType, valueCount, fromVec->data(), toVec->data(), currentVec->data(), p);
    progress = p;

 }

計算出新的值後,便可以通過內建屬性定義好的writeBlock將新的值付給UI對象:

static void updateAnimatable(id obj, POPPropertyAnimationState *anim, bool shouldAvoidExtraneousWrite = false)
{
    pop_animatable_write_block write = anim->property.writeBlock;
     if (NULL == write)
        return;
    write(obj, currentVec->data());
}

pop動畫的過程大體上如上所示,也就是在每一幀將通過不同的曲線函數計算出新的插值並賦給UI對象,以此來實現動畫

custom animation

下面我們來看看如何通過pop來實現一個自定義的動畫,pop對自定義動畫的支持感覺比較單一,可以認爲就是一個定時器的功能而已。。。
想要自定義動畫我們就需要有一個自定義的函數曲線,比如我們要實現一個彈簧動畫(跟spring動畫類似),我們使用如下的時間函數,輸出爲[0-1](更多的緩動函數可以去這查看:http://easings.net/zh-cn):

float ElasticEaseOut(float p)
{
    return sin(-13 * M_PI_2 * (p + 1)) * pow(2, -6 * p) + 1;
}

當有了定義好的緩動曲線後,我們就可以通過POPCustomAnimation來實現自定義動畫,POPCustomAnimation會在每次CADisplayLink定時器觸發時回調我們定義好的函數,同時給我們傳遞相關的時間參數:

POPCustomAnimation *customAni = [POPCustomAnimation animationWithBlock:^BOOL(id target, POPCustomAnimation *animation) {
        
        //動畫開始的時間,我們可以記錄下來作爲基準時間
        if(_baseTime == 0){
            _baseTime = animation.currentTime;
        }

        //根據當前時間,計算出當前的時間進度,並根據動畫週期歸一化到[0-1]
        double progress = (animation.currentTime - _baseTime)/_duration;

        //使用ElasticEaseOut自定義曲線根據當前進度計算出新的值,該值大小也爲[0-1]
        double caculateValue = ElasticEaseOut(progress);

        //根據緩動函數的輸出,計算新的值,並賦給UI對象
        CGPoint current = CGPointZero;
        current.x = _from.x + (_to.x - _from.x) * caculateValue;
        current.y = _from.y + (_to.y - _from.y) * caculateValue;
        _testView.frame = CGRectMake(current.x, current.y, 20, 20);
        
        //如果當前進度小於1,則繼續動畫
        if(progress < 1.0){
            return YES;
        }
        return NO;
    }];
    [_testView pop_addAnimation:layCus forKey:@"custom"];

可以看到如下的彈簧效果,與spring效果類似:

ElasticEaseOut
總結

通過上面的介紹我們大概也瞭解了pop動畫引擎了,pop相比iOS的coreanimation的優勢在於提供了spring以及decay動畫效果,iOS7的spring動畫效果較弱,CASpringAnimation能夠提供的效果較好,不過需要iOS9或以上的版本,除此之外pop還允許你自定義動畫,所以pop還是有一定的吸引力。不過我們也可以發現pop動畫是在主線層執行的,因此如果主線層做耗時操作的話,動畫就不那麼流暢了,有興趣可以試一試。。。

參考:

ios核心動畫高級技巧
pop
緩動函數

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