動畫實作

ViewController 轉場初階指南:簡單打造酷炫的轉場動畫

一直以來,很多開發者都費盡心思去了解視圖控制器 (ViewController) 之間的轉換,可是,線上能簡單說明視圖控制器動畫轉場的教程卻非常少。這篇文章由 Fran Obarrio 帶大家客製由一個視圖控制器轉場到另一個的動畫,讓大家用最簡單的方法,創建令人驚艷的轉場動畫。
ViewController 轉場初階指南:簡單打造酷炫的轉場動畫
ViewController 轉場初階指南:簡單打造酷炫的轉場動畫
In: 動畫實作, iOS App 程式開發, Swift 程式語言, Xcode

本篇原文(標題:Cool Custom ViewController Transitions — Advanced)刊登於作者 Medium,由 Fran Obarrio 所著,並授權翻譯及轉載。

今天,我們會客製由一個視圖控制器 (ViewController) 轉場到另一個的轉場動畫。

為什麼會想要寫這個主題呢?過去,我花了很多時間去了解視圖控制器之間的轉換。遺憾的是,目前線上鮮有能簡單說明視圖控制器動畫轉場的教程。

因此,我決定寫幾篇文章,教大家用最簡單的方法,創建令人驚艷的轉場動畫。

讓我們開始來創建客製化的 UINavigationController 轉場動畫吧!

先看一下我們最後的成果:

view-controller-demo

專案

讓我們先花點時間來了解這個專案和以下元素:

  • MainViewController.swift:主視圖控制器,你將會在這裡呈現 CollectionView,並推向 PushedViewController。
  • MainViewController+DataSource.swift:我們在這檔案放置 CollectionView DataSource 方法。
  • MainViewController+ProtocolTransitions.swift:在這裡,我們可以看到要製作動畫的每個物件的協定 (Protocol),包括 CellImageView、BackgroundColor View、和 Cell Background View。
  • PushedViewController.swift:專案中的第二個視圖控制器,它就是動畫的最終狀態。
  • AnimationCollectionViewLayout.swift:這是用在 MainViewController CollectionView 的 CollectionView Layout。
  • CardCell.swift:這是 UICollectionViewCell,我們將在專案中 MainViewController 的 CollectionView 上用到它。
  • Helpers.swift:我們會在這裡定義在 MainViewController 和 PushedViewController 上使用的協定,以獲取要進行動畫處理的物件的參考。
  • PushAnimator.swift: 此類別將繼承自 NSObject,並將實作 UIViewControllerAnimatedTransitioning 協定,以從 MainViewController 轉換到 PushedViewController。
  • PopAnimator.swift: 此類別將繼承自 NSObject ,並將實作 UIViewControllerAnimatedTransitioning 協定,以從 PushedViewController 轉換到 MainViewController。
  • TransitionCoordinator.swift: 此類別將繼承自 NSObject,並將實作 UINavigationControllerDelegate 協定。 在此類別中,我們定義 push 時使用 PushAnimator 的轉場,以及在 pop 時使用的 PopAnimator 轉場效果。

小提示:當視圖控制器從 UINavigationController 的堆疊 (stack) 中 push 和 pop 時,UINavigationControllerDelegate 協定定義了 UINavigationController 可以實現的方法。

實現動畫的步驟

1. 正常狀態

viewcontroller-1

2. CollectionView Background Cell 縮小

viewcontroller-2

3. CollectionView Background 擴展到容器的寬度,並刪除圓形的邊框

viewcontroller-3

4. CollectionView Background 會擴展至這個視圖控制器背景的底部和頂部,並將瓶子移動到頂部

viewcontroller-4

讓我們進入動畫控制器!

在這邊,我們將提供 2 個客製化的轉場動畫:一個用來 push,一個用在 pop。現在讓我們專注在 push 動畫,首先,我們需要準備什麼?

一個動畫控制器 (Animation Controller) !

該控制器需要實作下列兩種方法,以符合 UIViewControllerAnimatedTransitioning() 協定。

1. transitionDuration:整個動畫的持續時間

transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

2. animateTransition(using:): 這是動畫邏輯所在

animateTransition(using transitionContext: UIViewControllerContextTransitioning)

讓我們開始第一個動畫吧。

  • PushAnimator:
class PushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    }

}

在轉場期間,我們需要提供一個容器視圖 (container view),讓轉場動畫在裡面運行。

  • 以下是需要添加到控制器視圖的 transitionContext 視圖:
class PushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView        

        guard let fromVC = transitionContext.viewController(forKey: .from) as? animTransitionable,
            let toVC = transitionContext.viewController(forKey: .to) as? animTransitionable else {
                transitionContext.completeTransition(false)
                return
        }

        let fromViewController = transitionContext.viewController(forKey: .from)!
        fromViewController.view.backgroundColor = UIColor.clear

        let toViewController = transitionContext.viewController(forKey: .to)!

    }

}

我們將會新增一個協定,用來獲取要執行動畫的視圖單元格的參考。該協定可用於 MainViewController 和 PushedViewController。

protocol animTransitionable {
    var cellImageView: UIImageView { get }
    var backgroundColor: UIView { get }
    var cellBackground: UIView { get }
}

所以⋯⋯ 我們要如何透過動畫使物件從一個視圖控制器移動到另一個呢?我們需要創建每個視圖,將它們添加到容器視圖中,並設置動畫。

來看一下如何實作程式碼吧:

class PushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView        

        guard let fromVC = transitionContext.viewController(forKey: .from) as? animTransitionable,
            let toVC = transitionContext.viewController(forKey: .to) as? animTransitionable else {
                transitionContext.completeTransition(false)
                return
        }

        let fromViewController = transitionContext.viewController(forKey: .from)!
        fromViewController.view.backgroundColor = UIColor.clear

        let toViewController = transitionContext.viewController(forKey: .to)!

        let imageViewSnapshot = UIImageView(image: fromVC.cellImageView.image)
        imageViewSnapshot.contentMode = .scaleAspectFit


        // Background View With Correct Color
        let backgroundView = UIView()
        backgroundView.frame = fromVC.backgroundColor.frame
        backgroundView.backgroundColor = fromVC.backgroundColor.backgroundColor
        containerView.addSubview(backgroundView)


        // Cell Background
        let cellBackground = UIView()
        cellBackground.frame = containerView.convert(fromVC.cellBackground.frame, from: fromVC.cellBackground.superview)
        cellBackground.backgroundColor = fromVC.cellBackground.backgroundColor
        cellBackground.layer.cornerRadius = fromVC.cellBackground.layer.cornerRadius
        cellBackground.layer.masksToBounds = fromVC.cellBackground.layer.masksToBounds


        containerView.addSubview(fromViewController.view)
        containerView.addSubview(toViewController.view)

        containerView.addSubview(cellBackground)
        containerView.addSubview(imageViewSnapshot)

        fromViewController.view.isHidden = true
        toViewController.view.isHidden = true


        imageViewSnapshot.frame = containerView.convert(fromVC.cellImageView.frame, from: fromVC.cellImageView.superview)
    }

}

來到這一步,我們得到:

  1. ImageView(瓶子)
  2. BackgroundView(背景色)
  3. CellBackground View(CollectionView 的 Card View,它將在 PushedViewController 中展開成大背景)

現在讓我們看一下動畫:

class PushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView        

        guard let fromVC = transitionContext.viewController(forKey: .from) as? animTransitionable,
            let toVC = transitionContext.viewController(forKey: .to) as? animTransitionable else {
                transitionContext.completeTransition(false)
                return
        }

        let fromViewController = transitionContext.viewController(forKey: .from)!
        fromViewController.view.backgroundColor = UIColor.clear

        let toViewController = transitionContext.viewController(forKey: .to)!

        let imageViewSnapshot = UIImageView(image: fromVC.cellImageView.image)
        imageViewSnapshot.contentMode = .scaleAspectFit


        // Background View With Correct Color
        let backgroundView = UIView()
        backgroundView.frame = fromVC.backgroundColor.frame
        backgroundView.backgroundColor = fromVC.backgroundColor.backgroundColor
        containerView.addSubview(backgroundView)


        // Cell Background
        let cellBackground = UIView()
        cellBackground.frame =  containerView.convert(fromVC.cellBackground.frame, from: fromVC.cellBackground.superview)
        cellBackground.backgroundColor = fromVC.cellBackground.backgroundColor
        cellBackground.layer.cornerRadius = fromVC.cellBackground.layer.cornerRadius
        cellBackground.layer.masksToBounds = fromVC.cellBackground.layer.masksToBounds


        containerView.addSubview(fromViewController.view)
        containerView.addSubview(toViewController.view)

        containerView.addSubview(cellBackground)
        containerView.addSubview(imageViewSnapshot)

        fromViewController.view.isHidden = true
        toViewController.view.isHidden = true


        imageViewSnapshot.frame = containerView.convert(fromVC.cellImageView.frame, from: fromVC.cellImageView.superview)


        let frameAnim1 = CGRect(x: 0, y: cellBackground.frame.minY, width: UIScreen.main.bounds.width, height: cellBackground.frame.height)
        let frameAnim2 = CGRect(x: 0, y: toVC.cellBackground.frame.minY, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height - toVC.cellBackground.frame.minY)


        let animator1 = {
            UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1.3) {
                cellBackground.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
            }
        }()

        let animator2 = {
            UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.9) {
                cellBackground.layer.cornerRadius = 0
                cellBackground.frame = frameAnim1
            }
        }()

        let animator3 = {
            UIViewPropertyAnimator(duration: 0.2, dampingRatio: 1.4) {
                cellBackground.frame = frameAnim2
                imageViewSnapshot.frame = containerView.convert(toVC.cellImageView.frame, from: toVC.cellImageView.superview)
            }
        }()


        animator1.addCompletion { _ in
            animator2.startAnimation()
        }

        animator2.addCompletion {  _ in
            animator3.startAnimation(afterDelay: 0.1)
        }


        animator3.addCompletion {  _ in

            imageViewSnapshot.removeFromSuperview()
            cellBackground.removeFromSuperview()
            fromViewController.view.removeFromSuperview()

            toViewController.view.isHidden = false

            transitionContext.completeTransition(true)


        }

        animator1.startAnimation()

    }
}

以下是動畫的簡要說明:

Animator 1:我們將 CellBackground 視圖縮減為原本的 0.9 倍大

Animator 2:我們將它擴展到主視圖的寬度

Animator 3:我們將 CellBackground 的寬拉滿主視圖,高度則是根據 toVC (PushedViewController) 的 CellBackground,再加上我們為瓶子添加的動畫而定。

而 Pop animator 的製作方法是一樣的,只是順序相反。讓我們看看程式碼:

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {


    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView

         guard let fromVC = transitionContext.viewController(forKey: .from) as? animTransitionable,
         let toVC = transitionContext.viewController(forKey: .to) as? animTransitionable else {
         transitionContext.completeTransition(false)
         return
         }

        let fromViewController = transitionContext.viewController(forKey: .from)!
        fromViewController.view.backgroundColor = UIColor.clear

        let toViewController = transitionContext.viewController(forKey: .to)!

        let imageViewSnapshot = UIImageView(image: fromVC.cellImageView.image)
        imageViewSnapshot.contentMode = .scaleAspectFit
        imageViewSnapshot.frame = containerView.convert(fromVC.cellImageView.frame, from: fromVC.cellImageView.superview)


        //Background View Correct Color
        let backgroundView = UIView()
        backgroundView.frame = fromVC.backgroundColor.frame
        backgroundView.backgroundColor = fromVC.backgroundColor.backgroundColor

        // Cell Background
        let cellBackground = UIView()
        cellBackground.frame =  containerView.convert(fromVC.cellBackground.frame, from: fromVC.cellBackground.superview)
        cellBackground.backgroundColor = fromVC.cellBackground.backgroundColor

        let cellBackgroundToVC = containerView.convert(toVC.cellBackground.frame, from: toVC.cellBackground.superview)
        let imageViewToVC = containerView.convert(toVC.cellImageView.frame, from: toVC.cellImageView.superview)


        containerView.addSubview(toViewController.view)
        containerView.addSubview(cellBackground)
        containerView.addSubview(imageViewSnapshot)

        fromViewController.view.isHidden = true
        toViewController.view.isHidden = true



        let frameAnim1 = CGRect(x: fromVC.cellBackground.frame.minX, y: cellBackgroundToVC.minY , width: UIScreen.main.bounds.width, height: cellBackgroundToVC.height)
        let frameAnim2 = CGRect(x: cellBackgroundToVC.minX, y: cellBackgroundToVC.minY, width: cellBackgroundToVC.width, height: cellBackgroundToVC.height )
        let frameImageToVC = containerView.convert(toVC.cellImageView.frame, from: toVC.cellImageView.superview)


        let animator1 = {
            UIViewPropertyAnimator(duration: 0.3, curve: .easeOut) {
                cellBackground.frame = frameAnim1
            }
        }()

        let animator2 = {
            UIViewPropertyAnimator(duration: 0.3, curve: .easeOut) {
                imageViewSnapshot.frame = CGRect(x: frameImageToVC.minX, y: cellBackgroundToVC.minY - (toVC.cellImageView.frame.height / 2) , width: imageViewToVC.width, height: imageViewToVC.height)
            }
        }()

        let animator3 = {
            UIViewPropertyAnimator(duration: 0.35, dampingRatio: 0.6) {
                cellBackground.frame = frameAnim2
                cellBackground.layer.cornerRadius = 10

                imageViewSnapshot.frame = frameImageToVC

            }
        }()


        // Animations Completion Handler
        animator1.addCompletion {  _ in
             animator3.startAnimation()
        }

        animator3.addCompletion { _ in

            imageViewSnapshot.removeFromSuperview()
            cellBackground.removeFromSuperview()

            toViewController.view.isHidden = false

            transitionContext.completeTransition(true)
        }

        animator1.startAnimation()
        animator2.startAnimation()


    }
}

所以,這裡要做的最後一件事,就是製作 CollectionView Cell(單元格)滑動時,背景色的動畫處理。

     func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

        var visibleRect = CGRect()

        visibleRect.origin = collectionView.contentOffset
        visibleRect.size = collectionView.bounds.size

        let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)

        guard let indexPath = collectionView.indexPathForItem(at: visiblePoint) else { return }

        UIView.animate(withDuration: 0.3) {
            self.backgroundView.backgroundColor  =  self.dictionaryDataArray[indexPath.row]["hexBackgroundColor"]!.hexColor
        }        

    }

來到這一步,我們已經製作好所需的一切了,只需要在兩個視圖控制器之間添加 TransitionCoordinator。

請在 MainViewController 添加以下程式碼:

let transition = TransitionCoordinator()

當單元格被點擊後,我們會將這個 transition 物件添加到 NavigationController delegate 中。

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        if (collectionView.cellForItem(at: indexPath) as? CardCell) != nil {

            // Reference for the selected Cell
            self.selectedIndexPath = indexPath

            let finalVC = PushedViewController()
            finalVC.selectedImage = dictionaryDataArray[indexPath.row]["bottleName"]!
            finalVC.topHexColor = dictionaryDataArray[indexPath.row]["hexBackgroundColor"]!

            navigationController?.delegate = transition
            navigationController?.pushViewController(finalVC, animated: true)

        }        
    }

讓我們看一下 TransitionCoordinator 的程式碼:

// NavigationController Delegate for Transition Coordinator
class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        switch operation {
        case .push:
            return PushAnimator()
        case .pop:
            return PopAnimator()
        default:
            return nil
        }

    }

}

此類別設置 NavigationController delegate,並檢查我們正在執行哪種類型的操作:如果正在執行 push,它就會回傳一個 PushAnimator() 物件;而如果是 pop,它就會回傳一個 PopAnimator() 物件。

總結

這個轉場效果剛開始接觸可能有點困難,但請相信我,這實作起來很簡單! 希望你喜歡這篇文章!

如果你有任何評論、問題或建議,請隨時在下面留言!

你可以在這裡下載 Github 原始碼。

本篇原文(標題:Cool Custom ViewController Transitions — Easy)刊登於作者 Medium,由 Fran Obarrio 所著,並授權翻譯及轉載。

聯絡作者:Fran Obarrio,你可以透過 LinkedInTwitter、或 Email 聯絡他。

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。

聯絡方式:電郵:[email protected]
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。