[iOS 10 day by day] Day 4:新的動畫 API UIViewPropertyAnimator

戴倉薯發表於2016-09-01

《iOS 10 day by day》是 shinobicontrols 公司編寫的系列部落格,介紹開發者需要了解的 iOS 10 新特性,每週更新。本系列翻譯(文集地址)已取得官方授權。目錄點此。倉薯翻譯,歡迎指正:)

Shinobicontrols 為 iOS 和 Android 開發者提供高效能、響應式的 UI 控制元件 SDK,尤其是圖表方面的控制元件。 官網 : shinobicontrols.com twitter : @shinobicontrols

曾經的黑暗年代

用基於 block 的 UIView animation 來編寫 view 屬性(frame, transform 等等)變化的動畫非常簡單。只需要短短几行程式碼:

view.alpha = 1

UIView.animate(withDuration: 2) {
    containerView.alpha = 0
}複製程式碼

你可以指定動畫結束之後呼叫的 completion block。如果預設的勻速動畫不能滿足你的要求,還可以調整時間曲線。

但是,如果你需要一種自定義的曲線動畫,相應的屬性變化首先要快速開始,然後再急速慢下來,該怎麼辦呢?另外一個有點麻煩的問題是,怎麼取消正在進行中的動畫?雖然這些問題都可以解決,用第三方庫或者建立一個新的 animation 來取代進行中的 animation。但蘋果在 UIKit 中新加的元件能把這些步驟簡化許多:進入UIViewPropertyAnimator的世界吧!

Animation 的新紀元

UIViewPropertyAnimator 的 API 設計得很完善,可擴充套件性也很好。它 cover 了傳統 UIView animation 動畫的絕大部分功能,並且大大增強了你對動畫過程的掌控能力。具體來說,你可以在動畫過程中任意時刻暫停,可以隨後再選擇繼續,甚至還能在動畫過程中動態改變動畫的屬性(例如,本來動畫終點在螢幕左下角的,可以在動畫過程中把終點改到右上角)。

為了探索這個新的類,我們來看幾個例子,這幾個例子都是演示一張圖片劃過螢幕的動畫。如同所有 Day by Day 系列的文章,例子的程式碼可以在 Github 上下載到。這次我們用的是 Playground。

Playground 的準備

我們所有的 playground 頁面都是讓一個小忍者劃過螢幕的動畫。為了方便對比這些頁面的程式碼,我們把公共部分的程式碼藏在 Sources 資料夾裡。這樣不僅能簡化每個頁面的程式碼,還能加快編譯過程,因為 Sources 裡的程式碼是預編譯過的。

Sources 裡包含一個簡單的UIView子類,叫做NinjaContainerView。它的唯一功能就是新增一個 UIImageView 作為子 view,來顯示我們的小忍者。我把忍者圖片加到了 Resources 裡。

import UIKit

public class NinjaContainerView: UIView {

    public let ninja: UIImageView = {
        let image = UIImage(named: "ninja")
        let view = UIImageView(image: image)
        view.frame = CGRect(x: 0, y: 0, width: 45, height: 39)
        return view
    }()

    public override init(frame: CGRect) {
        // Animating view
        super.init(frame: frame)

        // Position ninja in the bottom left of the view
        ninja.center = {
            let x = (frame.minX + ninja.frame.width / 2)
            let y = (frame.maxY - ninja.frame.height / 2)
            return CGPoint(x: x, y: y)
        }()

        // Add image to the container
        addSubview(ninja)
        backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
    }

    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// Moves the ninja view to the bottom right of its container, positioned just inside.
    public func moveNinjaToBottomRight() {
        ninja.center = {
            let x = (frame.maxX - ninja.frame.width / 2)
            let y = (frame.maxY - ninja.frame.height / 2)
            return CGPoint(x: x, y: y)
        }()
    }
}複製程式碼

現在,在每個 playground 頁面裡,我們可以複製貼上以下程式碼:

import UIKit
import PlaygroundSupport

// Container for our animating view
let containerView = NinjaContainerView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

let ninja = containerView.ninja

// Show the container view in the Assistant Editor
PlaygroundPage.current.liveView = containerView複製程式碼

這樣我們就可以用上 Playground 強大的 "Live View" 功能,不用啟動 iOS 模擬器就可以展示動畫效果。儘管 Playground 還是有些不好用的地方,但用來嘗試新功能是非常合適的。

要顯示 Live View,點選選單欄上的 View -> Assistant Editor -> Show Assistant Editor,或者點選右上角工具欄裡兩環相套的圖示。如果在右半邊的編輯器裡沒有看到 live view,要確保選中的是 Timeline 而不是 Manual —— 不得不承認我在這裡浪費了一點時間。

從簡單的開始

UIViewPropertyAnimator 的用法可以跟傳統的 animation block 一樣:

UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
    containerView.moveNinjaToBottomRight()
}.startAnimation()複製程式碼

這會觸發一個時長為 1 秒,時間曲線是緩進緩出的動畫。動畫的內容是閉包裡的部分。

最簡單的動畫

注意我們是通過呼叫 startAnimation() 來顯式啟動動畫的。另外一種建立 animator 的方法可以不用手動啟動動畫,就是 runningPropertyAnimator(withDuration:delay:options:animations:completion:)。確實有點長,所以可能還不如用第一種。

先建立好 animator ,再往上新增動畫也很容易:

// view 設定好之後,我們先來一個簡單的動畫
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut)

// 新增第一個 animation block
animator.addAnimations {
    containerView.moveNinjaToBottomRight()
}

// 然後再加第二個
animator.addAnimations {
    ninja.alpha = 0
}複製程式碼

這兩個 animation block 會同時進行。

兩個 animation block

新增 completion block 的方法也很類似:

animator.addCompletion {
    _ in
    print("Animation completed")
}

animator.addCompletion {
    position in
    switch position {
    case .end: print("Completion handler called at end of animation")
    case .current: print("Completion handler called mid-way through animation")
    case .start: print("Completion handler called  at start of animation")
    }
}複製程式碼

如果動畫完整跑完的話,我們可以在控制檯看到以下資訊:

Animation completed
Completion handler called at end of animation複製程式碼

進度拖拽和反向動畫

我們可以利用 animator 讓動畫跟隨拖拽的進度進行:

let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn)

// Add our first animation block
animator.addAnimations {
    containerView.moveNinjaToBottomRight()
}

let scrubber = UISlider(frame: CGRect(x: 0, y: 0, width: containerView.frame.width, height: 50))
containerView.addSubview(scrubber)

let eventListener = EventListener()
eventListener.eventFired = {
    animator.fractionComplete = CGFloat(scrubber.value)
}

scrubber.addTarget(eventListener, action: #selector(EventListener.handleEvent), for: .valueChanged)複製程式碼

Playground 總體來說是很好用的,而且還能在 Live View 裡面新增可互動的 UI 控制元件。然而,接受響應事件就有點麻煩,因為我們需要一個 NSObject 的子類來監聽諸如 .valueChanged 這種事件。所以,我們簡單建立一個 EventListener,一旦觸發它的 handleEvent 方法,它會呼叫我們的 eventFired 閉包。

這裡 fractionComplete 值的計算方法跟時間沒有關係了,所以我們的小忍者不再像之前指定的一樣,會優雅地緩動。

Property animator 最強大的功能體現在它能隨時打斷正在進行的動畫。讓動畫反向也非常容易,只需設定 isReversed 屬性即可。

為了演示這一點,我們使用關鍵幀動畫,這樣就可以製作一個多階段的動畫了:

animator.addAnimations {
    UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [.calculationModeCubic], animations: {
        UIView.addKeyframe(withRelativeStartTime: 0,  relativeDuration: 0.5) {
            ninja.center = containerView.center
        }
        UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
            containerView.moveNinjaToBottomRight()
        }
    })
}

let button = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 30)))
button.setTitle("Reverse", for: .normal)
button.setTitleColor(.black(), for: .normal)
button.setTitleColor(.gray(), for: .highlighted)
let listener = EventListener()
listener.eventFired = {
    animator.isReversed = true
}

button.addTarget(listener, action: #selector(EventListener.handleEvent), for: .touchUpInside)
containerView.addSubview(button)

animator.startAnimation()複製程式碼

按下按鈕的時候,animator 就會把動畫反向進行,只要這一時刻動畫還沒結束。

自定義時間曲線

Property animator 在簡潔優美的同時,還有很強的擴充套件性。如果你需要在蘋果提供的時間函式之外自定義另一種時間曲線,只需傳進一個實現 UITimingCurveProvider 協議的物件。大部分情況下用到的是 UICubicTimingParameters 或者 UISpringTimingParameters

例如,我們想讓小忍者在劃過螢幕的過程中,先快速加速,然後再慢慢停止。如下圖的貝塞爾曲線所示(繪製曲線用了這個很方便的線上工具):

貝塞爾曲線

let bezierParams = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.05, y: 0.95),
                                                   controlPoint2: CGPoint(x: 0.15, y: 0.95))

let animator = UIViewPropertyAnimator(duration: 4, timingParameters:bezierParams)

animator.addAnimations {
    containerView.moveNinjaToBottomRight()
}

animator.startAnimation()複製程式碼

擴充套件閱讀

新的 property animator 讓編寫動畫更簡單,它的 API 跟傳統方法類似,還新增了打斷動畫、自定義時間曲線等功能。

Apple 為 UIViewPropertyAnimator 提供了詳盡的文件。另外,也可以看看這場 WWDC 視訊,深度解讀這些新的 API,還講了怎麼用新的 API 來做 viewController 跳轉的過渡動畫。另外還有一些有趣的例子,例如一些簡單的遊戲

原文地址:iOS 10 Day by Day :: Day 4 :: UIViewPropertyAnimator

原作者:Sam Burnstone @sam_burnstone

ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols

文集地址:iOS 10 day by day 倉薯翻譯

譯者:戴倉薯