仿Uber啟動(dòng)動(dòng)畫,內(nèi)附OC/Swift版代碼

這是一篇轉(zhuǎn)載的譯文,非常感謝譯者的分享,原譯文地址

可以在這下載到由本人所寫的OC版實(shí)現(xiàn)代碼,歡迎指正,歡迎Star

您還可以在這里找到由譯者更新至Swift 3.0的最終的Fuber工程,請(qǐng)使用Xcode 8.0 beta4 或更新版本打開。

-------------------

[譯文]如何制作一個(gè)類似Uber的濺落式啟動(dòng)屏

本文翻譯自 How To Create an Uber Splash Screen逞带, Derek Selander 發(fā)表于Raywenderlich预明。

受限于譯者英語(yǔ)水平及翻譯經(jīng)驗(yàn),譯文難免有詞不達(dá)意,甚至錯(cuò)誤的地方,還望不吝賜教,予以指正

-------------------

一個(gè)好的濺落式啟動(dòng)頁(yè)(別被毫無(wú)動(dòng)畫效果的靜態(tài)啟動(dòng)頁(yè)迷惑)须教,使開發(fā)人員有機(jī)會(huì)在展示動(dòng)畫期間,從后端獲取必要的數(shù)據(jù)斩芭。同時(shí)它在應(yīng)用啟動(dòng)期間讓用戶始終保持高昂興趣方面也發(fā)揮了重要作用轻腺。

雖然濺落式啟動(dòng)頁(yè)已廣泛存在,但是你很難找到一個(gè)如Uber這般出色的划乖。在2016年的首季贬养,Uber釋出一個(gè)由CEO領(lǐng)導(dǎo)的品牌重塑戰(zhàn)略,品牌重塑的成果之一琴庵,便是一個(gè)非常炫酷的濺落式啟動(dòng)頁(yè)误算。

本文以仿制Uber啟動(dòng)動(dòng)畫為目標(biāo)仰美。其中運(yùn)用了大量的CAlayerCAAnimation類,及其相應(yīng)子類儿礼。相對(duì)于概念介紹咖杂,本文更著重于如何運(yùn)用這些類去實(shí)現(xiàn)一個(gè)產(chǎn)品級(jí)的動(dòng)畫效果。如需了解動(dòng)畫背后的相關(guān)知識(shí)蚊夫,請(qǐng)?jiān)L問(wèn) Marin Todorov 的系列視頻教程:
Intermediate iOS Animation

開始

鑒于本文涉及的動(dòng)畫眾多诉字,這里提供一個(gè)已為后續(xù)動(dòng)畫創(chuàng)建好所有CALayer的起始工程

起始工程是一個(gè)叫做Fuber的應(yīng)用这橙,F(xiàn)uber提供(Segway)駕乘共享服務(wù)奏窑,乘客通過(guò)向Segway駕駛員發(fā)出請(qǐng)求导披,來(lái)邀請(qǐng)其搭載自己抵達(dá)城市的任何地方屈扎。Fuber發(fā)展迅速,已在60多個(gè)國(guó)家為用戶提供服務(wù)撩匕,但也面臨眾多國(guó)家的反對(duì)和工會(huì)要求其必須與司機(jī)簽訂合同的問(wèn)題鹰晨。:](原作者賣萌了)

<center>
Splash screen
Splash screen

</center>

最終,我們會(huì)創(chuàng)建一個(gè)如下的非常炫酷的濺落式啟動(dòng)頁(yè):

<center>
Fuber Animation
Fuber Animation

</center>

打開并運(yùn)行起始工程止毕,簡(jiǎn)單瀏覽一下工程結(jié)構(gòu)模蜡。

首先從視圖控制器開始,應(yīng)用通過(guò)負(fù)責(zé)子視圖(切入)切出任務(wù)的RootContainerViewController加載SplashViewController扁凛。父視圖控制器從啟動(dòng)頁(yè)開始運(yùn)行忍疾,直至應(yīng)用的所有準(zhǔn)備工作全部完成。這期間應(yīng)用會(huì)連接到后端谨朝,獲取后續(xù)所需數(shù)據(jù)卤妒。需要指出的是,在這個(gè)簡(jiǎn)單的項(xiàng)目中啟動(dòng)頁(yè)被設(shè)計(jì)成了一個(gè)獨(dú)立的模塊字币。

RootContainerViewController中已經(jīng)實(shí)現(xiàn)好了兩個(gè)方法:showSplashViewController()showSplashViewControllerNoPing()则披。
由于教程中大部分時(shí)間,都在調(diào)用showSplashViewControllerNoPing()方法(調(diào)試啟動(dòng)動(dòng)畫)洗出,所以我們先將精力放在SplashViewController的子視圖動(dòng)畫創(chuàng)建上士复,然后在通過(guò)showSplashViewController()模擬一個(gè)訪問(wèn)API的延遲效果,并隨即跳轉(zhuǎn)到主視圖控制器翩活。

濺落式啟動(dòng)頁(yè)視圖及其圖層結(jié)構(gòu)

SplashViewController的視圖(view)包含兩個(gè)子視圖(subview)阱洪。 第一個(gè)子視圖是用于構(gòu)成波紋網(wǎng)格背景的TileGridview,它包含了一系列按網(wǎng)格排列的TileView實(shí)例菠镇。另一個(gè)子視圖名為AnimatedULogoView冗荸,它構(gòu)成了 U 字型的動(dòng)畫圖標(biāo)。

<center>
Splash Screen
Splash Screen

</center>

AnimatedULogoView包含4個(gè)CAShapeLayer:

  • circleLayer 用于實(shí)現(xiàn)字母 U 的白色背景
  • lineLayer 用于實(shí)現(xiàn)從circleLayer的中心到邊緣的一條線段
  • squareLayer 用于實(shí)現(xiàn)位于circleLayer中心位置的方塊
  • maskLayer 用作視圖遮罩辟犀,通過(guò)改變其bounds的動(dòng)畫效果俏竞,來(lái)將其它所有圖層的動(dòng)畫效果整齊劃一地混合起來(lái)绸硕。

通過(guò)組合這幾個(gè)CAShaperLayer動(dòng)畫,共同實(shí)現(xiàn)了Fuber中字母 U 的動(dòng)畫效果魂毁。

<center>
RiderIconView
RiderIconView

</center>

了解了圖層的構(gòu)成之后玻佩,接下來(lái)我們就來(lái)添加一些動(dòng)畫讓AnimatedULogoView動(dòng)起來(lái)吧。

讓圓形動(dòng)起來(lái)

創(chuàng)建復(fù)雜動(dòng)畫的關(guān)鍵席楚,在于排除視覺干擾專注于我們正在實(shí)現(xiàn)的部分咬崔。 打開AnimatedULogoView.swift文件。找到init(frame:)方法烦秩,注釋掉除circleLayer外其它向視圖中添加子圖層(sublayer)的方法垮斯,完成動(dòng)畫后會(huì)再將其全部添加回來(lái)。注釋完成后的代碼如下:

override init(frame: CGRect) {
  super.init(frame: frame)
 
  circleLayer = generateCircleLayer()
  lineLayer = generateLineLayer()
  squareLayer = generateSquareLayer()
  maskLayer = generateMaskLayer()
 
//  layer.mask = maskLayer
  layer.addSublayer(circleLayer)
//  layer.addSublayer(lineLayer)
//  layer.addSublayer(squareLayer)
}

找到generateCircleLayer()方法只祠,了解下圓形是如何被創(chuàng)建的兜蠕。其實(shí)只是簡(jiǎn)單地通過(guò) UIBezierPath 創(chuàng)建了一個(gè) CAShapeLayer (圖層)。 注意看這行代碼:

layer.path = UIBezierPath(arcCenter: CGPointZero, 
                             radius: radius/2, 
                         startAngle: -CGFloat(M_PI_2), 
                           endAngle: CGFloat(3*M_PI_2),
                          clockwise: true).CGPath

startAngle 傳入 0 或使用默認(rèn)值, 弧線會(huì)從右側(cè)(3點(diǎn)鐘位置)開始抛寝。傳入 -M_PI_2 即 -90度, 則會(huì)從頂部開始熊杨,如果 endAngle 恰好是270度即 3 * M_PI_2,弧線則再次回到頂點(diǎn)(形成一個(gè)圓形)盗舰。注意為了繪制的動(dòng)畫效果晶府,我們使用圓形的半徑作為lineWidth

circleLayer的動(dòng)畫需要三個(gè)CAAnimation子類來(lái)實(shí)現(xiàn):一個(gè)作用于stokeEndCAKeyframeAnimation動(dòng)畫钻趋,一個(gè)作用于transformCABasicAnimation動(dòng)畫川陆,和一個(gè)負(fù)責(zé)將兩部分動(dòng)畫組合起來(lái)的CAAnimationGroup。這將一次性同時(shí)創(chuàng)建所有動(dòng)畫蛮位。

在事先寫好的animateCircleLayer()方法中添加如下代碼:

  // strokeEnd
  let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
  strokeEndAnimation.timingFunction = strokeEndTimingFunction
  strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay
  strokeEndAnimation.values = [0.0, 1.0]
  strokeEndAnimation.keyTimes = [0.0, 1.0]

通過(guò)向動(dòng)畫的Values屬性提供的 0.0 和 1.0速兔,我們便透過(guò)Core Animation框架生成了一個(gè)從 startAngleendAngle 沿順時(shí)針旋轉(zhuǎn)的動(dòng)畫衔彻。隨著 strokeEnd 屬性值的增加审磁,弧線沿著圓周慢慢伸展啡浊,圓形也漸漸被"填滿"。在這個(gè)例子中陶因,如果我們將values屬性的值設(shè)為[0.0, 0.5]骡苞,則僅會(huì)畫半個(gè)圓,這是因?yàn)?StrokeEnd 在動(dòng)畫結(jié)束時(shí)剛達(dá)好到圓周的一半楷扬。

譯者注:“圓形也漸漸被‘填滿’”一句的填滿是引起來(lái)的解幽,并不是真的被填滿,而是描邊的 lineWidth 與圓形半徑相同烘苹,從而產(chǎn)生了填滿的視覺效果躲株。可參考generateCircleLayer()方法中layer.fillColor = UIColor.clear.cgColor這段代碼镣衡,事實(shí)上填充色被設(shè)置為透明霜定,

現(xiàn)在添加形變(transform)動(dòng)畫:

  // transform
  let transformAnimation = CABasicAnimation(keyPath: "transform")
  transformAnimation.timingFunction = strokeEndTimingFunction
  transformAnimation.duration = kAnimationDuration - kAnimationDurationDelay
 
  var startingTransform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0, 0, 1)
  startingTransform = CATransform3DScale(startingTransform, 0.25, 0.25, 1)
  transformAnimation.fromValue = NSValue(CATransform3D: startingTransform)
  transformAnimation.toValue = NSValue(CATransform3D: CATransform3DIdentity)

該動(dòng)畫同時(shí)實(shí)現(xiàn)了放大和沿 Z 軸旋轉(zhuǎn)的兩個(gè)形變档悠。這使得圓形在沿順時(shí)針旋轉(zhuǎn)45度的同時(shí)逐漸變大。這里的旋轉(zhuǎn)很重要望浩,因?yàn)閳A形的旋轉(zhuǎn)要與lineLayer和其它圖層一塊動(dòng)起來(lái)時(shí)的位置和速度保持一致辖所。

最后在animateCircleLayer()方法的最下面添加一個(gè)CAAnimationGroup。這個(gè)動(dòng)畫組將包含之前的兩個(gè)動(dòng)畫磨德,這樣我們僅向circleLayer圖層添加一次動(dòng)畫即可缘回。

  // Group
  let groupAnimation = CAAnimationGroup()
  groupAnimation.animations = [strokeEndAnimation, transformAnimation]
  groupAnimation.repeatCount = Float.infinity
  groupAnimation.duration = kAnimationDuration
  groupAnimation.beginTime = beginTime
  groupAnimation.timeOffset = startTimeOffset
 
  circleLayer.addAnimation(groupAnimation, forKey: "looping")

這里我們修改了CAAnimationGroup的兩個(gè)重要屬性:beginTimetimeOffset。如果你對(duì)其中任何一個(gè)不熟悉典挑,那么你都可以在這里找到關(guān)于該屬性的介紹和使用說(shuō)明酥宴。
groupAnimationbeginTime 設(shè)置為與父視圖相同。

對(duì)timeOffeset的設(shè)置是必要的您觉,因?yàn)閯?dòng)畫首次運(yùn)行時(shí)實(shí)際上是從一半開始的拙寡。當(dāng)完成更多動(dòng)畫效果后,你可以試著改變startTimeOffset的值顾犹,并觀察動(dòng)畫在視覺效果上的不同倒庵。

將動(dòng)畫組添加到circleLayer之后,編譯并運(yùn)行應(yīng)用炫刷,檢查下動(dòng)畫效果.

<center>
Splash Screen CircleIn Animation
Splash Screen CircleIn Animation

</center>

注意: 試著刪除groupAnimation.animations數(shù)組中的strokeEndAnimationtransformAnimation,以確認(rèn)每個(gè)動(dòng)畫具體實(shí)現(xiàn)了哪些視覺效果. 可以按該方法再去驗(yàn)證一下文中的其它動(dòng)畫郁妈,你會(huì)驚訝于浑玛,僅僅改變動(dòng)畫的組合方式就可以產(chǎn)生如此令人難以預(yù)料的獨(dú)特視覺效果.

讓線段動(dòng)起來(lái)

完成了circleLayer的動(dòng)畫, 接下來(lái)我們?cè)賮?lái)完成lineLayer動(dòng)畫。還是在 AnimatedULogoView.swift文件中, 找到startAnimating()方法并注釋掉除animateLineLayer()外的所有動(dòng)畫調(diào)用噩咪。注釋后的代碼如下:

public func startAnimating() {
  beginTime = CACurrentMediaTime()
  layer.anchorPoint = CGPointZero
 
//  animateMaskLayer()
//  animateCircleLayer()
  animateLineLayer()
//  animateSquareLayer()
}

此外, 修改init(frame:)方法中的代碼顾彰,只顯示circleLayerlineLayer兩個(gè)圖層:

override init(frame: CGRect) {
  super.init(frame: frame)
 
  circleLayer = generateCircleLayer()
  lineLayer = generateLineLayer()
  squareLayer = generateSquareLayer()
  maskLayer = generateMaskLayer()
 
//  layer.mask = maskLayer
  layer.addSublayer(circleLayer)
  layer.addSublayer(lineLayer)
//  layer.addSublayer(squareLayer)
}

注釋掉圖層和動(dòng)畫后, 轉(zhuǎn)到animateLineLayer()方法并實(shí)現(xiàn)下面這組動(dòng)畫:

  // lineWidth
  let lineWidthAnimation = CAKeyframeAnimation(keyPath: "lineWidth")
  lineWidthAnimation.values = [0.0, 5.0, 0.0]
  lineWidthAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]
  lineWidthAnimation.duration = kAnimationDuration
  // Swift 3.0 keyTimes是一個(gè)NSNumber數(shù)組
  lineWidthAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

該動(dòng)畫會(huì)使lineLayer的寬度(width)呈現(xiàn)出先增后減的效果。

再為接下來(lái)的動(dòng)畫添加如下代碼:

  // transform
  let transformAnimation = CAKeyframeAnimation(keyPath: "transform")
  transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]
  transformAnimation.duration = kAnimationDuration
  transformAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]
 
  var transform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0.0, 0.0, 1.0)
  transform = CATransform3DScale(transform, 0.25, 0.25, 1.0)
  transformAnimation.values = [NSValue(CATransform3D: transform),
                               NSValue(CATransform3D: CATransform3DIdentity),
                               NSValue(CATransform3D: CATransform3DMakeScale(0.15, 0.15, 1.0))]

circleLayer的形變動(dòng)畫非常相似, 這里我們定義了個(gè)一個(gè)沿 Z 軸順時(shí)針旋轉(zhuǎn)的動(dòng)畫胃碾。 此外我們還為線段添加了一個(gè)先縮小到25%涨享,再恢復(fù)到原有尺寸,最后再縮小到15%的形變動(dòng)畫.

通過(guò)CAAnimationGroup將動(dòng)畫組合起來(lái)仆百,并添加到lineLayer上:

  // Group
  let groupAnimation = CAAnimationGroup()
  groupAnimation.repeatCount = Float.infinity
  groupAnimation.removedOnCompletion = false
  groupAnimation.duration = kAnimationDuration
  groupAnimation.beginTime = beginTime
  groupAnimation.animations = [lineWidthAnimation, transformAnimation]
  groupAnimation.timeOffset = startTimeOffset
 
  lineLayer.addAnimation(groupAnimation, forKey: "looping")

編譯并運(yùn)行厕隧,注意觀察變化.

<center>
Splash Screen Knockoutline Animation
Splash Screen Knockoutline Animation

</center>

注意我們?cè)O(shè)置了相同的初始形變值-M_PI_4,以便線段(line)和圓形(circle)在繪制時(shí)能對(duì)齊俄周。為此我們還將keyTimes 設(shè)置為[0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]吁讨。 數(shù)組中首尾兩個(gè)元素是確定的: 0 代表動(dòng)畫開始那一刻,1.0 代表動(dòng)畫結(jié)束那一刻峦朗,然后通過(guò)計(jì)算來(lái)獲取圓形繪制剛剛結(jié)束建丧、第二部分的動(dòng)畫即將開始時(shí)的那一刻。由于它是一個(gè)延遲的動(dòng)畫效果波势,所以我們還需要從 1.0 中減去通過(guò)kAnimationDurationDelay除以kAnimationDuration而得到的確切百分比翎朱,這是因?yàn)槲覀兿胱寗?dòng)畫在結(jié)束后的延遲過(guò)程中再返回到起點(diǎn)橄维。(譯者:形成一個(gè)循環(huán)動(dòng)畫,否則會(huì)出現(xiàn)跳躍拴曲,致使動(dòng)畫不連貫)

circleLayerlineLayer動(dòng)畫都已完成挣郭,接下來(lái)我們?cè)撏瓿芍虚g的方塊動(dòng)畫了。

讓方塊動(dòng)起來(lái)

與之前類似疗韵。 在startAnimating()函數(shù)中注釋掉除animateSquareLayer外的其它動(dòng)畫方法調(diào)用兑障。然后在像下面這樣修改init(frame:)方法的代碼:

override init(frame: CGRect) {
  super.init(frame: frame)
 
  circleLayer = generateCircleLayer()
  lineLayer = generateLineLayer()
  squareLayer = generateSquareLayer()
  maskLayer = generateMaskLayer()
 
//  layer.mask = maskLayer
  layer.addSublayer(circleLayer)
//  layer.addSublayer(lineLayer)
  layer.addSublayer(squareLayer)
}

完成后轉(zhuǎn)到animateSquareLayer()方法實(shí)現(xiàn)如下動(dòng)畫代碼:

  // bounds
  let b1 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: 2.0/3.0 * squareLayerLength, height: 2.0/3.0  * squareLayerLength))
  let b2 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: squareLayerLength, height: squareLayerLength))
  let b3 = NSValue(CGRect: CGRectZero)
 
  let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds")
  boundsAnimation.values = [b1, b2, b3]
  boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction]
  boundsAnimation.duration = kAnimationDuration
  boundsAnimation.keyTimes = [0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

這一部分動(dòng)畫用于改變CALayer的大小(bounds)蕉汪。創(chuàng)建一個(gè)先將其邊長(zhǎng)縮小到2/3流译,再恢復(fù),最終在縮小到零的關(guān)鍵幀動(dòng)畫者疤。

接下來(lái)福澡,為背景色添加動(dòng)畫效果:

  // backgroundColor
  let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor")
  backgroundColorAnimation.fromValue = UIColor.whiteColor().CGColor
  backgroundColorAnimation.toValue = UIColor.fuberBlue().CGColor
  backgroundColorAnimation.timingFunction = squareLayerTimingFunction
  backgroundColorAnimation.fillMode = kCAFillModeBoth
  backgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDuration
  backgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)

注意上面的fillMode屬性。一旦beginTime不為零時(shí), 動(dòng)畫就會(huì)在起始點(diǎn)和結(jié)束點(diǎn)保持住當(dāng)前顏色(CGColor)驹马。這避免了動(dòng)畫在被添加到父CAAnimationGroup時(shí)出現(xiàn)閃爍革砸。(譯者:這里譯的不好:(。請(qǐng)?jiān)囍淖冊(cè)搶傩缘脑O(shè)置糯累,看看視覺效果上有什么不同算利,以加深理解。)

了解了這些泳姐,我們就動(dòng)手來(lái)實(shí)現(xiàn)一下吧:

  // Group
  let groupAnimation = CAAnimationGroup()
  groupAnimation.animations = [boundsAnimation, backgroundColorAnimation]
  groupAnimation.repeatCount = Float.infinity
  groupAnimation.duration = kAnimationDuration
  groupAnimation.removedOnCompletion = false
  groupAnimation.beginTime = beginTime
  groupAnimation.timeOffset = startTimeOffset
  squareLayer.addAnimation(groupAnimation, forKey: "looping")

編譯并運(yùn)行檢查動(dòng)畫效果效拭。注意觀察方塊的變化。

<center>
Splash Screen Tutorial
Splash Screen Tutorial

</center>

現(xiàn)在將所有的動(dòng)畫組合起來(lái)看看效果如何胖秒!

注意: 在電腦的GPU完成對(duì)iOS設(shè)備的模擬任務(wù)前缎患,模擬器上的動(dòng)畫可能會(huì)有那么一點(diǎn)小抽。如果你的電腦帶不動(dòng)動(dòng)畫阎肝,可以試著將模擬器窗口調(diào)小或者轉(zhuǎn)到真機(jī)開發(fā)挤渔。

遮罩

首先,取消init(frame:)方法中對(duì)所有添加圖層方法的注釋风题,以及startAnimating()方法中對(duì)所有動(dòng)畫調(diào)用的注釋.

組合好所有動(dòng)畫后判导,再次編譯并運(yùn)行。

<center>
PreMask Animation
PreMask Animation

</center>

看上去還是有點(diǎn)怪怪的俯邓,是不是骡楼?圓形在縮小時(shí),它的邊緣會(huì)有一個(gè)小跳躍稽鞭。幸運(yùn)地是, 遮罩動(dòng)畫可以解決該問(wèn)題鸟整,讓所有子圖動(dòng)畫平滑整齊劃一.

轉(zhuǎn)到`animateMaskLayer()`方法并添加如下代碼:

  // bounds
  let boundsAnimation = CABasicAnimation(keyPath: "bounds")
  boundsAnimation.fromValue = NSValue(CGRect: CGRect(x: 0.0, 
                                                     y: 0.0, 
                                                 width: radius * 2.0, 
                                                height: radius * 2))
  boundsAnimation.toValue = NSValue(CGRect: CGRect(x: 0.0, 
                                                   y: 0.0, 
                                               width: 2.0/3.0 * squareLayerLength,
                                              height: 2.0/3.0 * squareLayerLength))
  boundsAnimation.duration = kAnimationDurationDelay
  boundsAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
  boundsAnimation.timingFunction = circleLayerTimingFunction

這是一個(gè)邊界(bounds)動(dòng)畫。記住朦蕴,由于這是一個(gè)應(yīng)用于所有子圖層的遮罩篮条,當(dāng)邊界發(fā)生變化時(shí), 整個(gè)AnimatedULogoView都將消失弟头,直至遮罩被應(yīng)用到所有子圖層。

現(xiàn)在在添加一個(gè)讓方塊變圓的圓角動(dòng)畫:

  // cornerRadius
  let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
  cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
  cornerRadiusAnimation.duration = kAnimationDurationDelay
  cornerRadiusAnimation.fromValue = radius
  cornerRadiusAnimation.toValue = 2
  cornerRadiusAnimation.timingFunction = circleLayerTimingFunction

將這兩個(gè)動(dòng)畫添加到一個(gè)CAAnimationGroup中涉茧,以完成這個(gè)圖層(的所有動(dòng)畫):

  // Group
  let groupAnimation = CAAnimationGroup()
  groupAnimation.removedOnCompletion = false
  groupAnimation.fillMode = kCAFillModeBoth
  groupAnimation.beginTime = beginTime
  groupAnimation.repeatCount = Float.infinity
  groupAnimation.duration = kAnimationDuration
  groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation]
  groupAnimation.timeOffset = startTimeOffset
  maskLayer.addAnimation(groupAnimation, forKey: "looping")

編譯并運(yùn)行赴恨。

<center>
RiderIconView Animation
RiderIconView Animation

</center>

看起來(lái)好多了!

網(wǎng)格

試想一下有一系列以TileGridView實(shí)例的方式來(lái)移動(dòng)的 UIView伴栓。 它們看起來(lái)會(huì)是什么樣呢伦连?呃。钳垮。惑淳。這里就不引用創(chuàng)并展開說(shuō)明了!(譯者:《創(chuàng)》是一部科幻電影饺窿。這里翻譯的不好歧焦,見諒!)

網(wǎng)格背景由一些列附加到TileGridView類的TileView組成肚医。為了便于從視覺上理解這個(gè)概念, 我們打開TileView.swift文件绢馍,找到init(frame:)方法,在方法的最后添加如下代碼:

layer.borderWidth = 2.0

編譯并運(yùn)行應(yīng)用肠套。

<center>
Fuber-Grid-View
Fuber-Grid-View

</center>

如果你所見舰涌,TileView被整齊地排成一張網(wǎng)格。整個(gè)創(chuàng)建邏輯都集中在TileGridView.swift文件的renderTileViews()方法內(nèi)糠排。幸運(yùn)的是舵稠,我們所需的布局邏輯(起始工程)已經(jīng)實(shí)現(xiàn)好。接下來(lái)要做的就是讓它動(dòng)起來(lái)!

讓瓦片視圖(TileView)動(dòng)起來(lái)

TileGridView僅有一個(gè)直接的子視圖(subview)containerView入宦。它負(fù)責(zé)添加所有的TileView。 此外室琢,還有一個(gè)名為tileViewRows的屬性, 它是一個(gè)二維數(shù)組乾闰,包含所有添加到containerView中的TileView

回到TileView中的init(frame:)方法. 刪除我們剛才添加的用于顯示邊界的代碼盈滴,并取消向圖層中添加chimeSplashImage方法的注釋涯肩。完成后的方法如下:

override init(frame: CGRect) {
  super.init(frame: frame)
  layer.contents = TileView.chimesSplashImage.CGImage
  layer.shouldRasterize = true
}

編譯并運(yùn)行。

<center>
Grid Starting
Grid Starting

</center>

酷巢钓。病苗。。症汹。我們就要大功告成了硫朦。

然而,TileGridView(以及它的TileView們)還需要添加一些動(dòng)畫效果背镇。打開TileView.swift文件咬展,找到startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:) 方法并添加如下動(dòng)畫代碼:

  let timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.2, 1)
  let linearFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
  let easeOutFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
  let easeInOutTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
  let zeroPointValue = NSValue(CGPoint: CGPointZero)
 
  var animations = [CAAnimation]()

這段代碼設(shè)置了一系列我們即將用到的時(shí)間函數(shù)泽裳。繼續(xù)添加下面的代碼:

  if shouldEnableRipple {
    // Transform.scale
    let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
    scaleAnimation.values = [1, 1, 1.05, 1, 1]
    scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes
    scaleAnimation.timingFunctions = [linearFunction, 
                                      timingFunction, 
                                      timingFunction, 
                                      linearFunction]
    scaleAnimation.beginTime = 0.0
    scaleAnimation.duration = duration
    animations.append(scaleAnimation)
 
    // Position
    let positionAnimation = CAKeyframeAnimation(keyPath: "position")
    positionAnimation.duration = duration
    positionAnimation.timingFunctions = [linearFunction, 
                                         timingFunction, 
                                         timingFunction, 
                                         linearFunction]
    positionAnimation.keyTimes = TileView.rippleAnimationKeyTimes
    positionAnimation.values = [zeroPointValue, 
                                zeroPointValue, 
                                NSValue(CGPoint:rippleOffset), 
                                zeroPointValue, 
                                zeroPointValue]
    positionAnimation.additive = true
 
    animations.append(positionAnimation)
  }

shouldEnableRipple是個(gè)布爾值,用于控制何時(shí)將形變動(dòng)畫和位置動(dòng)畫添加到我們剛剛創(chuàng)建的數(shù)組中破婆。在通過(guò)renderTileViews()方法創(chuàng)建時(shí)涮总,所有未處在TileGridView外圍邊緣的TileView,就已將shouldEnableRipple設(shè)為true祷舀。

添加一個(gè)不透明動(dòng)畫:

  // Opacity
  let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
  opacityAnimation.duration = duration
  opacityAnimation.timingFunctions = [easeInOutTimingFunction, 
                                      timingFunction, 
                                      timingFunction, 
                                      easeOutFunction, 
                                      linearFunction]
  opacityAnimation.keyTimes = [0.0, 0.61, 0.7, 0.767, 0.95, 1.0]
  opacityAnimation.values = [0.0, 1.0, 0.45, 0.6, 0.0, 0.0]
  animations.append(opacityAnimation)

該動(dòng)畫簡(jiǎn)單明了瀑梗,只是設(shè)置了一些非常特殊的的keyTimes

現(xiàn)在將這些動(dòng)畫添加到一個(gè)動(dòng)畫組中:

  // Group
  let groupAnimation = CAAnimationGroup()
  groupAnimation.repeatCount = Float.infinity
  groupAnimation.fillMode = kCAFillModeBackwards
  groupAnimation.duration = duration
  groupAnimation.beginTime = beginTime + rippleDelay
  groupAnimation.removedOnCompletion = false
  groupAnimation.animations = animations
  groupAnimation.timeOffset = kAnimationTimeOffset
 
  layer.addAnimation(groupAnimation, forKey: "ripple")

這會(huì)將groupAnimation添加到TileView實(shí)例上裳扯。注意抛丽,動(dòng)畫組會(huì)因shouldEnableRipple值的不同而可能包含一個(gè)或三個(gè)動(dòng)畫。

現(xiàn)在我們已經(jīng)為每一個(gè)TileView實(shí)現(xiàn)了動(dòng)畫, 接下來(lái)需要在TileGridView中去調(diào)用它們嚎朽。打開TileGridView.swift文件將以下代碼添加到startAnimatingWithBeginTime(_:)方法中:

private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
  for tileRows in tileViewRows {
    for view in tileRows {
      view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, 
                                                             rippleDelay: 0, 
                                                            rippleOffset: CGPointZero)
    }
  }
}

編譯并運(yùn)行铺纽。

<center>
Grid-1
Grid-1

</center>

嗯。哟忍。狡门。看上去已經(jīng)好多了锅很,但AnimatedULogoView的跳動(dòng)應(yīng)該通過(guò)TileView向外產(chǎn)生一個(gè)類似水波的漣漪效果其馏。這就意味還需要?jiǎng)?chuàng)建一個(gè),基于從中央視圖(view)到外圍視圖之間距離的爆安,用于與一個(gè)常數(shù)相乘的延遲系數(shù)叛复。

緊挨著startAnimatingWithBeginTime(_:)函數(shù)下面, 添加如下的一個(gè)新函數(shù):

private func distanceFromCenterViewWithView(view: UIView)->CGFloat {
  guard let centerTileView = centerTileView else { return 0.0 }
 
  let normalizedX = (view.center.x - centerTileView.center.x)
  let normalizedY = (view.center.y - centerTileView.center.y)
  return sqrt(normalizedX * normalizedX + normalizedY * normalizedY)
}

該方法可以便捷地獲取到,指定視圖與位于中心的視圖扔仓,兩個(gè)視圖(TileView)中心點(diǎn)之間的距離褐奥。

回到startAnimatingWithBeginTime(_:)函數(shù),將其內(nèi)容替換為如下代碼:

  for tileRows in tileViewRows {
    for view in tileRows {
      let distance = self.distanceFromCenterViewWithView(view)
 
      view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)
    }
  }

這里通過(guò)剛剛添加的distanceFromCenterViewWithView(_:)函數(shù)翘簇,來(lái)計(jì)算(每個(gè)子視圖)動(dòng)畫的延遲啟動(dòng)時(shí)間撬码。

編譯運(yùn)行.

<center>
Grid-2
Grid-2

</center>

好多了! 動(dòng)畫現(xiàn)在看上去已經(jīng)有模有樣了, 但還是少點(diǎn)什么。TileView應(yīng)該像水波一樣版保,向四周逐漸擴(kuò)散開來(lái)呜笑。

解決該問(wèn)的最好方法就是拿出自己的高中數(shù)學(xué)知識(shí),然后根據(jù)Tileview與中心點(diǎn)間距離來(lái)得到一個(gè)量化的頂點(diǎn)彻犁。

distanceFromCenterViewWithView(_:)函數(shù)下面再添加一個(gè)函數(shù):

private func normalizedVectorFromCenterViewToView(view: UIView)->CGPoint {
  let length = self.distanceFromCenterViewWithView(view)
  guard let centerTileView = centerTileView where length != 0 else { return CGPointZero }
 
  let deltaX = view.center.x - centerTileView.center.x
  let deltaY = view.center.y - centerTileView.center.y
  return CGPoint(x: deltaX / length, y: deltaY / length)
}

回到startAnimatingWithBeginTime(_:)方法叫胁,將代碼修改如下:

private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
  for tileRows in tileViewRows {
    for view in tileRows {
 
      let distance = self.distanceFromCenterViewWithView(view)
      var vector = self.normalizedVectorFromCenterViewToView(view)
 
      vector = CGPoint(x: vector.x * kRippleMagnitudeMultiplier * distance, 
                       y: vector.y * kRippleMagnitudeMultiplier * distance)
 
      view.startAnimatingWithDuration(kAnimationDuration, 
                                      beginTime: beginTime, 
                                      rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), 
                                      rippleOffset: vector)
    }
  }
}

這會(huì)通過(guò) rippleOffset 計(jì)算位于每個(gè)頂點(diǎn)(vector)的 TileView 的偏移量。

編譯運(yùn)行一下.

<center>
Grid-3
Grid-3

</center>

太棒了! 接下來(lái)是點(diǎn)睛之筆:添加一個(gè)放大的效果汞幢,這個(gè)放大的動(dòng)畫效果要?jiǎng)偤迷谡谡诌吔纾╞ounds)發(fā)生改變之前驼鹅。

startAnimatingWithBeginTime(_:)函數(shù)的開始位置,添加如下代碼:

  let linearTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
  
  let keyframe = CAKeyframeAnimation(keyPath: "transform.scale")
  keyframe.timingFunctions = [linearTimingFunction, 
                              CAMediaTimingFunction(controlPoints: 0.6, 0.0, 0.15, 1.0), 
                              linearTimingFunction]
  keyframe.repeatCount = Float.infinity;
  keyframe.duration = kAnimationDuration
  keyframe.removedOnCompletion = false
  keyframe.keyTimes = [0.0, 0.45, 0.887, 1.0]
  keyframe.values = [0.75, 0.75, 1.0, 1.0]
  keyframe.beginTime = beginTime
  keyframe.timeOffset = kAnimationTimeOffset
 
  containerView.layer.addAnimation(keyframe, forKey: "scale")

再次編譯并運(yùn)行。

<center>
FuberFinal
FuberFinal

</center>

漂亮谤民,我們已經(jīng)創(chuàng)建了一個(gè)產(chǎn)品級(jí)的動(dòng)畫效果堰酿,會(huì)有大一批的Fuber用戶在微博(Twitter)上為此點(diǎn)贊的!:](作者又賣了個(gè)萌U抛恪)

注意:試著改變kRippleMagnitudeMultiplierkRippleDelayMultiplier的值触创,看看會(huì)有什么有趣的事發(fā)生。

接下收尾为牍,在RootContainerViewController.swift文件中哼绑,將viewDidLoad()最后一行代碼showSplashViewControllerNoPing()改為showSplashViewController()

最后在編譯運(yùn)行一次碉咆,欣賞下自己的工作成果吧

<center>
Fuber Animation
Fuber Animation

</center>

給自己點(diǎn)個(gè)贊吧抖韩,這是一個(gè)非常炫酷的濺落式啟動(dòng)頁(yè)。

接下來(lái)

可以在這下載到OC版實(shí)現(xiàn)代碼,歡迎Star

可以在這下載到最終的Fuber工程.

如果想了解更多關(guān)于動(dòng)畫的知識(shí)疫铜,請(qǐng)?jiān)L問(wèn)這里的iOS動(dòng)畫教程.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茂浮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子壳咕,更是在濱河造成了極大的恐慌席揽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谓厘,死亡現(xiàn)場(chǎng)離奇詭異幌羞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)竟稳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門属桦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人他爸,你說(shuō)我怎么就攤上這事聂宾。” “怎么了诊笤?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵亏吝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我盏混,道長(zhǎng),這世上最難降的妖魔是什么惜论? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任许赃,我火速辦了婚禮,結(jié)果婚禮上馆类,老公的妹妹穿的比我還像新娘混聊。我一直安慰自己,他們只是感情好乾巧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布句喜。 她就那樣靜靜地躺著预愤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咳胃。 梳的紋絲不亂的頭發(fā)上植康,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音展懈,去河邊找鬼销睁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛存崖,可吹牛的內(nèi)容都是我干的冻记。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼来惧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冗栗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起供搀,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤隅居,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后趁曼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體军浆,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年挡闰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乒融。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摄悯,死狀恐怖赞季,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奢驯,我是刑警寧澤申钩,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站瘪阁,受9級(jí)特大地震影響撒遣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜管跺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一义黎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豁跑,春花似錦廉涕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宠纯。三九已至,卻和暖如春层释,著一層夾襖步出監(jiān)牢的瞬間婆瓜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工湃累, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勃救,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓治力,卻偏偏與公主長(zhǎng)得像蒙秒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宵统,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果晕讲,實(shí)現(xiàn)這些動(dòng)畫的過(guò)程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌马澈。在這里你可以看...
    每天刷兩次牙閱讀 8,465評(píng)論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果瓢省,實(shí)現(xiàn)這些動(dòng)畫的過(guò)程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌痊班。在這里你可以看...
    F麥子閱讀 5,094評(píng)論 5 13
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,519評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)勤婚、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,029評(píng)論 4 62
  • 今天比較忙涤伐,而且沒有靈感馒胆,于是選了一幅簡(jiǎn)單的畫臨摹。上了淡淡的紅色凝果,用黃色稍微提亮祝迂,效果還不錯(cuò)。
    棉花糖nancy閱讀 128評(píng)論 0 1