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

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

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

曾經的黑暗年代

用基於 block 的 UIView animation 來編寫 view 屬性(frame, transform 等等)變化的動畫很是簡單。只須要短短几行代碼:github

view.alpha = 1

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

你能夠指定動畫結束以後調用的 completion block。若是默認的勻速動畫不能知足你的要求,還能夠調整時間曲線。閉包

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

Animation 的新紀元

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

爲了探索這個新的類,咱們來看幾個例子,這幾個例子都是演示一張圖片劃過屏幕的動畫。如同全部 Day by Day 系列的文章,例子的代碼能夠在 Github 上下載到。此次咱們用的是 Playground。編輯器

Playground 的準備

咱們全部的 playground 頁面都是讓一個小忍者劃過屏幕的動畫。爲了方便對比這些頁面的代碼,咱們把公共部分的代碼藏在 Sources 文件夾裏。這樣不只能簡化每一個頁面的代碼,還能加快編譯過程,由於 Sources 裏的代碼是預編譯過的。ide

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 倉薯翻譯

譯者:戴倉薯

相關文章
相關標籤/搜索