iOS 10 的一個重要更新-用 UIViewPropertyAnimator 編寫動畫

曾經的黑暗年代閉包

 

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

 

view.alpha = 1ide

 

UIView.animate(withDuration: 2) {函數

    containerView.alpha = 0工具

}動畫

 

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

 

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

 

Animation 的新紀元設計

 

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

 

爲了探索這個新的類,咱們來看幾個例子,這幾個例子都是演示一張圖片劃過屏幕的動畫。如同全部 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。

 

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

http://cubic-bezier.com/#.17,.67,.83,.67

 

貝塞爾曲線

 

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 跳轉的過渡動畫。另外還有一些有趣的例子,例如一些簡單的遊戲。

相關文章
相關標籤/搜索