CADisplayLink結合UIBezierPath的神奇妙用

做過iOS動畫的朋友都知道,動畫中一大頭疼之處就是彈性、形變之類扭曲的效果。iOS7開始,我們開始可以直接使用UiView的渲染動畫API實現簡單的彈性效果。

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

dampingRatio是阻尼系數,取值范圍0~1,決定彈性效果的明顯程度;

velocity是初速度。

除此之外,iOS7又出現了一個重量級的家伙:UIKit Dynamics,可以用很簡單的代碼實現非常逼真的物理效果。

當然,更強大的是Facebook開源的Pop這個介于CAAnimation 和 UIDynamics之間的動畫引擎,使用習慣和CAAnimation基本別無二致,很方便上手,而且動畫效果非常出色,幀頻非常高,所以看上去的動畫會很連貫順滑。

但是以上要實現那種很Q彈、形變的效果還是有點困難。知道我同時遇到了CADisplayLink和貝塞爾曲線UIBezierPath。下面就是一些結合CADisplayLink和UIBezierPath的案例,并附上了源代碼地址。

Github地址

Github地址

Github地址

博文

Github地址

1、什么是CADisplayLink

簡單地說,它就是一個定時器,每隔幾毫秒刷新一次屏幕。

CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器。我們在應用中創建一個新的CADisplayLink對象,把它添加到一個runloop中,并給它提供一個target和selector在屏幕刷新的時候調用。

一但CADisplayLink以特定的模式注冊到runloop之后,每當屏幕需要刷新的時候,runloop就會調用CADisplayLink綁定的target上的selector,這時target可以讀到CADisplayLink的每次調用的時間戳,用來準備下一幀顯示需要的數據。例如一個視頻應用使用時間戳來計算下一幀要顯示的視頻數據。在UI做動畫的過程中,需要通過時間戳來計算UI對象在動畫的下一幀要更新的大小等等。

在添加進runloop的時候我們應該選用高一些的優先級,來保證動畫的平滑。可以設想一下,我們在動畫的過程中,runloop被添加進來了一個高優先級的任務,那么,下一次的調用就會被暫停轉而先去執行高優先級的任務,然后在接著執行CADisplayLink的調用,從而造成動畫過程的卡頓,使動畫不流暢。

duration屬性:提供了每幀之間的時間,也就是屏幕每次刷新之間的的時間。我們可以使用這個時間來計算出下一幀要顯示的UI的數值。但是 duration只是個大概的時間,如果CPU忙于其它計算,就沒法保證以相同的頻率執行屏幕的繪制操作,這樣會跳過幾次調用回調方法的機會。

frameInterval屬性:是可讀可寫的NSInteger型值,標識間隔多少幀調用一次selector方法,默認值是1,即每幀都調用一次。如果每幀都調用一次的話,對于iOS設備來說那刷新頻率就是60HZ也就是每秒60次,如果將frameInterval設為2 那么就會兩幀調用一次,也就是變成了每秒刷新30次。

pause屬性:控制CADisplayLink的運行。當我們想結束一個CADisplayLink的時候,應該調用-(void)invalidate從runloop中刪除并刪除之前綁定的target跟selector

另外CADisplayLink不能被繼承。

CADisplayLink 與 NSTimer 有什么不同?

iOS設備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結束都被調用,精確度相當高。

NSTimer的精確度就顯得低了點,比如NSTimer的觸發時間到的時候,runloop如果在阻塞狀態,觸發時間就會推遲到下一個runloop周期。并且NSTimer新增了tolerance屬性,讓用戶可以設置可以容忍的觸發的時間的延遲范圍。

CADisplayLink使用場合相對專一,適合做UI的不停重繪,比如自定義動畫引擎或者視頻播放的渲染。

NSTimer的使用范圍要廣泛的多,各種需要單次或者循環定時處理的任務都可以使用。

CADisplayLink使用的例子

self.displayLink= [CADisplayLinkdisplayLinkWithTarget:selfselector:@selector(updateTextColor)];self.displayLink.paused=YES;? [self.displayLinkaddToRunLoop:[NSRunLoopcurrentRunLoop] forMode:NSRunLoopCommonModes];-(void)updateTextColor{}- (void)startAnimation{self.beginTime=CACurrentMediaTime();self.displayLink.paused=NO;}- (void)stopAnimation{self.displayLink.paused=YES;? [self.displayLinkinvalidate];self.displayLink=nil;}

給非UI對象添加動畫效果

我們知道動畫效果就是一個屬性的線性變化,比如UIView動畫的EasyInEasyOut。通過數值按照不同速率的變化我們能生成更接近真實世界的動畫效果。我們也可以利用這個特性來使一些其他屬性按照我們期望的曲線變化。比如當播放視頻時關掉視頻的聲音我可以通過CADisplayLink來實現一個EasyOut的漸出效果:先快速的降低音量,在慢慢的漸變到靜音。

注意

通常來講:iOS設備的刷新頻率事60HZ也就是每秒60次。那么每一次刷新的時間就是1/60秒 大概16.7毫秒。當我們的frameInterval值為1的時候我們需要保證的是CADisplayLink調用的target的函數計算時間不應該大于 16.7否則就會出現嚴重的丟幀現象。 在mac應用中我們使用的不是CADisplayLink而是CVDisplayLink它是基于C接口的用起來配置有些麻煩但是用起來還是很簡單的。

2、Demo

實現這個形變效果的基本思路就是三句話:用CADisplayLink以其自身毫秒級刷新屏幕的特點去不斷調用一個方法,這個方法里面畫一條貝塞爾曲線,并且貝塞爾曲線的控制點是個動點。

第一個gif的實現思路:

首先我們需要兩個輔助視圖,并使用UIView的彈性動畫usingSpringWithDamping實現類似下面的效果:

新建@interface JellyView : UIView

JellyView.m:

- (void)drawRect:(CGRect)rect {CGFloatyOffset =30.0;CGFloatwidth? =CGRectGetWidth(rect);CGFloatheight? =CGRectGetHeight(rect);UIBezierPath*path = [UIBezierPathbezierPath];? ? [path moveToPoint:CGPointMake(0.0, yOffset)];//去設置初始線段的起點CGPointcontrolPoint =CGPointMake(width /2, yOffset +self.sideToCenterDelta);? ? [path addQuadCurveToPoint:CGPointMake(width, yOffset) controlPoint:controlPoint];? ? [path addLineToPoint:CGPointMake(width, height)];? ? [path addLineToPoint:CGPointMake(0.0, height)];? ? [path closePath];CGContextRefcontext =UIGraphicsGetCurrentContext();CGContextAddPath(context, path.CGPath);? ? [fillColor set];CGContextFillPath(context);}

上面代碼繪制了一個封閉的貝塞爾曲線,初始時刻封閉曲線是一個四方的矩形,因為[path addQuadCurveToPoint:CGPointMake(width, yOffset) controlPoint:controlPoint];中的controlPoint的縱坐標和左右兩個定點縱坐標相等。但這其實是個動點。注意到CGPoint controlPoint = CGPointMake(width / 2, yOffset + self.sideToCenterDelta);,我們可以看到動點的縱坐標是由yOffset + self.sideToCenterDelta決定的。yOffset是固定的值。self.sideToCenterDelta等于兩個輔助視圖的高度差。最后通過CADisplayLink的實時繪制,我們可以就可以看到屏幕上出現的形變效果了。

ViewController.m:

先創建一個實例displayLink.

self.displayLink= [CADisplayLinkdisplayLinkWithTarget:selfselector:@selector(displayLinkAction:)];? [self.displayLinkaddToRunLoop:[NSRunLoopmainRunLoop] forMode:NSDefaultRunLoopMode];

實現刷新器綁定的方法:

-(void)displayLinkAction:(CADisplayLink*)dis{CALayer*sideHelperPresentationLayer? =? (CALayer*)[self.sideHelperView.layerpresentationLayer];CALayer*centerHelperPresentationLayer =? (CALayer*)[self.centerHelperView.layerpresentationLayer];CGPointposition = [[centerHelperPresentationLayer valueForKeyPath:@"position"]CGPointValue];CGRectcenterRect = [[centerHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];CGRectsideRect = [[sideHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];NSLog(@"Center:%@",NSStringFromCGRect(centerRect));NSLog(@"Side:%@",NSStringFromCGRect(sideRect));CGFloatnewJellyViewTopConstraint? ? ? =? position.y-CGRectGetMaxY(self.view.frame);self.jellyViewTopConstraint.constant= newJellyViewTopConstraint;? ? [self.jellyViewlayoutIfNeeded];self.jellyView.sideToCenterDelta= centerRect.origin.y- sideRect.origin.y;}

這里有個地方花了我好長時間,就是我們不能直接通過self.sideHelperView.layer和self.centerHelperView.layer獲取兩個輔助視圖動畫過程中的變化的坐標,得到的是一個恒定的終點狀態的坐標。要想獲得動畫過程中的每個狀態的坐標,我們需要使用layer的presentationLayer,并且通過valueForKeyPath:@"position"的方式實時獲取動態坐標。

!!最后千萬別忘了調用[self.jellyView setNeedsDisplay];,否則- (void)drawRect:(CGRect)rect不會called.

第二個gif的實現思路:

接下來的思路完全大同小異,只不過實時刷新的定時器從CADisplayLink換成了同樣具有實時調用功能的手勢:UIGestureRecognizerStateChanged。

新建@interface BounceView : UIView

BounceView.m:

先準備好一個CAShapeLayer,并且填充顏色用來顯示形變的圖形。

- (void) createLine {self.verticalLineLayer= [CAShapeLayerlayer];self.verticalLineLayer.strokeColor= [[UIColorwhiteColor]CGColor];self.verticalLineLayer.lineWidth=1.0;self.verticalLineLayer.fillColor= [[UIColorwhiteColor]CGColor];? ? [self.layeraddSublayer:self.verticalLineLayer];}

當手勢開始變化的時候,我們讓self.verticalLineLayer.path等于變化中的貝塞爾曲線的CGPath,并且把手指的偏移程度的變量CGFloat amountX = [gr translationInView:self].x傳過去;

self.verticalLineLayer.path= [selfgetLeftLinePathWithAmount:amountX];

貝塞爾曲線的變化代碼如下:

//左邊曲線- (CGPathRef) getLeftLinePathWithAmount:(CGFloat)amount {UIBezierPath*verticalLine = [UIBezierPathbezierPath];CGPointtopPoint =CGPointMake(0,0);CGPointmidControlPoint =CGPointMake(amount,self.bounds.size.height/2);CGPointbottomPoint =CGPointMake(0,self.bounds.size.height);? ? [verticalLine moveToPoint:topPoint];? ? [verticalLine addQuadCurveToPoint:bottomPoint controlPoint:midControlPoint];? ? [verticalLine closePath];return[verticalLineCGPath];}

代碼還是大同小異,無非就是改變控制點midControlPoint,只不過這里是改變它的橫坐標而已。

3、總結

歸根結底,要實現這個形變的Q彈效果無非就是一個實時調用一個繪制貝塞爾曲線的方法,并且這個貝塞爾曲線的控制點是一個動點。那個實時調用就有很多實現的辦法了。各種原生的代理方法,當然還包括文中提到了毫秒級刷新器CADisplayLink。期待你能做出更加動感的動畫:)


資料參考:

CADisplayLinkhttp://www.lxweimin.com/p/c35a81c3b9eb

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容