pop是Facebook在開源的一款動畫引擎,看下其官方的介紹:
Pop是一款在iOS、tvOS和OS X平臺通用的可擴展動畫引擎。它在基本靜態動畫的基礎上,增加了彈性以及衰減動畫,這在創建真實有物裏性的交互很有用。其API能夠快速的整合進已有的Objective-C工程,可以對任意對象的任意屬性做動畫。這是一個成熟且經過測試的框架,在Paper這款優秀的app中有廣泛的應用。(iOS7之後蘋果也提供了Spring動畫(不過CASpringAnimation iOS9才提供)以及UIDynamic物理引擎(比如碰撞以及重力等物理效果不錯,有興趣可以玩玩))
那Pop動畫引擎跟CoreAnimation有啥區別?我們先來簡單瞭解一下蘋果的CoreAnimation:
CoreAnimation
先看下CoreAnimation在框架中所處的位置:
可以看出視圖的渲染以及動畫都是基於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的區別有興趣可以去了解。
Animate View
pop提供了幾種動畫,包括basic、Spring(彈簧)、Deacy(衰減)以及自定義的動畫
其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];
}
可以看到繪製出如下的曲線:
假如使用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"];
可以看到是如下曲線,有興趣可以自己是試試其它曲線。
實現原理
簡單瞭解完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效果類似:
總結
通過上面的介紹我們大概也瞭解了pop動畫引擎了,pop相比iOS的coreanimation的優勢在於提供了spring以及decay動畫效果,iOS7的spring動畫效果較弱,CASpringAnimation能夠提供的效果較好,不過需要iOS9或以上的版本,除此之外pop還允許你自定義動畫,所以pop還是有一定的吸引力。不過我們也可以發現pop動畫是在主線層執行的,因此如果主線層做耗時操作的話,動畫就不那麼流暢了,有興趣可以試一試。。。