每個像樣的iOS應用程式一定會有自定義元素、自定義UI以及自定義動畫等等很多自定義的東西。
假如你想讓你的應用脫穎而出,你必須花費一些時間為你的應用增添一些獨特的元素,這些元素將會使你的應用耳目一新。
在這個教程中,你將學會如何建立一個自定義的文字框檢視(text field view),當你點選這個文字框時,它的邊框會有一個令人愉悅的彈性動畫,效果如下圖:
在學習的過程中,你講會用到許多有趣的API:
- CAShapeLayer
- CADisplayLink
- UIView spring animations
- IBInspectable
開始吧!
首先下載啟動專案ElasticUI-Starter。
這個工程是基於Single View Applicetion模板的應用,建立過程是iOS\Application\Single View Application。目前在container view裡有兩個文字框和一個按鈕。
你的目標是當使用者點選時給它們一個伸縮的彈性動畫。怎麼實現這個功能?
這個技術是很簡單的,你將會用到四個control point views和一個CAShapeLayer物件,然後使用UIView的spring animations動畫使control points做動畫。當它們
在動畫過程中時,你要重繪它們位置周圍的形狀。
注意:如果你不熟悉CAShapeLayer這個類,請參閱 這裡 Scott Gardner寫的一篇很棒的教程,能夠迅速的幫你入門。
這個動畫看起來似乎有點複雜,但不用擔心,它比你想象中要容易。
建立一個基本的彈性檢視
首先,你要建立一個基本的彈性檢視,並且把它作為子檢視嵌入到UITextfield中,然後啟用這個檢視並控制彈性動畫。
在工程的導航器上,選中ElasticUI資料夾右擊選擇新建檔案,然後選擇iOS/Source/Cocoa Touch Class模板,然後點選下一步,命名這個類名為ElasticView,它的父
類選擇UIView ,語言選擇swift。單擊Next,然後選擇預設位置來建立儲存檔案相關的新類。
最重要的是,你需要建立4個控制點和一個CAShapeLayer物件。新增下面的程式碼,最終得到的類定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import UIKit class ElasticView: UIView { private let topControlPointView = UIView() private let leftControlPointView = UIView() private let bottomControlPointView = UIView() private let rightControlPointView = UIView() private let elasticShape = CAShapeLayer() override init(frame: CGRect) { super.init(frame: frame) setupComponents() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupComponents() } private func setupComponents() { } } |
這些檢視和圖層能夠立即建立。setUpComponents()是一個配置方法,它會在所有的初始化方法中呼叫。現在你要設法實現它。
在setUpComponents()方法中增添如下程式碼:
1 2 3 |
elasticShape.fillColor = backgroundColor?.CGColor elasticShape.path = UIBezierPath(rect: self.bounds).CGPath layer.addSublayer(elasticShape) |
以上是配置圖形圖層,設定它的填充色和ElasticView的背景色一樣,填充的路徑的和檢視的邊界一樣。最後把它新增到圖層結構上。
接下來,在setUpComponents()方法的最後新增以下程式碼:
1 2 3 4 5 6 |
for controlPoint in [topControlPointView, leftControlPointView, bottomControlPointView, rightControlPointView] { addSubview(controlPoint) controlPoint.frame = CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0) controlPoint.backgroundColor = UIColor.blueColor() } |
在你的檢視上新增了四個控制點。為了更好地除錯,我們把控制點的背景色改成了藍色,這樣容易在模擬器裡看到它們。在教程的最後部分你會移除這段程式碼。
你需要把這些控制點分別放到上邊界中心、下邊界中心、左邊界中心和右邊界中心。這樣做是為了,當你讓它們離開檢視的時候,你可以利用它們的位置在你的CAShapeLayer物件上繪製新的路徑。
這個操作會頻繁進行,因此建立一個新的函式來是實現它。在ElasticView.swift檔案中中新增以下程式碼:
1 2 3 4 5 6 |
private func positionControlPoints(){ topControlPointView.center = CGPoint(x: bounds.midX, y: 0.0) leftControlPointView.center = CGPoint(x: 0.0, y: bounds.midY) bottomControlPointView.center = CGPoint(x:bounds.midX, y: bounds.maxY) rightControlPointView.center = CGPoint(x: bounds.maxX, y: bounds.midY) } |
這個函式在檢視邊界上將每個控制點移到正確的位置。
在setUpComponents()函式呼叫之後呼叫新的函式:
1 |
positionControlPoints() |
在實現動畫之前,你可以在storyboard新增一個View把玩一下,這樣你就可以知道ElasticView類是怎麼工作的。
開啟Main.storyboard檔案,拉一個UIView物件到Controller的檢視上,設定它的Custom Class為ElasticView。不用在意它的位置,只要保證它在螢幕內就可以,接下來你就可以看到將要發生的事。
編譯並執行程式:
看上圖,四個小的藍色正方形–它們就是你在setupComponents函式中新增的控制點檢視。現在為了得到彈性效果,你將會在CAShapeLayer物件上用它們建立一個路徑。
使用UIBezierPath類繪製圖形
在你探究接下來的一系列步驟之前,想象下如何繪製2D圖形–具體來說,你依賴畫線,特別是直線和曲線。在畫任何線之前,不論是直線還算複雜的曲線,你需要至少確定起點和終點,或者更多的位置點。
這些點全是CGponit類,你必須確認這些點在當前座標系下的X座標和Y座標。
如果你想要向量圖形,例如正方形、多邊形或者複雜的彎曲圖形,會更復雜。
想要模擬彈性效果,你要畫一個二次貝塞爾曲線(Quadratic Bézier Curves),看起來像個長方形,但是這個長方形的每個邊都有一個控制點,並且它提供了一個有彈性效果的曲線。
貝塞爾曲線是以Pierre Bézier的名字命名的,他是一位法國的工程師,在CAD/CAM系統下從事展現曲線的工作。下面是貝塞爾曲線的樣式:
藍色的實心圓是控制點,它們是你之前建立的4個檢視,紅色的點是長方形的頂點。
注意:蘋果公司對 UIBezierPath類 的文件介紹已經很深入了,如果你想深入瞭解如何建立一個路徑,它還是值得一看的。
現在是時候把理論付諸實踐了。在ElasticView.swift檔案中新增下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private func bezierPathForControlPoints()->CGPathRef { // 1 let path = UIBezierPath() // 2 let top = topControlPointView.layer.presentationLayer().position let left = leftControlPointView.layer.presentationLayer().position let bottom = bottomControlPointView.layer.presentationLayer().position let right = rightControlPointView.layer.presentationLayer().position let width = frame.size.width let height = frame.size.height // 3 path.moveToPoint(CGPointMake(0, 0)) path.addQuadCurveToPoint(CGPointMake(width, 0), controlPoint: top) path.addQuadCurveToPoint(CGPointMake(width, height), controlPoint:right) path.addQuadCurveToPoint(CGPointMake(0, height), controlPoint:bottom) path.addQuadCurveToPoint(CGPointMake(0, 0), controlPoint: left) // 4 return path.CGPath } |
在上面的函式中的程式碼有一點複雜,所以下面是分步解析一下,方便大家理解:
1、建立一個UIBezierPath類物件來儲存你的形狀。
2、提取四個控制點的位置分別為top、left、bottom和right四個常量。使用presentationLayer的原因是為了在控制點檢視動畫期間得到它們變化中的的位置。
3、通過長方形的頂點和4個控制點,繪製曲線,來建立路徑。
4、返回路徑的CGPathRef,這就是我們期望的圖層形狀。
當控制點在動畫過程中的時候,你需要呼叫這個方法,因為它可以一直在重繪新的圖形。到底該怎麼做呢?
CADisplayLink 物件是一個定時器,它允許應用程式的活動和顯示器的重新整理率同步。你只需要新增一個target和一個action,那麼當螢幕的內容更新的時候,action方法將會被呼叫。
它是一個很完美的機會去重繪你的路徑並且更新你的圖形圖層。
首先,新增一個每次更新都必須呼叫的方法:
1 2 3 |
func updateLoop() { elasticShape.path = bezierPathForControlPoints() } |
然後,在ElasticView.swift類中建立一個CADisplayLink物件名為displayLink的變數,程式碼如下:
1 2 3 4 5 |
private lazy var displayLink : CADisplayLink = { let displayLink = CADisplayLink(target: self, selector: Selector("updateLoop")) displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) return displayLink }() |
這個懶載入模式的變數就是意味著這個物件當你需要用的時候它才會被建立。每次螢幕更新的時候,就會呼叫updateLoop()函式。你需要開始或啟用link,因此增加下面的程式碼:
1 2 3 4 5 6 7 |
private func startUpdateLoop() { displayLink.paused = false } private func stopUpdateLoop() { displayLink.paused = true } |
你已經做好了無論什麼時候去移動控制點的,然後去畫一個新路徑的所有準備工作,那麼接下來就是移動它們了。
UIView Spring Animations
蘋果公司是很擅長新增新的特性的,當iOS系統新版本釋出的時候,spring animations 是最近版本包含的眾多特性之一,它可以很容易的使你的應用增加令人吃驚的元素。它允許你給動畫元素新增自定義的阻尼運動和初始速度,使動畫更特殊和有彈性。
注意:如果你想精通動畫,點選這裡檢視iOS Animations by Tutorials。
在ElasticView.swift 中新增以下程式碼,迅速獲得控制點的運動軌跡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
func animateControlPoints() { //1 let overshootAmount : CGFloat = 10.0 // 2 UIView.animateWithDuration(0.25, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5, options: nil, animations: { // 3 self.topControlPointView.center.y -= overshootAmount self.leftControlPointView.center.x -= overshootAmount self.bottomControlPointView.center.y += overshootAmount self.rightControlPointView.center.x += overshootAmount }, completion: { _ in // 4 UIView.animateWithDuration(0.45, delay: 0.0, usingSpringWithDamping: 0.15, initialSpringVelocity: 5.5, options: nil, animations: { // 5 self.positionControlPoints() }, completion: { _ in // 6 self.stopUpdateLoop() }) }) } |
下面是按步驟分解:
1、overshootAmount是控制點移動的偏移量。
2、在spring animation 動畫中Block塊包含的即將到來的UI變化將會持續0.25秒。假如你不熟悉spring animation 但是擅長物理學,你可以參閱UIView類的官方文件瞭解damping變數和velocity變數的詳細說明。對於並非專家的普通人來說,僅僅知道這兩個變數是控制動畫如何伸縮的就可以。通過多次修改填寫這兩個變數的數值來找到我們要的動畫效果是很正常的。
3、上下左右移動控制點,將會產生動畫。
4、建立另一個spring animation 動畫使檢視還原。
5、重置控制點的位置——這也是一個動畫。
6、動畫結束的時候暫停displaylink更新。
目前為止,還沒有呼叫animateControlPoints函式。我們自定義控制的目的是一旦點選檢視,就會用動畫產生。所以我們最好在touchedBegan函式中呼叫上面的函式。新增的程式碼如下:
1 2 3 4 |
override func touchesBegan(touches: Set, withEvent event: UIEvent) { startUpdateLoop() animateControlPoints() } |
執行一下工程,並且點選自定義的檢視。瞧一瞧!
重構和優化
你已經看到這個很酷的動畫,但是為了使ElasticView類更加抽象你還是有很多工作要處理的。
第一個障礙是清除overshootAmount。目前,它以硬編碼的方式設定值為10,但是我們希望它的值應該可以通過程式設計方式和Interface Builder來改變,這將是一個很大的改變。
@IBInspectable 是Xcode 6.0 的一個新特性,它是通過nterface Builder 設定自定義屬性的很好的途徑。
注意:假如你想了解更多關於@IBInspectable,請參閱Caroline Begbie寫的 Modern Core Graphics with Swift。
你將使用這個令人驚歎的新特性增加一個@IBInspectable 型別的overshootAmount屬性,這樣你建立的每一個ElasticView類的物件可以設定成不同的值。
在ElasticView類中增添下面的程式碼:
1 |
@IBInspectable var overshootAmount : CGFloat = 10 |
在animateControlPoints() 函式中引用這個屬性 ,用
1 |
let overshootAmount = self.overshootAmount |
替換
1 |
let overshootAmount : CGFloat = 10.0 |
開啟Main.storyboard,點選ElasticView,然後選擇Attributes Inspector選項卡,具體如下圖:
你將會看到一個新的選項,顯示了自定檢視的類名和一個以Overshoot A…命名的輸入框。
使用 @IBInspectable宣告的每一個變數,都可以在Interface Builder介面上看到一個輸入框,在這個輸入框裡可以設定它的值。
為了看到這中現象,複製當前的ElasticView,這樣你就得到兩個檢視,把新的檢視放到原來檢視的上面,如下圖所示:
設定新檢視和老檢視Overshoot Amount屬性值分別為40和20.
編譯並執行程式。點選兩個檢視發現不同之處。正如你看到的那樣,不同的動畫效果依賴於你在Interface builderz中設定的Overshoot Amount值。
改變新檢視中Overshoot Amount的值為-40,看看會發生什麼。你會看到4個控制點向內運動,但是背景卻沒有發生改變。
你是否準備好自己去修復這個bug? 我打賭你可以做到的!
我給你一條線索:你需要在setupComponents方法中做些改變。靠自己試一下,但是如果你遇到了困難,看一下下面的解決方法。
解決方案
1 2 3 |
// You have to change the background color of your view after the elasticShape is created, otherwise the view and layer have the same color backgroundColor = UIColor.clearColor() clipsToBounds = false |
現在你已經完成了ElasticView這個類,你可以將它嵌入不同的控制元件,例如文字框和按鈕等等。
製作一個彈性的UITextfield
你已經建立建立了具有核心功能的彈性檢視,下一步就是把它嵌入到自定義的文字框中。
右擊ElasticUI工程的導航欄,然後選擇New File…命令,選擇iOS/Source/Cocoa Touch Class模板,然後點選下一步。
命名為ElasticTextField,父類選UITextfield,程式語言選擇Swift。點選下一步然後建立。
開啟ElasticTextField.swift檔案,把他的內容替換成下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import UIKit class ElasticTextField: UITextField { // 1 var elasticView : ElasticView! // 2 @IBInspectable var overshootAmount: CGFloat = 10 { didSet { elasticView.overshootAmount = overshootAmount } } // 3 override init(frame: CGRect) { super.init(frame: frame) setupView() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupView() } // 4 func setupView() { // A clipsToBounds = false borderStyle = .None // B elasticView = ElasticView(frame: bounds) elasticView.backgroundColor = backgroundColor addSubview(elasticView) // C backgroundColor = UIColor.clearColor() // D elasticView.userInteractionEnabled = false } // 5 override func touchesBegan(touches: Set, withEvent event: UIEvent) { elasticView.touchesBegan(touches, withEvent: event) } } |
這裡有很多邏輯,下面我們一步步分解:
1、有一個ElasticView型別的屬性
2、一個叫做overshootAmount的變數,它是IBInspectable型別,所以你可以通過Interface Builder靈活的控制它的值。重寫了didSet方法,你只需設定彈性檢視的overshootAmount的值就可以了。
3、兩個標準的初始化方法,它們都呼叫可setupView()方法。
4、這裡是配置文字框的方法,下面讓我們更詳細的分解講述它:
a.設定clipsToBounds值為false,這可以是彈性檢視的大小可以超過其父檢視的大小,改變UITextfield的borderStyle屬性為.None,使它變成扁平的。
b.建立一個ElasticView物件並新增到當前檢視上作為子檢視。
c.改變當前檢視的背景色為透明色,這樣做的原因是讓ElasticView決定檢視的背景色。
d.最後,設定ElasticView的userInteractionEnabled屬性為false. 否則它將會觸發當前檢視的Touches事件。
5、重寫touchesBegan方法,並將它傳遞到ElasticView,使它可以做動畫。
開啟Main.storyboard,選中兩個UITextfield物件,在Identity Inspector中把類型別從UITextField改成ElasticTextField型別。
當然,你也要刪除那兩個為了測試而建立的ElasticView物件。
執行程式,點選文字框,你會發現實際上它並沒有用。
原因是在你建立ElasticView的時候,設定的只是shaperLayer的背景色為透明的,並不是檢視本身。
要解決這個bug,你需要一個方法,這個方法的作用是無論何時你給檢視設定背景色時,都要使檢視的shape layer 設定成和檢視相同的顏色。
傳遞背景色
因為你想用elasticShape 作為你檢視的主要背景色,所以你需要在ElasticView類中重寫backgroundColor方法。
在ElasticView.swift檔案中增添下面的程式碼:
1 2 3 4 5 6 7 8 |
override var backgroundColor: UIColor? { willSet { if let newValue = newValue { elasticShape.fillColor = newValue.CGColor super.backgroundColor = UIColor.clearColor() } } } |
willSet方法在你設定值之前被呼叫,你會發現這個值已經被傳遞,然後將fillColor的顏色設定為使用者選擇的顏色,隨後你會呼叫super並將其背景色設定為clearColor。
執行程式,你就會得到一個很棒的彈性檢視。你一定很開心。
最後的調整
你會發現UITextfield的佔位符距離它的左邊界很近。你不覺得它離得太近了嗎?你想自己修復這個bug嗎?這次沒有提示,如果你遇到了困難,看看下面的程式碼:
1 2 3 4 5 6 7 8 |
// Add some padding to the text and editing bounds override func textRectForBounds(bounds: CGRect) -> CGRect { return CGRectInset(bounds, 10, 5) } override func editingRectForBounds(bounds: CGRect) -> CGRect { return CGRectInset(bounds, 10, 5) } |
移除除錯資訊
開啟 ElasticView.swift檔案並且從setupComopents方法中移除下面的程式碼:
1 |
controlPoint.backgroundColor = UIColor.blueColor() |
目前,你應該會以你已經完成的工作而驕傲。因為你已經把一個系統UITextfield控制元件變成了可伸縮的檢視,並且建立了一個可以可以巢狀到各種UI控制元件的自定的可伸縮的UIView檢視。
下一步
這裡有一個完整專案的 連結
你有一個完整的彈性文字框,並且很多UI控制元件都可以應用這些技術。
您已經瞭解瞭如何使用檢視位置改變自定義形狀和新增反彈效果。擁有此技能,就可以說世界盡在你的掌握之中。
深入研究的話,你可以嘗試各種不同的動畫,增加更多的控制點,繪製一些看起來更炫酷的形狀,等等。
更多關於不同動畫的學習,請參閱easings.net,內容非常不錯。
在你對這個技術熟悉之後,你可以嘗試將BCMeshTransformView整合到你的專案。它是Bartosz Ciechanowski寫的一個很好的庫,你可以操作你檢視上的單獨的畫素點。
想象一下如果你可以把畫素點變成各種不同的形狀是多麼酷的一件事情。
如何使用Swift語言建立一個彈性UI控制元件是一個有趣的講解,我希望你能從這個講解中學到一些東西。
假如你有關於如何使用Swift做動畫的問題,評論或者好的想法,請在下面留言。我期待著你的回覆!