Swift — 實作 The UX in Motion Manifesto 的動畫效果 Part.1
原文:
中文翻譯:
之前在好想工作室的 “ 想知道嗎?” 活動裡面曾經有分享關有關於動畫的效果,有小小提到這篇文章的動畫效果,然後最近的在工作室的分享活動剛好有人分享 UX in Motion 的這篇文章,所以下次的「 想知道嗎?」我就要來挑戰實作這些範例。( 好繞口
而且我超愛做動畫 — by Jeremy Xue
好啦開始了
#1 Easing(緩和效果)
暫時事件發生時,物體行為符合使用者的預期動作
在 Swift 中,這個效果應該算是容易實現的,我們只要使用 animate(withDuration:delay:options:animations:completion:) 這個方法,裡面的 options 中的 UIView.AnimationOptions 就有許多效果能夠讓我們選擇,詳細的動畫效果選項我們可以查看這篇官方文件:
我們可以使用其中的來做我們 Easing 的效果:
- curveEaseIn — 緩進
- curveEaseOut — 緩出
- curveEaseInOut — 緩進緩出
接著在我們 animations 裡面加上位置改變的話,就能呈現出位移的動畫效果,呈現效果如下:
當然我們也可以加上一些彈性的效果,呈現起來會比較符合現實的物理效果,這時我們可以使用 Swift 中的 animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:),來為它加上阻尼比(dampingRatio)與速率(velocity)效果,讓畫面呈現會可以類似彈簧的效果,簡單介紹一下這兩者:
- Damping Ratio:
用於彈簧動畫的阻尼比(或減振比)。如果要平滑減速動畫不振盪,使用 1 的阻尼值;如果要增加震盪請使用接近 0 的阻尼值。
幫大家 Wiki 科普科普( 雖然我也不是很懂… )
阻尼比 - 维基百科,自由的百科全书
阻尼比(英语:Damping ratio )是 工程上的 無因次量,描述系統在受到擾動後振盪及衰減的情形。許多系統在受擾動,離開其…
zh.wikipedia.org
- Velocity:
彈簧動畫的初始速率。值為 1 的時候為於一秒內通過總動畫的距離。例如,如果總動畫距離為 200 points,如果你希望動畫的開頭匹配 100 pt/s 的視圖速率,則使用 0.5 的 velocity。
接著我們將這兩者數值調整為 0.5,來看看呈現的動畫效果:
規劃 & 實作功能:
# Easing & Elastic
這邊我們範例簡單做一個商品購買頁的介面。
第一個效果是當我們點選購買按鈕之後,要彈出一個客製化的警告視窗,讓使用者再次確定購買資訊
第二個效果則是當使用者確認購買之後,會顯示出購買成功等等動畫,讓使用者知道這次購買成功了。
簡單設計完成我們的購買商品的主畫面:
接下來我們會實作一個簡單的客製化警告視窗,這邊我的方法是使用 Storyboard 建立我的 alertView。然後因為我們需要點下夠按鈕才會顯示警告視窗,所以我會在 viewDidLoad 時候讓我們的 alertView 變形到看不見的樣子,這邊我們想要 alertView 的高度被壓縮,所以我們將 y 的值設為 0:
alertView.transform = CGAffineTransform(scaleX: 1, y: 0)
接下來你可能會覺得,為什麼不用程式碼生成 View、不使用修改 CGRect 方式?為什麼我要特地用 Stroyboard 拉一個 View 元件出來,然後又在 viewDidLoad 的時候變形它?
簡單來說,可以分兩個原因,第一個我只是想要知道它顯示在畫面上是什麼樣子,所以用 Storyboard 拉出元件,畢竟可視的介面調整真的很方便;第二點有點像是第一點所提到的,我不用特別去計算它要變形的長寬要多少,不管我如何之後如何變形它,只要我在後面加上 CGAffineTransform.identity 就能變回我們當初設計的原樣,也減少了一堆程式碼。
alertView.transform = CGAffineTransform.identity
然後接著,我們還需要給他一個縮小的變形方法,這時我們 y 所設定的縮放值( scale )就不能使用 0 了。因為將一個 View 的高度乘上 0,那他的高度也會直接變成 0,等同於沒有高度了,所以高為 0 的 View 是做不出有變形效果的,他會直接消失在畫面上:
所以這邊我們如同前面一樣 alertView 他的高度壓縮,因為不能為 0,所以將 y 的縮放值設置為趨近於 0 即可,所以這邊我們設為 0.001:
alertView.transform = CGAffineTransform(scaleX: 1, y: 0.001)
這樣我們顯示與消失 alertView 的方法都完成了,接下來只要實作簡單的購買後顯示訊息的方法即可,這邊我們會想要淡入飛出的效果,所以這邊我們會先把該元件的 alpha( 透明度 )設置為 0,之後再透過 UIView.animate 方式將 alpha 設置為 1 就能夠有淡入的效果,位移則是修改座標即可,簡單示意一下:
UIView.animate(withDuration: 1, delay: delay, options: UIViewAnimationOptions.curveEaseIn, animations: { messageLabel.alpha = 1 messageLabel.center.y -= moveY }) { (finish) in UIView.animate(withDuration: 1, animations: { messageLabel.alpha = 0 }, completion: { (finish) in // 我們這邊的 Label 是在這個 function 前產生的,所以動畫結束就將它移除
messageLabel.removeFromSuperview() // 將我們的 alertView 縮小
self.hideView() })}
這邊你會發現,那我有 4 個顯示不同訊息的 Label 不就要設定四個 UIView.animate 嗎?想當初我也真的傻傻的設定,但你忘了我們有 function 可以使用,我可以把我們的 function 的參數設定成這樣:
func showMessage(message:String,delay:Double,moveY:CGFloat) { // 這邊只寫出有使用到參數的部分,並不是完整程式碼 // 創建 Label
let label = UILabel()
label.text = message //你的動畫效果
UIView.animate(withDuration: 0.4, delay: delay, options: UIViewAnimationOptions.curveEaseIn, animations: { messageLabel.center.y -= moveY }}
以下是我們 Easing 和 Elastic 效果的 Demo 畫面:
#2 Offset & Delay(位移和延遲)
原本以為這邊介紹的 Delay 效果,以為只是單純的延遲動畫時間的感覺,就像是 TableView Cell 一個一個慢慢顯示這樣,但是這邊的意思為:
在潛意識下「告訴」使用者,該物體的特性,讓使用者做好準備。
即便使用者還沒辨別出這些物體 是什麼,設計師已經(利用動畫)傳達它們會以某種形式「有所不同」,效果非常明顯。
Ps. 因為真的還沒有太多這方面的 Sence ,還是用原文的範例:
規劃 & 實作功能:
# Offset & Delay
這邊我規劃是一個類似聊天軟體的聯絡人的畫面。
這邊第一個功能大致上與範例相同,點選懸浮按鈕就會跑到上方圖片,並將上方圖片區域改變為控制更換照片以及修改資料的區域。
第二個功能就是下方按鈕點選時,會將上方頁面整個向上推,顯示出現下方隱藏的區塊及選項。如果再點選一次就會隱藏此區塊。
最後我們的畫面大概長這個樣子:
接下來我們要開始做讓按鈕取代上方圖片區塊的效果,在做之前我想強調:
只要你說的出來的動畫其實都做得出來( 不是太複雜的效果的話 ),只是要清楚它的 “ 執行順序 ” 以及 “ 過程 ” 發生了什麼事!!
所以我們就可以分析我們的按鈕過程發生了什麼,並查看哪些動畫方式是 「 一起進行 」還是「 分別執行 」。以我們這個按鈕來舉例:
- 按鈕移動至圖片中央
- 按鈕圓角及框線效果消失,並且按鈕放大到與圖片相同,這時我們的後面的修改資料區域需提前出現在按鈕後方
- 按鈕消失,這時我的修改區域就會顯示出來
而消失也是相同原理,大部分的時候基本上都是變成反向操作而已:
- 按鈕出現,蓋住所有區域
- 修改資料區域消失,修改按鈕大小,恢復按鈕圓角及框線
- 按鈕恢復到原位置
所以我們的程式碼會長得類似像這樣:
UIView.animate(withDuration: 0.4, animations: { self.moreButton.center = self.myImage.center}) { (finish) in UIView.animate(withDuration: 0.2, animations: { self.moreButton.layer.cornerRadius = 1 self.moreButton.layer.borderColor = UIColor.clear.cgColor self.moreButton.frame = self.myImage.frame self.editView.alpha = 1 }) { (finish) in UIView.animate(withDuration: 0.2, animations: { self.moreButton.alpha = 0 }) }}
你可以將我們的 finish 的 closure 當成我們的上面的 Step 1、2、3 這樣看待,都是每一段執行完成後再繼續執行下個段落,所以前面才會提到說要「 好好看清楚動畫的流程 」,因為他決定了你的程式碼該放在哪個位置。
那我們下一個 View 的移動相較起來就很簡單了,因為他就是三個 View 同時向上或向下位移,再加上一個箭頭的旋轉效果,所以根據這些需求我們也可以像上述一樣定義一個方法,這邊我們會需要定義一個 isPushed 變數,來查看我們的 view 是否被推移:
var isPushed = false
這樣我們就能輕鬆做出該位移多少的方法,也能經由判斷式來判斷是否位移過了,如果還沒位移就旋轉,有位移過就轉回原本的樣子:
func moveView(moveY:CGFloat) { // 判斷是否被推被位移
UIView.animate(withDuration: 0.8) { if self.isPushed { self.upButton.transform = CGAffineTransform.identity } else { self.upButton.transform = CGAffineTransform(rotationAngle: .pi) } } // 位移動畫 UIView.animate......}
到目前階段的動畫效果為:
接下來你可能會發現,因為我們手機畫面一次都只能顯示兩個 View 的內容,因此需要多按一次按鈕才能夠看到顯示內容,又或是想立即到修改畫面,但卻又沒有修改的浮動按鈕可以按,如下面所示:
所以我們應該要在打開下方的區域的 View 的時候,把上面修改區塊的按鈕收回,而當我們打開下方區域點案浮動按鈕時,應該把下方區域隱藏回到初始狀態,直接顯示上方區塊,因此我們要在這兩個方法加上一個判斷式:
- 3個 View 的位移動畫方式
// 當上方區域顯示後,點選打開下方區域@IBAction func pushSettingView(_ sender: Any) { if editView.alpha == 1 { exitEditView(sender)
}}
- 顯示上方區域的按鈕動畫方式
// 當下方區域顯示,點選浮動按鈕時@IBAction func showMoreAction(_ sender: Any) { if isPushed { pushSettingView(sender) }}
如此一來,不管在哪個畫面兩個動畫都能夠互相溝通了:
題外動畫效果 — StackView
除了這兩種動畫效果以外,我還想要教學一個簡單就能實現動畫效果,而且顯示的效果也很不錯,那就是我們的 StackView。首先注意到我們個人資訊的 View 其中顯示 Label 的地方是一個 StackView 包起來,但其實裡面藏著 6 個元件,每個 Label 下方都藏著一個 TextField 只是被我隱藏起來。
但 StackView 有趣的是當其中的元件消失,那麼 StackView 的大小會自動調整,自適應裡面剩下的元件大小,因此我們會利用這項特性來製作動畫:
@IBAction func editEnable(_ sender: Any) { // 判斷 textField 是否顯示的判斷式 if textFieldCollection[0].isHidden == true { UIView.animate(withDuration: 1) { for textField in self.textFieldCollection { textField.alpha = 1 textField.isHidden = false } } } else { UIView.animate(withDuration: 1) { for textField in self.textFieldCollection { textField.isHidden = true textField.alpha = 0 } } }}
因為我們的 textField 預設被我設為隱藏,所以我們只要簡單地將他的 isHidden 設置為 true 讓他顯示,就可以有下方效果:
本次的 Github 範例要等到可能實作 4 ~ 6 個方法才會一次貼上去給大家參考,請大家見諒,還有程式碼在 Medium 排版問題我會認真想想~
有更好的動畫實作方式,勞煩大大們互相交流分享,謝謝指教