日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

模拟音乐 app 的 Now Playing 动画

發(fā)布時間:2024/3/24 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 模拟音乐 app 的 Now Playing 动画 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

原文:Recreating the Apple Music Now Playing Transition
作者:Warren Burton
譯者:kmyhy

在許多 iPhone app 中的一種常見的可視化模板就是讓一疊卡片從屏幕外邊滑入。你可能在“提醒”之類的 app 中看過這個,它的列表是以一疊卡片的形式從下到上出現(xiàn)的。“音樂”app 也是這樣的,當前曲目從最小化的播放器放大為一個全屏的卡片。

這些動畫看起來并不復雜,它是以層疊的方式出現(xiàn)的。但如果你仔細看,實際上這個動畫中包含了許多東西。和電影中的好的特效一樣,好的動畫總是以不引人注意的方式出現(xiàn)。

在本教程中,你將復制“音樂”app 的從小到大的卡片式動畫。為了使問題簡單化,你將使用常規(guī)的 UIKit API。

在這篇教程中,你需要具備:

  • Xcode 9.2 及以上版本
  • 熟悉自動布局的概念
  • 能夠在 IB 中創(chuàng)建、修改 UI 及自動布局約束
  • 能夠?qū)⒋a中 IBOutlet 連接到 IB 對象
  • 熟悉 UIView 動畫 API

開始

從這里下載開始項目。

Build & run。app 名字叫做 RazePlayer,它具有一個簡單的音樂類 app 的 UI。點擊 collection view 中的一首歌曲,底部的播放器會載入這首歌曲。播放器并不會真的播放這首歌曲,而是由播放列表來決定是否播放。

故事板

開始項目中包含了所有的 view controller,它們處于“半完成”狀態(tài),你可以將主要精力放在動畫的創(chuàng)建上。打開 Main.storyboard 。

為了一開始能夠正常顯示視圖,請使用 iPhone 8 模擬器。

在故事板中,從左到右分別是:

  • Tab Bar Controller,其中有一個 SongViewController:當 app 一啟動時看到的 collection view。上面是一些假的、重復的曲目集。
  • 迷你播放器 View Controller:以子控制器的形式嵌入到 SongViewController 中。這視圖你需要進行動畫。
  • Maxi Song Card View Controller:這個視圖承載的是動畫的最終狀態(tài)。和這個故事板一起,它將作為你主要會用到的類。
  • Song Play Control View Controller:你會在動畫的過程中使用到它。

在項目導航器中展開這個個項目。這個項目使用了標準的 MVC 模式,將數(shù)據(jù)和 View Controller 分離。你使用得最頻繁的文件是 Song.swift,它代表了一首單獨的歌曲。

如果你愿意,你可以在稍后瀏覽這些文件,但在本教程中,你不需要了解太多。在本教程中,你將在 View Layer 文件夾下的這幾個文件中進行工作:

  • Main.storyboard:包含這個項目的所有 UI。
  • SongViewController.swift: 主視圖控制器。
  • MiniPlayerViewController.swift: 顯示當前選中的曲目。
  • MaxiSongCardViewController.swift: 以卡片動畫的方式顯示從播放器的最小化狀態(tài)變到播放器的最大化狀態(tài)。
  • SongPlayControlViewController.swift: 包含了這個動畫的其它 UI。

稍微看一下蘋果的“音樂”app 是如何從迷你播放器變成一張大卡片的。專輯封面不斷地放大成一張大圖,tab bar 向下移動并消失。很難在動畫過程中捕捉到這個動畫的所有特效。幸好,在你克隆這個動效的時候,可以將動畫變成慢動作。

第一個任務是從迷你播放器變成全屏卡片。

對背景圖片進行動畫

iOS 動畫經(jīng)常會釋放一些煙霧來愚弄用戶的眼睛,讓它們以為它們看到的一切都是真的。你的第一個任務就是讓它顯示縮小的背景內(nèi)容。

創(chuàng)建一個假背景

打開 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 約束。

然后準備呈現(xiàn) MaxiSongCardViewController。按 Cmd+回車鍵,或者 View ? Standard Editor ? Show Standard Editor 來關(guān)閉助手編輯器。

打開 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 來進行進一步處理。迷你播放器 不知道也不關(guān)心接下來的事情。

讓我們分步解釋上面的代碼:

  • 從故事板中初始化一個 MaxiSongCardViewControlelr。在 guard 語句中用一個 assetionFailure 在設計時確認對象創(chuàng)建是否創(chuàng)建成功。
  • 創(chuàng)建一張 SongViewController 的截圖,并傳遞給新視圖控制器。makeSnapshot 是開始項目中的一個現(xiàn)成的助手方法。
  • 當前曲目對象被傳遞給 MaxiSongCardViewController 對象。
  • 以 modal 形式呈現(xiàn),以非動畫形式。被呈現(xiàn)的控制器將采用自己的動畫序列。
  • 然后,找到 prepare(for:sender:) 函數(shù),在 miniPlayer = destination 一句后添加:

    miniPlayer?.delegate = self

    Build & run ,從曲目集中選擇一首歌,點擊迷你播放器。你會看到一個黑色的屏幕。OK!

    你會發(fā)現(xiàn)狀態(tài)欄消失了。先來搞定這個。

    修改狀態(tài)欄的外觀

    彈出的 controller 擁有一個黑色背景,因此你的狀態(tài)欄應該用清淡的顏色樣式。打開 MaxiSongCardViewController.swift,添加代碼:

    override var preferredStatusBarStyle: UIStatusBarStyle {return .lightContent }

    Build & run,點擊 迷你播放器 彈出 MaxiSongCardViewController。狀態(tài)欄現(xiàn)在應該變成了黑底白字。

    這一節(jié)的最后一個任務是創(chuàng)建一種效果,讓控制器和背景區(qū)別開來。

    縮小 view controller。

    打開 MaxiSongCardViewController.swift 添加屬性:

    let primaryDuration = 4.0 //最終會改成 0.5 let backingImageEdgeInset: CGFloat = 15.0

    一個是動畫的時長,一個是背景圖的留邊。后面我們會讓動畫變快,現(xiàn)在故意弄慢一點,以便能夠看清整個動作。

    然后,在文件末尾添加一個擴展:

    //背景圖片的動畫 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)} }

    分別做如下說明:

  • 設置 image 的最后的 frame。我們通過圖片的縱橫比來設置垂直的留邊,這樣圖片比例不會失真。
  • 模糊遮罩層是一個 UIView,位于 Image View 之上,黑色的背景色。設置 alpha 值以便讓圖片稍微變得模糊一點。
  • 設置圖片的圓角。
  • 使用最簡單的 UIView 動畫 API,告訴 image view 以動畫方式改變成新的布局。在對自動布局約束進行動畫時,我們必須在動畫塊中調(diào)用 layoutIfNeeded() 方法,否則動畫不會被執(zhí)行。
  • 通過公開的 getter 方法,讓我們的代碼保持簡潔。
  • 然后,在 viewDidLoad() 方法中,在 super 一句后添加:

    backingImageView.image = backingImage

    這里將先前從 SongViewController 截取的截圖設置為背景圖。

    最后在 viewDidAppear(_:) 方法最后添加:

    animateBackingImageIn()

    當視圖顯示時,執(zhí)行動畫。

    Build & run,選擇一首歌,點擊迷你播放器。你會看到當前 view controller 非常緩慢地后退到背景中……

    干得漂亮!完成了動畫中的第一部分。接下來一個相當重要的內(nèi)容,將迷你播放器中的縮略圖放大成卡片中的大圖。

    放大曲目圖片

    打開 Main.storyboard 展開視圖樹。

    你需要關(guān)注的是這些 view:

    • Cover Image Container:一個白色背景的 UIView。你將在 scroll view 中改變它的位置。
    • Cover Art Image:你將對這個 UIImageView 進行變形。它的背景是黃色,這樣它就很容易在 Xcode 中識別出來。這個視圖有兩個地方值得注意:

      • Aspect:設置為 1:1。這表示它總是一個正方形。
      • Height:一個固定的值。待會你會知道為什么。

    打開 MaxiSongCardViewController.swift。你可以看到這兩個 view 和關(guān)閉按鈕,都已經(jīng)創(chuàng)建和連接了對應的 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 顯示。它之前一直隱藏,以便為了讓你看清背景圖片上發(fā)生了什么。

    然后,在 viewDidLoad() 末尾添加下面幾行:

    coverImageContainer.layer.cornerRadius = cardCornerRadius coverImageContainer.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]

    這里只設置了上面兩個角的圓角半徑。

    Build & run,點擊迷你播放器,你會看到在背景的截圖上顯示了 container viwe 和 image view。

    注意看 image view 的圓角。它沒有使用一句代碼,完全是通過用戶定義的運行時屬性面板來實現(xiàn)的。

    設置封面圖片的約束

    在這一部分,你將添加封面圖片動畫中會用到的一些約束。

    打開 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。

    現(xiàn)在所有的約束都已經(jīng)創(chuàng)建完畢,可以執(zhí)行動畫了。

    Build & run;點擊一首曲目,然后點擊迷你播放器,確保一切正常。

    創(chuàng)建數(shù)據(jù)源協(xié)議

    你必須知道 cover image 動畫時候的起點位置。你可以傳遞一個迷你播放器的引用給大播放器,以便將所需信息傳遞給它,但這會在 MiniPlayerViewController 和 MaxiSongCardViewController 之間創(chuàng)建一個強依賴關(guān)系。除此之外,我們可以用協(xié)議來傳遞這個信息。

    關(guān)閉助手編輯器,添加下列協(xié)議到 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} }

    這里定義了一個協(xié)議,用于告訴大播放器需要動畫的信息。然后讓 MiniPlayerViewController 實現(xiàn)這個協(xié)議,以便提供相應的信息。UIView 內(nèi)置了一些轉(zhuǎn)換矩形和點的方法,將會非常有用。

    然后,打開 MaxiSongCardViewController.swift 在主類中添加下列屬性:

    weak var sourceView: MaxiPlayerSourceProtocol!

    這個屬性使用了弱引用以避免持有循環(huán)。

    打開 SongViewController.swift,在 expandSong 方法的 present(_,animated:) 一句前添加:

    maxiCard.sourceView = miniPlayer

    這里將起始 view 的引用在初始化時傳給大播放器。

    開始動畫

    在這一節(jié),你將所有艱苦工作的成功組裝起來,將 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()})} }

    上述代碼分為以下幾步:

  • 計算起始位置,用源視圖的坐標減去 scroll view 的垂直偏移。
  • 將 container view 放到開始位置。
  • 將 container view 動畫到結(jié)束位置。
  • 首先讓背景色以漸變形式變化,以免轉(zhuǎn)換過于突兀。
  • 其次以動畫方式修改 container 的 top inset 并漸入關(guān)閉按鈕。
  • 將 container 動畫到開始位置。這個方法稍后會用到。它是 animateImageLayerIn 的逆過程。
  • 然后,在 viewDidAppear(_:) 方法最后添加:

    animateImageLayerIn()

    這讓動畫開始走時間線。

    然后,在 viewWillAppear(_:) 方法添加:

    configureImageLayerInStartPosition()

    這樣,在視圖開始顯示之前到達開始位置。這里使用了 viewWillAppear 方法,這樣將 image layer 移動到開始位置的過程不會被用戶覺察到。

    Build & run,然后點擊迷你播放器以彈出大播放器。你會看到 container 會上行到指定位置。它的形狀沒有發(fā)生改變,因為 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 的動畫類似。讓我們來過一遍:

  • 將 cover image 通過 source view 中指定的信息放置到開始位置。
  • 將 cover image 動畫到終止位置。最后的高度等于 container 的寬度減去它的 insets。因為寬高比是 1:1,因此寬度等于高度。
  • 對于關(guān)閉動畫,將 cover image 動畫到開始位置。
  • 然后,在 viewDidAppear 方法中最后添加:

    animateCoverImageIn()

    這會在視圖顯示到屏幕上之后觸發(fā)動畫。

    然后,在 viewWillAppear 方法最后添加:

    coverArtImage.image = sourceView.originatingCoverImageView.image configureCoverImageInStartPosition()

    這里從數(shù)據(jù)源對象獲取了 UIImage,傳遞給 image view。在特定情況下這樣做是可以的,比如現(xiàn)在,因為 UIImage 中的像素足夠多,圖片不會被顆粒化或者被拉伸。

    Build & run,image view 從一開始的縮略圖長變大,同時 container view 的 frame 隨之變大。

    關(guān)閉動畫

    卡片頂部的按鈕被鏈接到了 dismissAction(_:) 方法。目前這個方法只會操作一個關(guān)閉動作,沒有任何動畫。

    和彈出 view controller 中所做的一樣,你需要讓 MaxiSongCardViewController 處理它自己的關(guān)閉動畫。

    打開 MaxiSongCardViewController.swift 將 dismissAction(_:) 修改為:

    @IBAction func dismissAction(_ sender: Any) {animateBackingImageOut()animateCoverImageOut()animateImageLayerOut() { _ inself.dismiss(animated: false)} }

    這播放了一個和之前的呈現(xiàn)動畫相反的動畫。當動畫完成,我們解散 MaxiSongCardViewController。

    Build & run,彈出大播放器,然后點關(guān)閉按鈕。封面圖片和 container view 又縮回到迷你播放器的樣子。但是有一個顯示上的問題,就是 Tab bar 會閃一下。我們后面會搞定它。

    顯示曲目信息

    再觀察一下音樂 app,你會發(fā)現(xiàn)打開的卡片中包含一個進度條和音量控制,還列出了歌曲名、藝術(shù)家、專輯和下一曲。這些并不是完全都放在了一個 view controller 中——而是封裝成組件。

    接下來的任務是在 scroll view 中嵌入一個 View controller。為了節(jié)省時間,已經(jīng)為你準備好了一個:SongPlayControlViewController。

    嵌入子控制器

    第一個任務是從 scroll view 中將底下的 image container 分離出來。

    打開 Main.storyboard。刪除 cover image container 底部到 superView 底部的約束。會提示有布局錯誤,說 scroll view 需要有一個 Y 坐標或者高度約束。不用管。

    然后,你需要創(chuàng)建一個子視圖控制器用于顯示歌曲詳情,步驟如下:

  • 添加一個 Container View 作為 Scroll view 的子 view。
  • 確保這個 Container Viw 位于視圖樹中的 Sketchy Skirt 的上層(也就是說它要在 Document Outline 中位于 Strechy Skirt 之下)。
  • 這會多出一個 segue 以及一個 view controller 對象。刪除這個自動添加的 view controller。
  • 現(xiàn)在為新添加的 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 缺乏高度約束的問題。

  • 選中 container view。
  • 打開 Add New Constraints 菜單。
  • 設置 Height 為 400。在 height 約束前面打勾。
  • 點擊 Add 1 Constraint。
  • 到此為止,所有的自動布局錯誤都將消失。

    播放控件的動畫

    接下來的效果是在動畫結(jié)束時,將播放控件從屏幕底部上移和 cover image 接在一起。

    在標準編輯器中打開 MaxiSongCardViewController.swift,在助手編輯器中打開 Main.storyboard。

    在 MaxiSongCardViewController 主類中添加屬性:

    //lower module constraints @IBOutlet weak var lowerModuleTopConstraint: NSLayoutConstraint!

    將這個 outlet 連接到 image container 和 Container View 之間的間距約束。

    關(guān)閉助手編輯器,在 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 之間的間距操作一個簡單動畫:

  • 隨便計算一個開始時的間距。 view 的高度減去寬度就行了。
  • 將控制器放到開始位置。
  • 根據(jù)不同方向,執(zhí)行動畫。
  • 一個助手方法,將控制器動畫到指定位置。
  • 將控制器移出。
  • 接下來將動畫添加到時間線。首先,在 viewDidAppear 方法最后添加:

    animateLowerModuleIn()

    在 viewWillAppear 方法最后添加:

    stretchySkirt.backgroundColor = .white // 避免封面圖片和下面的 songPlayControl 之間顯示出間隙 configureLowerModuleInStartPosition()

    然后,在 dismissAction 方法中,調(diào)用 animateImageLayerOut(completion:) 一句前執(zhí)行解散動畫:

    animateLowerModuleOut()

    最后,在 MaxiSongCardViewController.swift 中添加這個方法將當前歌曲傳遞給新控制器。

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {if let destination = segue.destination as? SongSubscriber {destination.currentSong = currentSong} }

    這里檢查了 destination 是否實現(xiàn)是一個 SongSubscriber,然后將歌曲傳遞給它。這里演示了一個簡單的依賴注入。

    Build & run。彈出大播放器界面,你會看到 SongPlayControl 的視圖會上移到指定位置。

    隱藏 Tab Bar

    在最終完成之前還有一個東西需要處理,這就是 tab bar。你可以修改 tab bar 的 frame,但這會將相關(guān)的 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。實際上,是對齊到了安全區(qū)。高度約束設置為 128,然后點擊 Add 4 Constaints 創(chuàng)建約束。

    接著,用助手編輯器打開 MaxiSongCardViewController.swift,將 3 個屬性連接到 Image view。

    • bottomSectionImageView 連接到 Image View。
    • bottomSectionLowerConstraint 連接到 Bottom 約束。
    • bottomSectionHeight 連接到 height 約束。

    最后,關(guān)閉助手編輯器,添加一個擴展到 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()}}} }

    這段代碼和其它動畫類似。每一步驟你都熟悉。

  • 用指定圖片賦給 image view,如果沒有圖片的話,將 height 設置為 0。
  • 將 image view 移到屏幕底部以下。
  • 將 image view 移到正常位置。
  • 最后一件事情是在這個文件里執(zhí)行動畫。

    首先,在 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 會上移然后下降到正常的位置。

    恭喜!你已經(jīng)模仿了一個“音樂” app 的卡片動畫(幾乎是重新建造的)。

    接下來做什么?

    你可以從這里下載完成后的項目。

    在本教程中,你學習了:

    • 對自動布局約束進行動畫。
    • 將多個動畫放入時間線以組合成復雜動畫。
    • 用靜態(tài)的截圖模擬動畫。
    • 用委托模式在兩個對象之間創(chuàng)建弱綁定。

    注意,靜態(tài)截圖的方法在卡片呈現(xiàn)時底層視圖被改變的情況下無法使用,在這種情況下異步事件會導致一個刷新動作。

    在開發(fā)中動畫的代價是昂貴的,而且要做出滿意的效果很難。但是,它常常是值得的,因為動畫為 app 增加了亮點,能夠讓普通的 app 變得與眾不同。

    希望本教程能夠激發(fā)你創(chuàng)建自己動畫的靈感。有任何建議后疑問,或者分享你的創(chuàng)意,請加入到討論中來!

    總結(jié)

    以上是生活随笔為你收集整理的模拟音乐 app 的 Now Playing 动画的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。