模拟音乐 app 的 Now Playing 动画
原文:Recreating the Apple Music Now Playing Transition
作者:Warren Burton
譯者:kmyhy
在許多 iPhone app 中的一種常見的可視化模板就是讓一疊卡片從屏幕外邊滑入。你可能在“提醒”之類的 app 中看過這個,它的列表是以一疊卡片的形式從下到上出現的。“音樂”app 也是這樣的,當前曲目從最小化的播放器放大為一個全屏的卡片。
這些動畫看起來并不復雜,它是以層疊的方式出現的。但如果你仔細看,實際上這個動畫中包含了許多東西。和電影中的好的特效一樣,好的動畫總是以不引人注意的方式出現。
在本教程中,你將復制“音樂”app 的從小到大的卡片式動畫。為了使問題簡單化,你將使用常規的 UIKit API。
在這篇教程中,你需要具備:
- Xcode 9.2 及以上版本
- 熟悉自動布局的概念
- 能夠在 IB 中創建、修改 UI 及自動布局約束
- 能夠將代碼中 IBOutlet 連接到 IB 對象
- 熟悉 UIView 動畫 API
開始
從這里下載開始項目。
Build & run。app 名字叫做 RazePlayer,它具有一個簡單的音樂類 app 的 UI。點擊 collection view 中的一首歌曲,底部的播放器會載入這首歌曲。播放器并不會真的播放這首歌曲,而是由播放列表來決定是否播放。
故事板
開始項目中包含了所有的 view controller,它們處于“半完成”狀態,你可以將主要精力放在動畫的創建上。打開 Main.storyboard 。
為了一開始能夠正常顯示視圖,請使用 iPhone 8 模擬器。
在故事板中,從左到右分別是:
- Tab Bar Controller,其中有一個 SongViewController:當 app 一啟動時看到的 collection view。上面是一些假的、重復的曲目集。
- 迷你播放器 View Controller:以子控制器的形式嵌入到 SongViewController 中。這視圖你需要進行動畫。
- Maxi Song Card View Controller:這個視圖承載的是動畫的最終狀態。和這個故事板一起,它將作為你主要會用到的類。
- Song Play Control View Controller:你會在動畫的過程中使用到它。
在項目導航器中展開這個個項目。這個項目使用了標準的 MVC 模式,將數據和 View Controller 分離。你使用得最頻繁的文件是 Song.swift,它代表了一首單獨的歌曲。
如果你愿意,你可以在稍后瀏覽這些文件,但在本教程中,你不需要了解太多。在本教程中,你將在 View Layer 文件夾下的這幾個文件中進行工作:
- Main.storyboard:包含這個項目的所有 UI。
- SongViewController.swift: 主視圖控制器。
- MiniPlayerViewController.swift: 顯示當前選中的曲目。
- MaxiSongCardViewController.swift: 以卡片動畫的方式顯示從播放器的最小化狀態變到播放器的最大化狀態。
- SongPlayControlViewController.swift: 包含了這個動畫的其它 UI。
稍微看一下蘋果的“音樂”app 是如何從迷你播放器變成一張大卡片的。專輯封面不斷地放大成一張大圖,tab bar 向下移動并消失。很難在動畫過程中捕捉到這個動畫的所有特效。幸好,在你克隆這個動效的時候,可以將動畫變成慢動作。
第一個任務是從迷你播放器變成全屏卡片。
對背景圖片進行動畫
iOS 動畫經常會釋放一些煙霧來愚弄用戶的眼睛,讓它們以為它們看到的一切都是真的。你的第一個任務就是讓它顯示縮小的背景內容。
創建一個假背景
打開 Main.storyboard 展開 Maxi Song Card View Controller。有兩個視圖,我們將用于作為背景圖片和模糊圖層。
打開 MaxiSongCardViewController.swift 在 dimmerLayer outlet 下面添加幾個屬性:
@IBOutlet weak var backingImageTopInset: NSLayoutConstraint! @IBOutlet weak var backingImageLeadingInset: NSLayoutConstraint! @IBOutlet weak var backingImageTrailingInset: NSLayoutConstraint! @IBOutlet weak var backingImageBottomInset: NSLayoutConstraint!然后,按住 option 鍵,在項目導航器中,點擊 Main.storyboard,打開助手編輯器。然后 MaxiSongCardViewController.swift 會在左邊打開,而 Main.storyboard 會在右邊打開。如果你在南半球,你也可以用別的方法打開助手編輯器。
接著,將背景圖片的 IBOutlet 連接到故事板上:
- 展開 MaxiSongCardViewController 的頂級對象以及它的頂層約束。
- 將 backingImageTopInset 連接到 Backing Image View 的 top 約束。
- 將 backingImageBottomInset 連接到 Backing Image View 的 bottom 約束。
- 將 backingImageLeadingInset 連接到 Backing Image View 的 leading 約束。
- 將 backingImageTrailingInset 連接到 Backing Image View 的 trailing 約束。
然后準備呈現 MaxiSongCardViewController。按 Cmd+回車鍵,或者 View ? Standard Editor ? Show Standard Editor 來關閉助手編輯器。
打開 SongViewController.swift。首先,在文件底部添加一個擴展:
extension SongViewController: MiniPlayerDelegate {func expandSong(song: Song) {//1.guard let maxiCard = storyboard?.instantiateViewController(withIdentifier: "MaxiSongCardViewController") as? MaxiSongCardViewController else {assertionFailure("No view controller ID MaxiSongCardViewController in storyboard")return}//2.maxiCard.backingImage = view.makeSnapshot()//3.maxiCard.currentSong = song//4.present(maxiCard, animated: false)} }當你點擊 迷你播放器 時,它委托給 SongViewController 來進行進一步處理。迷你播放器 不知道也不關心接下來的事情。
讓我們分步解釋上面的代碼:
然后,找到 prepare(for:sender:) 函數,在 miniPlayer = destination 一句后添加:
miniPlayer?.delegate = selfBuild & run ,從曲目集中選擇一首歌,點擊迷你播放器。你會看到一個黑色的屏幕。OK!
你會發現狀態欄消失了。先來搞定這個。
修改狀態欄的外觀
彈出的 controller 擁有一個黑色背景,因此你的狀態欄應該用清淡的顏色樣式。打開 MaxiSongCardViewController.swift,添加代碼:
override var preferredStatusBarStyle: UIStatusBarStyle {return .lightContent }Build & run,點擊 迷你播放器 彈出 MaxiSongCardViewController。狀態欄現在應該變成了黑底白字。
這一節的最后一個任務是創建一種效果,讓控制器和背景區別開來。
縮小 view controller。
打開 MaxiSongCardViewController.swift 添加屬性:
let primaryDuration = 4.0 //最終會改成 0.5 let backingImageEdgeInset: CGFloat = 15.0一個是動畫的時長,一個是背景圖的留邊。后面我們會讓動畫變快,現在故意弄慢一點,以便能夠看清整個動作。
然后,在文件末尾添加一個擴展:
//背景圖片的動畫 extension MaxiSongCardViewController { //1.private func configureBackingImageInPosition(presenting: Bool) {let edgeInset: CGFloat = presenting ? backingImageEdgeInset : 0let dimmerAlpha: CGFloat = presenting ? 0.3 : 0let cornerRadius: CGFloat = presenting ? cardCornerRadius : 0backingImageLeadingInset.constant = edgeInsetbackingImageTrailingInset.constant = edgeInsetlet aspectRatio = backingImageView.frame.height / backingImageView.frame.widthbackingImageTopInset.constant = edgeInset * aspectRatiobackingImageBottomInset.constant = edgeInset * aspectRatio//2.dimmerLayer.alpha = dimmerAlpha//3.backingImageView.layer.cornerRadius = cornerRadius}//4.private func animateBackingImage(presenting: Bool) {UIView.animate(withDuration: primaryDuration) {self.configureBackingImageInPosition(presenting: presenting)self.view.layoutIfNeeded() //這句很重要!}}//5.func animateBackingImageIn() {animateBackingImage(presenting: true)}func animateBackingImageOut() {animateBackingImage(presenting: false)} }分別做如下說明:
然后,在 viewDidLoad() 方法中,在 super 一句后添加:
backingImageView.image = backingImage這里將先前從 SongViewController 截取的截圖設置為背景圖。
最后在 viewDidAppear(_:) 方法最后添加:
animateBackingImageIn()當視圖顯示時,執行動畫。
Build & run,選擇一首歌,點擊迷你播放器。你會看到當前 view controller 非常緩慢地后退到背景中……
干得漂亮!完成了動畫中的第一部分。接下來一個相當重要的內容,將迷你播放器中的縮略圖放大成卡片中的大圖。
放大曲目圖片
打開 Main.storyboard 展開視圖樹。
你需要關注的是這些 view:
- Cover Image Container:一個白色背景的 UIView。你將在 scroll view 中改變它的位置。
Cover Art Image:你將對這個 UIImageView 進行變形。它的背景是黃色,這樣它就很容易在 Xcode 中識別出來。這個視圖有兩個地方值得注意:
- Aspect:設置為 1:1。這表示它總是一個正方形。
- Height:一個固定的值。待會你會知道為什么。
打開 MaxiSongCardViewController.swift。你可以看到這兩個 view 和關閉按鈕,都已經創建和連接了對應的 outlet:
//cover image @IBOutlet weak var coverImageContainer: UIView! @IBOutlet weak var coverArtImage: UIImageView! @IBOutlet weak var dismissChevron: UIButton!然后,找到 viewDidLoad(),刪除下面幾句:
//DELETE THIS LATER scrollView.isHidden = true這會讓 UIScrollView 顯示。它之前一直隱藏,以便為了讓你看清背景圖片上發生了什么。
然后,在 viewDidLoad() 末尾添加下面幾行:
coverImageContainer.layer.cornerRadius = cardCornerRadius coverImageContainer.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]這里只設置了上面兩個角的圓角半徑。
Build & run,點擊迷你播放器,你會看到在背景的截圖上顯示了 container viwe 和 image view。
注意看 image view 的圓角。它沒有使用一句代碼,完全是通過用戶定義的運行時屬性面板來實現的。
設置封面圖片的約束
在這一部分,你將添加封面圖片動畫中會用到的一些約束。
打開 MaxiSongCardViewController.swift。接著,添加下列約束:
@IBOutlet weak var coverImageLeading: NSLayoutConstraint! @IBOutlet weak var coverImageTop: NSLayoutConstraint! @IBOutlet weak var coverImageBottom: NSLayoutConstraint! @IBOutlet weak var coverImageHeight: NSLayoutConstraint!然后,用助手編輯器打開 Main.storyboard,連接 outlet:
- 連接 coverImageLeading、coverImageTop 和 coverImageBottom 到 image view 的leading、top 和 bottom 約束。
- 連接 coverIamgeHeight 到 image view 的 height 約束。
最后一個約束是從 cover image container 頂部到 scroll view 的 content View 之間的約束。
打開 MaxiSongCardViewController.swift。然后,添加下列屬性:
//cover image constraints @IBOutlet weak var coverImageContainerTopInset: NSLayoutConstraint!最后,連接 coverImageContainerTopInset 到 cover image container 的上邊距約束上;這個約束在 IB 上的 constant 值為 57。
現在所有的約束都已經創建完畢,可以執行動畫了。
Build & run;點擊一首曲目,然后點擊迷你播放器,確保一切正常。
創建數據源協議
你必須知道 cover image 動畫時候的起點位置。你可以傳遞一個迷你播放器的引用給大播放器,以便將所需信息傳遞給它,但這會在 MiniPlayerViewController 和 MaxiSongCardViewController 之間創建一個強依賴關系。除此之外,我們可以用協議來傳遞這個信息。
關閉助手編輯器,添加下列協議到 MaxiSongCardViewController.swift 中:
protocol MaxiPlayerSourceProtocol: class {var originatingFrameInWindow: CGRect { get }var originatingCoverImageView: UIImageView { get } }然后,打開 MiniPlayerViewController.swift,在文件末添加下列代碼:
extension MiniPlayerViewController: MaxiPlayerSourceProtocol {var originatingFrameInWindow: CGRect {let windowRect = view.convert(view.frame, to: nil)return windowRect}var originatingCoverImageView: UIImageView {return thumbImage} }這里定義了一個協議,用于告訴大播放器需要動畫的信息。然后讓 MiniPlayerViewController 實現這個協議,以便提供相應的信息。UIView 內置了一些轉換矩形和點的方法,將會非常有用。
然后,打開 MaxiSongCardViewController.swift 在主類中添加下列屬性:
weak var sourceView: MaxiPlayerSourceProtocol!這個屬性使用了弱引用以避免持有循環。
打開 SongViewController.swift,在 expandSong 方法的 present(_,animated:) 一句前添加:
maxiCard.sourceView = miniPlayer這里將起始 view 的引用在初始化時傳給大播放器。
開始動畫
在這一節,你將所有艱苦工作的成功組裝起來,將 image view 動畫到指定位置。
打開 MaxiSongCardViewController.swift,添加如下擴展:
//Image Container animation. extension MaxiSongCardViewController {private var startColor: UIColor {return UIColor.white.withAlphaComponent(0.3)}private var endColor: UIColor {return .white}//1.private var imageLayerInsetForOutPosition: CGFloat {let imageFrame = view.convert(sourceView.originatingFrameInWindow, to: view)let inset = imageFrame.minY - backingImageEdgeInsetreturn inset}//2.func configureImageLayerInStartPosition() {coverImageContainer.backgroundColor = startColorlet startInset = imageLayerInsetForOutPositiondismissChevron.alpha = 0coverImageContainer.layer.cornerRadius = 0coverImageContainerTopInset.constant = startInsetview.layoutIfNeeded()}//3.func animateImageLayerIn() {//4.UIView.animate(withDuration: primaryDuration / 4.0) {self.coverImageContainer.backgroundColor = self.endColor}//5.UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseIn], animations: {self.coverImageContainerTopInset.constant = 0self.dismissChevron.alpha = 1self.coverImageContainer.layer.cornerRadius = self.cardCornerRadiusself.view.layoutIfNeeded()})}//6.func animateImageLayerOut(completion: @escaping ((Bool) -> Void)) {let endInset = imageLayerInsetForOutPositionUIView.animate(withDuration: primaryDuration / 4.0,delay: primaryDuration,options: [.curveEaseOut], animations: {self.coverImageContainer.backgroundColor = self.startColor}, completion: { finished incompletion(finished) //fire complete here , because this is the end of the animation})UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseOut], animations: {self.coverImageContainerTopInset.constant = endInsetself.dismissChevron.alpha = 0self.coverImageContainer.layer.cornerRadius = 0self.view.layoutIfNeeded()})} }上述代碼分為以下幾步:
然后,在 viewDidAppear(_:) 方法最后添加:
animateImageLayerIn()這讓動畫開始走時間線。
然后,在 viewWillAppear(_:) 方法添加:
configureImageLayerInStartPosition()這樣,在視圖開始顯示之前到達開始位置。這里使用了 viewWillAppear 方法,這樣將 image layer 移動到開始位置的過程不會被用戶覺察到。
Build & run,然后點擊迷你播放器以彈出大播放器。你會看到 container 會上行到指定位置。它的形狀沒有發生改變,因為 container 的高度取決于 image view 的高度。
Source Image 的動畫
打開 MaxiSongCardViewController.swift 添加一個擴展:
//cover image animation extension MaxiSongCardViewController {//1.func configureCoverImageInStartPosition() {let originatingImageFrame = sourceView.originatingCoverImageView.framecoverImageHeight.constant = originatingImageFrame.heightcoverImageLeading.constant = originatingImageFrame.minXcoverImageTop.constant = originatingImageFrame.minYcoverImageBottom.constant = originatingImageFrame.minY}//2.func animateCoverImageIn() {let coverImageEdgeContraint: CGFloat = 30let endHeight = coverImageContainer.bounds.width - coverImageEdgeContraint * 2UIView.animate(withDuration: primaryDuration, delay: 0, options: [.curveEaseIn], animations: {self.coverImageHeight.constant = endHeightself.coverImageLeading.constant = coverImageEdgeContraintself.coverImageTop.constant = coverImageEdgeContraintself.coverImageBottom.constant = coverImageEdgeContraintself.view.layoutIfNeeded()})}//3.func animateCoverImageOut() {UIView.animate(withDuration: primaryDuration,delay: 0,options: [.curveEaseOut], animations: {self.configureCoverImageInStartPosition()self.view.layoutIfNeeded()})} }這段代碼和 iamge container 的動畫類似。讓我們來過一遍:
然后,在 viewDidAppear 方法中最后添加:
animateCoverImageIn()這會在視圖顯示到屏幕上之后觸發動畫。
然后,在 viewWillAppear 方法最后添加:
coverArtImage.image = sourceView.originatingCoverImageView.image configureCoverImageInStartPosition()這里從數據源對象獲取了 UIImage,傳遞給 image view。在特定情況下這樣做是可以的,比如現在,因為 UIImage 中的像素足夠多,圖片不會被顆粒化或者被拉伸。
Build & run,image view 從一開始的縮略圖長變大,同時 container view 的 frame 隨之變大。
關閉動畫
卡片頂部的按鈕被鏈接到了 dismissAction(_:) 方法。目前這個方法只會操作一個關閉動作,沒有任何動畫。
和彈出 view controller 中所做的一樣,你需要讓 MaxiSongCardViewController 處理它自己的關閉動畫。
打開 MaxiSongCardViewController.swift 將 dismissAction(_:) 修改為:
@IBAction func dismissAction(_ sender: Any) {animateBackingImageOut()animateCoverImageOut()animateImageLayerOut() { _ inself.dismiss(animated: false)} }這播放了一個和之前的呈現動畫相反的動畫。當動畫完成,我們解散 MaxiSongCardViewController。
Build & run,彈出大播放器,然后點關閉按鈕。封面圖片和 container view 又縮回到迷你播放器的樣子。但是有一個顯示上的問題,就是 Tab bar 會閃一下。我們后面會搞定它。
顯示曲目信息
再觀察一下音樂 app,你會發現打開的卡片中包含一個進度條和音量控制,還列出了歌曲名、藝術家、專輯和下一曲。這些并不是完全都放在了一個 view controller 中——而是封裝成組件。
接下來的任務是在 scroll view 中嵌入一個 View controller。為了節省時間,已經為你準備好了一個:SongPlayControlViewController。
嵌入子控制器
第一個任務是從 scroll view 中將底下的 image container 分離出來。
打開 Main.storyboard。刪除 cover image container 底部到 superView 底部的約束。會提示有布局錯誤,說 scroll view 需要有一個 Y 坐標或者高度約束。不用管。
然后,你需要創建一個子視圖控制器用于顯示歌曲詳情,步驟如下:
現在為新添加的 container view 添加下列約束:
- Leading、Trailing 和 Bottom 約束。對齊到 scroll view,間距 0。
- Top 對齊到 Cover Image Container 的底部,間距 30。
第一次放置視圖時的 Y 坐標是很重要的,請將它放到 image container view 的下面,這樣你定義約束時會更方便。
最后,將這個 Container View 所包含的 segue 綁定到 SongPlayControlViewController。按住 Control 鍵,從這個 container view 拖一條線到 SongPlayControlViewController。
松開鼠標,選擇 Embed。
最后,需要將 scroll view 中的這個 Container View 的高度做個限制,以解決 scroll view content 缺乏高度約束的問題。
到此為止,所有的自動布局錯誤都將消失。
播放控件的動畫
接下來的效果是在動畫結束時,將播放控件從屏幕底部上移和 cover image 接在一起。
在標準編輯器中打開 MaxiSongCardViewController.swift,在助手編輯器中打開 Main.storyboard。
在 MaxiSongCardViewController 主類中添加屬性:
//lower module constraints @IBOutlet weak var lowerModuleTopConstraint: NSLayoutConstraint!將這個 outlet 連接到 image container 和 Container View 之間的間距約束。
關閉助手編輯器,在 MaxiSongCardViewController.swift 中新增擴展:
//lower module animation extension MaxiSongCardViewController {//1.private var lowerModuleInsetForOutPosition: CGFloat {let bounds = view.boundslet inset = bounds.height - bounds.widthreturn inset}//2.func configureLowerModuleInStartPosition() {lowerModuleTopConstraint.constant = lowerModuleInsetForOutPosition}//3.func animateLowerModule(isPresenting: Bool) {let topInset = isPresenting ? 0 : lowerModuleInsetForOutPositionUIView.animate(withDuration: primaryDuration,delay:0,options: [.curveEaseIn],animations: {self.lowerModuleTopConstraint.constant = topInsetself.view.layoutIfNeeded()})}//4.func animateLowerModuleOut() {animateLowerModule(isPresenting: false)}//5.func animateLowerModuleIn() {animateLowerModule(isPresenting: true)} }這個擴展對 SongPlayControllerViewController 的 view 和 Image Container 之間的間距操作一個簡單動畫:
接下來將動畫添加到時間線。首先,在 viewDidAppear 方法最后添加:
animateLowerModuleIn()在 viewWillAppear 方法最后添加:
stretchySkirt.backgroundColor = .white // 避免封面圖片和下面的 songPlayControl 之間顯示出間隙 configureLowerModuleInStartPosition()然后,在 dismissAction 方法中,調用 animateImageLayerOut(completion:) 一句前執行解散動畫:
animateLowerModuleOut()最后,在 MaxiSongCardViewController.swift 中添加這個方法將當前歌曲傳遞給新控制器。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {if let destination = segue.destination as? SongSubscriber {destination.currentSong = currentSong} }這里檢查了 destination 是否實現是一個 SongSubscriber,然后將歌曲傳遞給它。這里演示了一個簡單的依賴注入。
Build & run。彈出大播放器界面,你會看到 SongPlayControl 的視圖會上移到指定位置。
隱藏 Tab Bar
在最終完成之前還有一個東西需要處理,這就是 tab bar。你可以修改 tab bar 的 frame,但這會將相關的 view controller 框架搞亂。所以,我們需要再釋放一些煙霧彈:
- 通過截屏獲得 Tab Bar 的圖片。
- 將它傳遞給 MaxiSongCardViewController。
- 對這張 tab bar 圖片進行動畫。
首先,在 MaxiSongCardViewController 中加入:
// 假 tabbar 的約束 var tabBarImage: UIImage? @IBOutlet weak var bottomSectionHeight: NSLayoutConstraint! @IBOutlet weak var bottomSectionLowerConstraint: NSLayoutConstraint! @IBOutlet weak var bottomSectionImageView: UIImageView!然后,打開 Main.storyboard 拖一個 Image View 到 MaxiSongCardViewController 中。你要將它放在視圖樹的 scroll view 的上層(在 document outline 中則是位于 scroll view 的下方)。
打開 Add Constraints 菜單,去掉 Constain to margins 選項。將它的 leading、trailing 和 bottom 對齊 superview,值都是 0。實際上,是對齊到了安全區。高度約束設置為 128,然后點擊 Add 4 Constaints 創建約束。
接著,用助手編輯器打開 MaxiSongCardViewController.swift,將 3 個屬性連接到 Image view。
- bottomSectionImageView 連接到 Image View。
- bottomSectionLowerConstraint 連接到 Bottom 約束。
- bottomSectionHeight 連接到 height 約束。
最后,關閉助手編輯器,添加一個擴展到 MaxiSongCardViewController.swift:
// 假 tab bar 動畫 extension MaxiSongCardViewController {//1.func configureBottomSection() {if let image = tabBarImage {bottomSectionHeight.constant = image.size.heightbottomSectionImageView.image = image} else {bottomSectionHeight.constant = 0}view.layoutIfNeeded()}//2.func animateBottomSectionOut() {if let image = tabBarImage {UIView.animate(withDuration: primaryDuration / 2.0) {self.bottomSectionLowerConstraint.constant = -image.size.heightself.view.layoutIfNeeded()}}}//3.func animateBottomSectionIn() {if tabBarImage != nil {UIView.animate(withDuration: primaryDuration / 2.0) {self.bottomSectionLowerConstraint.constant = 0self.view.layoutIfNeeded()}}} }這段代碼和其它動畫類似。每一步驟你都熟悉。
最后一件事情是在這個文件里執行動畫。
首先,在 viewDidAppear 方法最后添加:
animateBottomSectionOut()然后,在 viewWillAppear 方法最后添加:
configureBottomSection()接著,在 dissmissAction 方法的 animateImageLayerOut(completion:) 一句之前添加:
animateBottomSectionIn()然后,打開 SongViewController.swift 在 expandSong(song:) 方法的 present(animated:) 一句前添加:
if let tabBar = tabBarController?.tabBar {maxiCard.tabBarImage = tabBar.makeSnapshot() }在這里,我們隊 Tab Bar 進行了截圖,如果 Tab Bar 不為空,將截圖傳遞給 MaxiSongCardViewController。
最后,打開 MaxiSongCardViewController.swift 將 primaryDuration 屬性修改為 0.5,這樣你就不必忍受慢吞吞的動畫的折磨了!
Build & run,彈出大播放器界面, tab bar 會上移然后下降到正常的位置。
恭喜!你已經模仿了一個“音樂” app 的卡片動畫(幾乎是重新建造的)。
接下來做什么?
你可以從這里下載完成后的項目。
在本教程中,你學習了:
- 對自動布局約束進行動畫。
- 將多個動畫放入時間線以組合成復雜動畫。
- 用靜態的截圖模擬動畫。
- 用委托模式在兩個對象之間創建弱綁定。
注意,靜態截圖的方法在卡片呈現時底層視圖被改變的情況下無法使用,在這種情況下異步事件會導致一個刷新動作。
在開發中動畫的代價是昂貴的,而且要做出滿意的效果很難。但是,它常常是值得的,因為動畫為 app 增加了亮點,能夠讓普通的 app 變得與眾不同。
希望本教程能夠激發你創建自己動畫的靈感。有任何建議后疑問,或者分享你的創意,請加入到討論中來!
總結
以上是生活随笔為你收集整理的模拟音乐 app 的 Now Playing 动画的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arctime pro怎么加字幕?arc
- 下一篇: [Git]git的一些常用操作笔记