[iOS 開發筆記 ] 什麼?你說動畫不能繼續?WAAAAAAT!? - 用貝茲曲線來加減速倒數數字

相信大家都想過做自己的「倒數計時器」,看看那精美的Google搜尋結果 ↓

要做出想要的功能其實真的不難,利用 NSTimer 定期更新就可以輕鬆達到到數的目標。

上面的那種模式,快速而且簡單容易實現,但是如果遇到更複雜的Layout像是
complex_layout
如果不加思索的將 Customized View的 code 全部寫在 ViewController 裡面,無庸置疑的會是一場災難,基於 OOP 的精神,我們需要把功能集中到單一 View 上面,Code 變更具有重用性

到這邊其實也很簡單,不過這樣做的倒數完全只是依照線性的前進/減少,如果想要玩多一點花招像是倒數加減速的話就有「不少」的困難。
complex_layout
事情是這樣的,如果我們想要使用貝茲曲線對 view 做移動、透明度等等的加減速效果,也不是一件困難的事情

// 向左加速後減速移動
let easeInOutMoving = CAKeyframeAniamtion(keyPath: "position.x")
easeInOutMoving.fillMode = kCAFillModeForwards
easeInOutMoving.removedOnCompletion = false
easeInOutMoving.values = [0, 100]
easeInOutMoving.keyTimes = [0, 1]
easeInOutMoving.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

只要對 CAKeyframeAnimation 的 timeFunction 設定 Control Points (在這邊我直接使用了已經定義好的 EaseInEaseOut 做範例),就可以依照想要的曲線做 Animation 呈現,不過需要對「數字」本身在做加減增值區間效果的話,我們就需要知道貝茲曲線座標上實際的數值 B(t),因為 CAAniamtion 並不支援對文字的處理。

關於貝茲曲線的說明
css3-transitions_TimingFunction
說起來也沒有多困難,除非開發者需要自己實作關於貝茲曲線的計算,否則只需要從既有的 CAMediaTimingFunction 中取得 B(t) 就好......當然如果有這麼簡單就阿彌陀佛。

實際上, CAMediaTimingFunction 不提供取得 B(t) 的方法,換句話說......沒錯!!那就真的得自己做 Bezier 計算了!不過謝天謝地,在廣大的 npm_module 裡面已經有實作的 module 可以用,所以這邊就主要參考了 bezier-easing 實作了一個 Swift 版本

實際上的算式與實作文章可以看這一篇

那麼事情就完成一半了,既然已經可以得到實際上增減的區間值,再來就只剩下更新文字的呈現,甚至還可以推算出任何時間點動畫的進度,隨時暫停/繼續接上動畫進度都沒問題,那們在這邊進一步的完成 CountingLabel。

完成的 CountingLabel 與原本最原初的 CountingLabel 更新的方式有點不太一樣,由於有可能會有數到小數點的呈現上需求,所以在最完善的版本裡面將 NSTimer 更換成 CADisplayLink 來以每秒 60 格的方式更新。

不過在決定繼承 UILabel 來實作 CoutingLabel 的時候說真的一直很猶豫是不是使用 UIView 來完成功能才能保證元件行為合乎預期 - 像是當 UILabel 的 text 被任意更動的時候會造成顯示上的困擾,甚至根本不希望 text 的值可以被任意更動,只期許 text 可以被內部方法改變,但如果重頭客製 UIView 時會有設置 font attributes 的困擾以及缺乏了 UILabel 在 AutoLayout 中可以自適化尺寸的優勢。

在一番考量後目前決定繼承 UILabel 來實作功能,如果各位有其他更好的意見請不吝告訴小弟我,非常感謝!

最後獻上github,請各位大神鞭小力一點!
https://github.com/Calvin-Huang/CHCountingLabel