pop是Facebook在開源的一款動(dòng)畫引擎据某,看下其官方的介紹:
Pop是一款在iOS墩朦、tvOS和OS X平臺(tái)通用的可擴(kuò)展動(dòng)畫引擎勺阐。它在基本靜態(tài)動(dòng)畫的基礎(chǔ)上,增加了彈性以及衰減動(dòng)畫盾似,這在創(chuàng)建真實(shí)有物里性的交互很有用敬辣。其API能夠快速的整合進(jìn)已有的Objective-C工程,可以對(duì)任意對(duì)象的任意屬性做動(dòng)畫。這是一個(gè)成熟且經(jīng)過測(cè)試的框架溉跃,在Paper這款優(yōu)秀的app中有廣泛的應(yīng)用村刨。(iOS7之后蘋果也提供了Spring動(dòng)畫(不過CASpringAnimation iOS9才提供)以及UIDynamic物理引擎(比如碰撞以及重力等物理效果不錯(cuò),有興趣可以玩玩))
那Pop動(dòng)畫引擎跟CoreAnimation有啥區(qū)別撰茎?我們先來簡單了解一下蘋果的CoreAnimation:
CoreAnimation
先看下CoreAnimation在框架中所處的位置:
可以看出視圖的渲染以及動(dòng)畫都是基于CoreAnimation框架(看名字容易以為只是動(dòng)畫相關(guān))嵌牺,其地位還是相當(dāng)重要。我們來看下iOS在視圖的渲染以及動(dòng)畫的各個(gè)階段都發(fā)生了蝦米龄糊,這其中涉及到應(yīng)用內(nèi)部以及應(yīng)用外部:
應(yīng)用內(nèi)部4個(gè)階段:
布局
這個(gè)階段是用戶在程序內(nèi)部設(shè)置組織視圖或圖層的關(guān)系,比如設(shè)置view的backgroundColor枯饿、frame等屬性诡必;顯示
這是圖層的寄宿圖片被繪制的階段,比如實(shí)現(xiàn)了-drawRect:或-drawLayer:inContext:方法搔扁,這些方法會(huì)這這個(gè)階段執(zhí)行,這些繪制方法是由CPU在應(yīng)用內(nèi)部同步地完成稿蹲,屬于離屏渲染。準(zhǔn)備
這個(gè)階段苛聘,CoreAnimation框架會(huì)將渲染視圖的各種屬性以及動(dòng)畫的參數(shù)等數(shù)據(jù)準(zhǔn)備好涂炎;同時(shí)這個(gè)階段還會(huì)解壓需要渲染的image。提交
這是在應(yīng)用內(nèi)部發(fā)生的最后階段唱捣,CoreAnimation打包準(zhǔn)備好的所有視圖/圖層以及動(dòng)畫的屬性网梢,然后通過IPC(進(jìn)程間通信)發(fā)送到render server進(jìn)行顯示,可以看到其實(shí)視圖的渲染以及動(dòng)畫是在另外一個(gè)進(jìn)程處理的拣宰。在iOS5和之前的版本是SpringBoard進(jìn)程(同時(shí)管理著iOS的主屏)烦感,在iOS6之后的版本中叫做BackBoard。
應(yīng)用外部2個(gè)階段:
一旦這些打包好的數(shù)據(jù)到達(dá)render server手趣,這些數(shù)據(jù)會(huì)被反序列化成另一個(gè)叫做渲染樹的圖層樹,根據(jù)這個(gè)樹狀結(jié)構(gòu)气笙,render server做如下工作:
- 根據(jù)layer的屬性值,如果圖層包含動(dòng)畫缸棵,則計(jì)算其屬性的中間插值谭期,然后設(shè)置OpenGL幾何形狀(紋理化的三角形)來執(zhí)行渲染
- 在屏幕上渲染可見的三角形
所以整個(gè)階段包含六個(gè)階段,如果有動(dòng)畫踏志,最后兩個(gè)階段會(huì)重復(fù)的執(zhí)行胀瞪。前五個(gè)階段都是通過CPU處理的,只有最后一個(gè)階段使用GPU凄诞。而且你能控制的只有前面兩個(gè)階段:布局和顯示,剩下都是CoreAnimation框架在內(nèi)部進(jìn)行處理伪朽。
簡單了解完CoreAnimaton的工作方式之后汛蝙,我們?cè)趤砜纯磒op實(shí)現(xiàn)動(dòng)畫的方式。
pop
CADisplayLink是一個(gè)和屏幕刷新率(每秒60幀)相同的定時(shí)器坚洽,pop實(shí)現(xiàn)的動(dòng)畫就是基于該定時(shí)器西土,它在每一幀計(jì)根據(jù)指定的time function計(jì)算出動(dòng)畫的中間值,然后將計(jì)算好的值賦給視圖或圖層(可以是任意對(duì)象)的屬性(比如透明度绘雁、frame等)援所,當(dāng)屬性發(fā)生變化之后,我們知道Core Animation會(huì)通過IPC把這些變化通知render server進(jìn)行渲染挪略,因此整個(gè)動(dòng)畫過程變成是你的應(yīng)用內(nèi)部驅(qū)動(dòng)的,render server則被動(dòng)接受數(shù)據(jù)進(jìn)行渲染挽牢,跟上面提到的Core Animation動(dòng)畫方式有所不同摊求;另一個(gè)不同是pop在動(dòng)畫過程中改變的是model layer的狀態(tài),不像Core Animation作用的是渲染樹的圖層樹室叉,Core Animation動(dòng)畫會(huì)在動(dòng)畫結(jié)束后回到起始位置茧痕, model layer, presentation layer 和 render layer的區(qū)別有興趣可以去了解。
Animate View
pop提供了幾種動(dòng)畫曼氛,包括basic令野、Spring(彈簧)舀患、Deacy(衰減)以及自定義的動(dòng)畫
其API跟Core Animation提供的API類似彩掐,我們來看看如何使用pop灰追,包括以下幾個(gè)步驟:
// 1 選擇動(dòng)畫類型 (POPBasicAnimation POPSpringAnimation POPDecayAnimation)
POPSpringAnimation *springAnimation = [POPSpringAnimation animation];
springAnimation.springBounciness=16;
springAnimation.springSpeed=6;
// 2 選擇要對(duì)視圖或者圖層的屬性做動(dòng)畫,比如我們想要縮放動(dòng)畫朴下,我們可以選擇:kPOPViewScaleXY苦蒿。
//pop提供了一些屬性佩迟,包括視圖屬性:kPOPViewAlpha kPOPViewBackgroundColor kPOPViewBounds kPOPViewCenter kPOPViewFrame等,
//圖層屬性:kPOPLayerBackgroundColor kPOPLayerBounds kPOPLayerScaleXY kPOPLayerSize kPOPLayerOpacity kPOPLayerPosition等灸姊,具體可以查看POPAnimatableProperty.m文件
springAnimation.property = [POPAnimatableProperty propertyWithName:kPOPViewScaleXY];
// 3 設(shè)置動(dòng)畫的終點(diǎn)值
springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.3, 1.3)];
// 4 為動(dòng)畫指定代理POPAnimatorDelegate(可選)秉溉,
springAnimation.delegate = self;
// 5 將動(dòng)畫添加到視圖或圖層中碗誉,開始做動(dòng)畫
[_testView pop_addAnimation:springAnimation forKey:@"springAnimation"];
可以看到API與Core Animation的基本類似哮缺,熟悉的同學(xué)應(yīng)該能很快使用上甲喝,具體的使用方式可以嘗試,比如Spring動(dòng)畫的幾個(gè)參數(shù)的效果茎匠,實(shí)踐出真知~
Animate NSObject
pop除了可以對(duì)view或著layer做動(dòng)畫之外押袍,還可以對(duì)任意NSObject對(duì)象的屬性做動(dòng)畫,其實(shí)動(dòng)畫本質(zhì)上也是離散的汽馋,當(dāng)每秒內(nèi)離散的數(shù)據(jù)足夠多的時(shí)候?qū)τ谌搜蹃碚f就是連續(xù)的圈盔。因此對(duì)NSObject對(duì)象屬性做動(dòng)畫本質(zhì)上也是計(jì)算出一系列的離散值驱敲,比如對(duì)下面的對(duì)象做動(dòng)畫,然后我們可以根據(jù)這些離散值來觀察pop的動(dòng)畫曲線:
@interface AnimatableObject : NSObject
@property (nonatomic,assign) CGFloat propertyValue;
@end
@implementation AnimatableObject
- (void)setPropertyValue:(CGFloat)newValue{
_propertyValue = newValue;
}
@end
上面的對(duì)象包含一個(gè)float類型的屬性握牧,由于這個(gè)對(duì)象的屬性并不是pop提供的內(nèi)建屬性(POPAnimatableProperty.mm中定義的)娩梨,因此我們需要?jiǎng)?chuàng)建一個(gè)新的動(dòng)畫屬性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];
};
}];
我們需要為這個(gè)動(dòng)畫屬性提供名稱以及writeBlock跟readBlock狈定,block里面定義如何將數(shù)值與對(duì)象屬性關(guān)聯(lián),現(xiàn)在我們對(duì)這個(gè)對(duì)象做動(dòng)畫并繪制相關(guān)的動(dòng)畫曲線措嵌。
我們對(duì)object做basic動(dòng)畫芦缰,采用easeInOut的時(shí)間函數(shù):
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"];
//根據(jù)獲取到的值來繪制曲線
-(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做動(dòng)畫:
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"];
可以看到是如下曲線饺藤,有興趣可以自己是試試其它曲線。
實(shí)現(xiàn)原理
簡單了解完pop的使用方式罗丰,我們來繼續(xù)聊一聊pop的實(shí)現(xiàn)方式萌抵,為了方便說明簡單分析下面的pop動(dòng)畫,移動(dòng)view的x位置:
POPBasicAnimation *basicAnimation = [POPBasicAnimation animation];
basicAnimation.property = [POPAnimatableProperty propertyWithName:kPOPLayerPositionX];
basicAnimation.toValue = @(200);
[_testView pop_addAnimation:basicAnimation forKey:nil];
- pop內(nèi)建屬性
kPOPLayerPositionX是pop內(nèi)建的屬性霎桅,pop內(nèi)置了常見的屬性動(dòng)畫讨永,保存在全局的靜態(tài)數(shù)組_staticStates[]中,對(duì)每個(gè)屬性定義好了讀取屬性值readBlock以及寫入屬性值的writeBlock(如果是自定義的屬性揭糕,則需要自己實(shí)現(xiàn)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的動(dòng)畫都是交給POPAnimator執(zhí)行的吏口,POPAnimator是一個(gè)負(fù)責(zé)執(zhí)行動(dòng)畫單例對(duì)象产徊,這個(gè)對(duì)象會(huì)開啟一個(gè)CADisplayLink定時(shí)器冬殃,該定時(shí)器會(huì)在每幀執(zhí)行動(dòng)畫:
//POPAnimator.mm
- (id)init
{
...
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)];
_displayLink.paused = YES;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
...
}
可以發(fā)現(xiàn)定時(shí)器是加到runloop的NSRunLoopCommonModes模式中的叁怪,這樣即便是UI滑動(dòng)的時(shí)候也不會(huì)影響動(dòng)畫的執(zhí)行奕谭。
當(dāng)我們使用pop_addAnimation把定義好的動(dòng)畫加到POPAnimator對(duì)象時(shí):
- (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key
{
...
//POPAnimator會(huì)先判斷該動(dòng)畫對(duì)象是否存在(所有動(dòng)畫會(huì)保存在內(nèi)部的一個(gè)字典對(duì)象中)了,如果存在就不重復(fù)添加執(zhí)行動(dòng)畫
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
// 將動(dòng)畫保存在_pendingList數(shù)組中
_pendingList.push_back(item);
// 開啟CADisplayLink定時(shí)器
updateDisplayLink(self);
//執(zhí)行_pendingList數(shù)組中的動(dòng)畫
[self _scheduleProcessPendingList];
}
- 基于NSRunLoop的動(dòng)畫更新機(jī)制
當(dāng)我們有動(dòng)畫需要被執(zhí)行時(shí)官册,pop會(huì)在主線層的runloop中添加觀察者难捌,監(jiān)聽kCFAllocatorDefault、kCFRunLoopBeforeWaiting和kCFRunLoopExit事件员淫,并在回調(diào)的時(shí)候處理執(zhí)行_pendingList里的動(dòng)畫
- (void)_scheduleProcessPendingList
{
...
if (!_pendingListObserver) {
__weak POPAnimator *weakSelf = self;
_pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
...
//在回調(diào)中執(zhí)行_pendingList中的動(dòng)畫
CFTimeInterval time = [self _currentRenderTime];
[self _renderTime:(0 != _beginTime) ? _beginTime : time items:_pendingList];
...
});
if (_pendingListObserver) {
CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes);
}
}
...
}
- 渲染 pending 動(dòng)畫
當(dāng)runloop觀察者的回調(diào)被執(zhí)行時(shí)介返,POPAnimator會(huì)根據(jù)當(dāng)前時(shí)間(需要這個(gè)時(shí)間去做插值)一個(gè)一個(gè)執(zhí)行_pendingList里的動(dòng)畫:
- (void)_renderTime:(CFTimeInterval)time item:(POPAnimatorItemRef)item
{
...
// 只執(zhí)行有效的動(dòng)畫
if (state->active && !state->paused) {
//根據(jù)當(dāng)前時(shí)間執(zhí)行動(dòng)畫
applyAnimationTime(obj, state, time);
//如果動(dòng)畫執(zhí)行完畢
if (state->isDone()) {
//將計(jì)算好的值設(shè)給視圖或圖層對(duì)象
applyAnimationToValue(obj, state);
}
}
...
}
static void applyAnimationTime(id obj, POPAnimationState *state, CFTimeInterval time)
{
//根據(jù)當(dāng)前時(shí)間計(jì)算推倒出新的值大小
if (!state->advanceTime(time, obj)) {
return;
}
POPPropertyAnimationState *ps = dynamic_cast<POPPropertyAnimationState*>(state);
if (NULL != ps) {
//將推倒出的新值作用到視圖或圖層對(duì)象
updateAnimatable(obj, ps);
}
}
pop會(huì)根據(jù)動(dòng)畫類型做不同的插值算法圣蝎,如下所示可以看到有四種不同的插值方式
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) {
//默認(rèn)采用kCAMediaTimingFunctionDefault時(shí)間函數(shù)
((POPBasicAnimation *)self).timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
// 將時(shí)間歸一化到[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.;
}
//根據(jù)當(dāng)前的時(shí)間牲证,以及from和to的值計(jì)算出新的當(dāng)前值
interpolate(valueType, valueCount, fromVec->data(), toVec->data(), currentVec->data(), p);
progress = p;
}
計(jì)算出新的值后从隆,便可以通過內(nèi)建屬性定義好的writeBlock將新的值付給UI對(duì)象:
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動(dòng)畫的過程大體上如上所示缭裆,也就是在每一幀將通過不同的曲線函數(shù)計(jì)算出新的插值并賦給UI對(duì)象,以此來實(shí)現(xiàn)動(dòng)畫
custom animation
下面我們來看看如何通過pop來實(shí)現(xiàn)一個(gè)自定義的動(dòng)畫辛燥,pop對(duì)自定義動(dòng)畫的支持感覺比較單一缝其,可以認(rèn)為就是一個(gè)定時(shí)器的功能而已。榴都。漠其。
想要自定義動(dòng)畫我們就需要有一個(gè)自定義的函數(shù)曲線,比如我們要實(shí)現(xiàn)一個(gè)彈簧動(dòng)畫(跟spring動(dòng)畫類似)拴驮,我們使用如下的時(shí)間函數(shù)柴信,輸出為[0-1](更多的緩動(dòng)函數(shù)可以去這查看:http://easings.net/zh-cn):
float ElasticEaseOut(float p)
{
return sin(-13 * M_PI_2 * (p + 1)) * pow(2, -6 * p) + 1;
}
當(dāng)有了定義好的緩動(dòng)曲線后,我們就可以通過POPCustomAnimation來實(shí)現(xiàn)自定義動(dòng)畫潜沦,POPCustomAnimation會(huì)在每次CADisplayLink定時(shí)器觸發(fā)時(shí)回調(diào)我們定義好的函數(shù),同時(shí)給我們傳遞相關(guān)的時(shí)間參數(shù):
POPCustomAnimation *customAni = [POPCustomAnimation animationWithBlock:^BOOL(id target, POPCustomAnimation *animation) {
//動(dòng)畫開始的時(shí)間窃判,我們可以記錄下來作為基準(zhǔn)時(shí)間
if(_baseTime == 0){
_baseTime = animation.currentTime;
}
//根據(jù)當(dāng)前時(shí)間喇闸,計(jì)算出當(dāng)前的時(shí)間進(jìn)度,并根據(jù)動(dòng)畫周期歸一化到[0-1]
double progress = (animation.currentTime - _baseTime)/_duration;
//使用ElasticEaseOut自定義曲線根據(jù)當(dāng)前進(jìn)度計(jì)算出新的值唆樊,該值大小也為[0-1]
double caculateValue = ElasticEaseOut(progress);
//根據(jù)緩動(dòng)函數(shù)的輸出刻蟹,計(jì)算新的值舆瘪,并賦給UI對(duì)象
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);
//如果當(dāng)前進(jìn)度小于1,則繼續(xù)動(dòng)畫
if(progress < 1.0){
return YES;
}
return NO;
}];
[_testView pop_addAnimation:layCus forKey:@"custom"];
可以看到如下的彈簧效果淀衣,與spring效果類似:
總結(jié)
通過上面的介紹我們大概也了解了pop動(dòng)畫引擎了召调,pop相比iOS的coreanimation的優(yōu)勢(shì)在于提供了spring以及decay動(dòng)畫效果唠叛,iOS7的spring動(dòng)畫效果較弱,CASpringAnimation能夠提供的效果較好艺沼,不過需要iOS9或以上的版本,除此之外pop還允許你自定義動(dòng)畫调鲸,所以pop還是有一定的吸引力剩拢。不過我們也可以發(fā)現(xiàn)pop動(dòng)畫是在主線層執(zhí)行的徐伐,因此如果主線層做耗時(shí)操作的話募狂,動(dòng)畫就不那么流暢了角雷,有興趣可以試一試性穿。。吗坚。