一款Loading動畫的實現思路系列已經結束了,非常感謝大家的捧場。
看過本系列的同學可能還記得,我對原動效做了簡化,為了讓大家回憶一下,也讓新來的同學有點印象,我先貼一下原動畫效果圖:
可以看到,圓被上方的豎線壓扁的時候,發生了不規則的變形,具體來說,圓的頂部比底部變形明顯。
這個很好理解,我們把球放到地上,拿手指去按它,手指按下的地方,肯定要比球和地面接觸的地方變形更明顯。
在Loading系列中我做了簡化,圓只是簡單的變成了橢圓,如下圖:
雖然效果也不錯,但還是有點遺憾,所以今天我們一起看一下,圓的不規則變形的一種實現方案,效果如圖:
好,我們開始吧。
看上去,這個動畫就是從一個形狀變成了另一個形狀。熟悉CAShapeLayer的同學,可能想到了它的path屬性,沒錯,path屬性是支持動畫的,那我們用UIBezierPath分別畫出動畫初始、結束的形狀,作為path動畫的from、to值,應該就可以了吧。
思路看上去沒有問題,我們來測試一下,示意代碼如下:
(p.s. 從本篇開始,我在文章示例中使用swift代碼,GitHub上會上傳Swift、OC兩個版本)
@IBAction func startAnimation(sender: AnyObject) { // reset animationLayer.removeAllAnimations() // 初始 let fromPath = ... // 圓 // 結束 let toPath = ... // 圓變形後的形狀 // end status animationLayer.path = toPath.CGPath // animation let animation = CABasicAnimation(keyPath: "path") animation.duration = 3 animation.fromValue = fromPath.CGPath animation.toValue = toPath.CGPath animationLayer.addAnimation(animation, forKey: nil) }
測試之前我們最好有個用例,或者有個非正式的預期。
比如我對這段代碼的預期是這樣的:
測試一下,結果是這樣的:
很明顯,測試結果和我們的預期不一樣;
由此,我們得出一個不嚴謹的結論:path動畫的效果是不可控的。
也許有的同學會說,換一種繪制方式,動畫效果可能就達到要求了,這是可能的,大家可以試一試,但本篇中,我就不去猜怎麼繪制才能達到要求了,我嘗試找一個可控的方案。所謂可控,就是動畫的每一步,形狀的樣子我們都知道。
如果我們能建立起形狀和動畫進度(用progress代替,取值0.0~1.0)的關系,那麼progress變化時,我們重繪形狀,應該就可以了。
思考一下,形狀和progress建立關系的難點在哪?
初始我們繪制了一個圓,結束時我們繪制了一個不規則的形狀,它們的繪制邏輯是不一樣的,從代碼層面講,它們各自有一套繪制代碼。兩套繪制代碼,聽著不太符合直覺。
比較符合直覺的是,我們只有一套繪制代碼,progress是這套代碼的參數,progress為0時,繪制的是圓,progress為1時,繪制的是不規則圖形。
一套代碼可以做到嗎?
可以的,前提是我們要將形狀進行分解,看上去不一樣的東西,經過分解後,很可能發現共同點。
請看下面的兩張圖:
可以看出,兩圖中的形狀都可以認為是由兩條平滑曲線(貝塞爾曲線)構成的。
(本篇不深入貝塞爾曲線,大家只要知道貝塞爾曲線由起點、終點和N個控制點決定就好)
假設藍線和紅線都以頂部為起點,以底部為終點,動畫過程,其實就是兩條曲線的起點下移,終點不動,控制點適當變化的過程。結合前面所說的,我們可以得到初步的方案:一套繪制代碼:繪制兩條貝塞爾曲線。
動畫:貝塞爾曲線的起點、終點、控制點隨progress值變化
大思路有了,但是貝塞爾曲線的起點、終點、控制點是如何隨progress值變化,才能實現不規則變形呢?
對我而言,這個問題還是太復雜了。覺得復雜, 接著分解。
規則的東西實現起來,總會簡單一些,我們先想一想,如何實現規則變形,打破規則,也不難,我們在規則變形的基礎上,破壞一些規則變形的條件,應該就能實現不規則變形。
在進行下一步的思考之前,我們要先處理一個問題,上述思路,分析是合理的,但存在一個技術問題:兩條貝塞爾曲線,是沒法完美模擬一個圓的(沒有深入調研,有興趣的同學請搜索“貝塞爾曲線擬合圓”)。
目前的結論是,四條貝塞爾曲線可以比較完美的模擬一個圓。
所以我們的方案調整一下,如下圖:
為了讓大家看的更清晰,我給形狀加上輔助點和輔助線(p.s. 輔助點和輔助線的思路來自Kitten的A-GUIDE-TO-iOS-ANIMATION),如下圖:
每條曲線的起點、終點和兩個控制點,應該比較清晰了。
在處理變形之前,我們先看下,四條貝塞爾曲線怎麼模擬出一個圓,如圖:
貝塞爾曲線擬合圓
有興趣的同學可以去找下相關的數學知識,可以搜“貝塞爾曲線擬合圓”。
此處我們直接引用別人的結論,如圖所示,第一個控制點和起點在連線與圓相切方向上,距離為半徑r的1/1.8,第二個控制點和終點也是類似的。
代碼中定義的下述常量,大家就知道是什麼意思了:
let controlPointFactor: CGFloat = 1.8
圓模擬出來了,現在我們來看一下如何規則變形,
簡化一下,先考慮豎直方向的變形,我們以圓的底部為原點(0, 0),豎直變形,可以認為是各曲線的起點、終點和有需要的控制點的y坐標均乘於一個系數,本例中取0.8(豎直方向壓扁),那麼變形如下圖:
只豎直方向變形
水平方向也類似,假設x方向系數為1.2(水平方向拉長),那麼變形如下圖:
只水平方向變形
兩者結合起來就得到了圓的規則變形,如圖(本篇中的規則變形可以認為是對稱變形,圓未必變成了數學意義上的橢圓):
規則變化實現了,接下來就該破壞規則變形的條件了。
大家跑的一樣快,隊形很整齊,想破壞隊形,只要讓一個人跑的比大家快或慢就行了。我們的動效中是頂部變形更明顯,所以,我們讓頂點y方向乘的系數小於0.8就可以了,也就說,頂點相對於其他點,y值變化的幅度更大,比0.8時的位置更接近原點(底點),如圖:
至此,我們的效果就實現了。發散一下,頂點跑的慢:
左點不向左跑,反而向右跑:
不多舉例了,大家可以看到,這種方案還是比較靈活的。
復雜的形狀可以由更多的貝塞爾曲線組成,只要我們找到貝塞爾曲線的起點、終點、控制點和progress的關系,就可以實現復雜可控的形狀動畫。
具體代碼實現,和本系列主線第一篇是類似的,采用的重繪方案,示意代碼如下:
// 創建CALayer子類 class CircleIrregularTransformLayer: CALayer // progress變化時,告知layer重繪自己 override static func needsDisplayForKey(key: String) -> Bool { switch key { case "progress": return true default: break } return super.needsDisplayForKey(key) } // 繪制代碼 override func drawInContext(ctx: CGContext) { let path = UIBezierPath() // 以底點為原點 let bottom = ... // 控制點偏移距離 let controlOffsetDistance = radius / 1.8 // 各點變化系數 let xFactor = ... // 根據progress計算 let yFactor = ... // 根據progress計算 // 頂點特殊的變化系數(破壞規則變形) let topYFactor = ... // 根據progress計算 // 右上弧 path.addCurveToPoint(dest0, controlPoint1: control0A, controlPoint2: control0B) // 左上弧 path.addCurveToPoint(dest1, controlPoint1: control1A, controlPoint2: control1B) // 左下弧 path.addCurveToPoint(dest2, controlPoint1: control2A, controlPoint2: control2B) // 右下弧 path.addCurveToPoint(dest3, controlPoint1: control3A, controlPoint2: control3B) CGContextAddPath(ctx, path.CGPath) CGContextSetLineWidth(ctx, lineWidth) CGContextSetStrokeColorWithColor(ctx, UIColor.blueColor().CGColor) CGContextStrokePath(ctx) // 輔助點 // 輔助線 }
大家在看代碼的時候,可能感覺各點的計算和文中提到的不完全一致,文中側重思路,是以底點為坐標系原點(0, 0)、常規坐標系(x軸向右為正方向,y軸向上為正方向)來描述的,而代碼中實現時,會使用UIKit的坐標系,底點在superView的坐標系中也不會是(0, 0),因此,請放心看代碼,思路是一樣的,不一樣的只是實現上的細節。
本篇作為一款Loading動畫系列的補充,到這就這結束了,非常感謝大家的捧場!
大家,下個系列見。
完整代碼
請參考GitHub上OneLoadingAnimation工程中Swift、OC目錄下的CircleIrregularTransform。
本系列的傳送門
一款Loading動畫的實現思路(一)
一款Loading動畫的實現思路(二)
一款Loading動畫的實現思路(三)
一款Loading動畫的實現思路(四·完結篇)
Loading動畫外篇·圓的不規則變形
鳴謝及推薦
原動效的設計者 moonjoin
Kitten的A-GUIDE-TO-iOS-ANIMATION
喵神發起的objc中國的動畫部分,都是很優秀的譯文,衷心為翻譯的同學點贊。
相關鏈接
Core Animation Programming Guide
CALayer Class Reference
CAShapeLayer Class Reference
UIBezierPath Class Reference