終于iOS11里,擁有了傻瓜化的交互式動畫
回顧
我們先思考一個問題:iOS11 之前創(chuàng)建哪類動畫最麻煩?
答:交互式動畫和自定義的timingFunction動畫。
無code無真相。我們先來看看早先版本的動畫接口是如何實現(xiàn)交互式動畫和自定義timingFunciton的。
如何實現(xiàn)一個交互式動畫?
大家知道,iOS里面動畫的實現(xiàn)方式主要是兩種,一種是UIViewAnimation和基于Layer層的CAAnimation。
兩種動畫的區(qū)別很多,當然,符合越底層的接口自由度越高的這個特點。CAAnimation的可定制性更強,但是在我看來,兩種動畫最主要的區(qū)別用一句話形容,就是.
UIViewAnimation是開弓沒有回頭箭。CAAnimation是流星錘,可收可放。
我們現(xiàn)在,就來實現(xiàn)一個用手勢控制的動畫。效果如圖。
我們的目的是利用UISlider控制動畫的進度,這個動畫就是圖片繞Y軸旋轉(zhuǎn)。
代碼如下。
- class ViewController: UIViewController {
- let imageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
- override func viewDidLoad() {
- super.viewDidLoad()
- imageView.image = UIImage.init(named: "wuyanzu.jpg")
- imageView.center = self.view.center
- imageView.layer.transform.m34 = -1.0/500
- self.view.addSubview(imageView)
- let basicAnimation = CABasicAnimation.init(keyPath: "transform.rotation.y")
- basicAnimation.fromValue = 0
- basicAnimation.toValue = CGFloat.pi
- basicAnimation.duration = 1
- imageView.layer.add(basicAnimation, forKey: "rotate")
- imageView.layer.speed = 0
- // Do any additional setup after loading the view, typically from a nib.
- }
- @IBAction func sliderValueChanged(sender:UISlider) {
- imageView.layer.timeOffset = CFTimeInterval(sender.value)
- }
- }
在iOS11之前,可交互動畫的原理很簡單。過程總結如下。
- 將layer的speed設置為0,這樣,動畫就處于暫停狀態(tài)
- 利用timeOffset來控制整個動畫的進度
再舉個例子,如果這個動畫不是利用UISlider控制旋轉(zhuǎn)角度,而是利用PanGesture移動的距離來控制呢?
那么這種情況,你需要找到的就是手勢的距離和Rotate動畫timeOffset的一種關聯(lián)。
我利用Sketch做了一個簡陋的草圖來模擬這種情況。
其實看完圖片我們已經(jīng)可以建立起手勢移動距離和timeOffset的關聯(lián)。
以橫向移動為前提,那么手指的x坐標/圖片的width 總是 <= 1.0,所以,當旋轉(zhuǎn)動畫的總時長為1,那么動畫的進度timeOffset就恰好等于x/imageView.width了。***的關聯(lián)了起來。
問題
我們也看到了這種處理方法的弊端。就是,實在太繁瑣了。
所以,在今年的wwdc里,蘋果為我們提供了一種非常方便的解決方案。
UIViewPropertyAnimator
其實在iOS10,蘋果已經(jīng)引入了另外一種基于View層的強大的動畫框架,UIViewPropertyAnimator.
他提供了一個非常棒的方法來解決以前自定義timingFunction只能由CAAnimation來處理的問題。
timingFunction
說到timingFunction,相信寫過動畫的人都非常清楚系統(tǒng)提供的幾種。
- Liner (線性)
- EaseIn (先慢后快)
- EaseOut (先快后慢)
- EaseInEaseOut (慢進,加速,減速)
實際上這幾種timingFunction只能說是勉強夠用。當你想更細致調(diào)整動畫速率的時候勢必會使用自定義的貝塞爾曲線來控制動畫速率。
比如在http://cubic-bezier.com/,我創(chuàng)建了一個自定義的曲線。
他的control point 分別是(0.17, 0.67, 0.71, 0.15)
那么,如果你想用這個貝塞爾曲線當做timingFunction,在iOS10之前你只能利用CABasicAnimatin來實現(xiàn)。
例如,***個旋轉(zhuǎn)動畫自定義timingFunction是這樣的。
- basicAnimation.timingFunction = CAMediaTimingFunction.init(controlPoints: 0.17, 0.67, 0.71, 0.15)
想在View層自定義timingFunction?
沒門。
所幸,我們在iOS10的時候擁有了UIViewPropertyAnimator
現(xiàn)在,我們?nèi)绱撕唵蔚木蛣?chuàng)建了一個自定義動畫速率的動畫。
- let convenienceAnimator = UIViewPropertyAnimator.init(duration: 0.66, controlPoint1: point1, controlPoint2: point2) {
- }
- convenienceAnimator.addCompletion({ (position) in
- if position == .end {
- }
- })
- convenienceAnimator.startAnimation()
iOS11中更強大的UIViewPropertyAnimator
session 230中,蘋果著重介紹了我們夢寐以求的簡單方便的交互式動畫api。
舉一個session 230中的例子來看一下新版本中如何實現(xiàn)交互式動畫。
這里,我們需要用手勢來控制動畫的進度。這里,動畫是讓小球從左向右移動100的距離。
看看代碼如何簡單的將動畫和手勢關聯(lián)起來。
- var animator:UIViewPropertyAnimator!
- var circle:UIImageView!
- func handlePan(recognizer:UIPanGestureRecognizer) {
- switch recognizer.state {
- case .began:
- animator = UIViewPropertyAnimator.init(duration: 1, curve: .easeOut, animations: {
- self.circle.frame = self.circle.frame.offsetBy(dx: 100, dy: 0)
- })
- animator.pauseAnimation()
- case .changed:
- let translation = recognizer.translation(in: self.circle)
- animator.fractionComplete = translation.x/100
- case .ended:
- animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
- default:
- break
- }
- }
手勢開始的時候創(chuàng)建animator。然后暫停,在這里,動畫暫停的本質(zhì)同樣是將Layer的speed設置為0。
動畫的完成率等同于手勢移動的距離除以總距離。
當手勢結束的時候,我們調(diào)用了continueAnimation讓動畫繼續(xù)執(zhí)行到結束。其實這種需求比較少見,最常見的應該是當手勢結束的時候讓動畫停留在這個階段而不是繼續(xù)進行動畫。
在這里,我們改造一下這個動畫,讓它更符合我們的用戶習慣。
首先,在手勢事件的外部定義好這個animator。
- circle.backgroundColor = UIColor.red
- circle.layer.cornerRadius = 10
- circle.frame = CGRect.init(x: 10, y: 100, width: 20, height: 20)
- circle.isUserInteractionEnabled = true
- self.view.addSubview(circle)
- animator = UIViewPropertyAnimator.init(duration: 1, curve: .easeOut, animations: {
- self.circle.frame = self.circle.frame.offsetBy(dx: 100, dy: 0)
- })
- animator.pauseAnimation()
然后,手勢的事件代碼如下。
- func handlePan(recognizer:UIPanGestureRecognizer) {
- switch recognizer.state {
- case .began:
- progress = animator.fractionComplete
- case .changed:
- let translation = recognizer.translation(in: self.circle)
- animator.fractionComplete = translation.x/100 + progress
- case .ended:
- break
- default:
- break
- }
- }
在這里,我們多了一個叫做progress的變量,這個變量的作用就是記錄當前動畫的進度,在每次手勢變化的時候,讓動畫保持連貫性。不然,每一次動畫都重新執(zhí)行了。
建議同學們這里自己用代碼試驗一下效果。
出現(xiàn)了一些問題?
話說講到這里,我不知道有沒有同學會對一個非常重要的問題感到疑惑。
什么問題呢?
就是創(chuàng)建animator的時候的timingFunction是EaseOut,先快后慢,那么理論上應該是手勢移動了一半,動畫早就進行的超過了一半才對。
因為EaseOut的動畫曲線是這樣的
注意看這張圖的橫縱坐標。
X坐標代表Time的進度,Y坐標代表動畫的進度。
當X走到51%的時候,動畫已經(jīng)進行了72%。
在我們的場景中,這意味著,當手勢移動了51個pixel的時候,circle這個view已經(jīng)跑了72個pixel。
想想這會造成什么問題?
問題就是,用戶在交互的時候完全摸不著頭腦。
再舉個形象的例子。
- 加入有一個UISlider控制一個Animator的進度,這個Animator是作用于View的透明度Alpha從1到0。
- 然后Animator的timingFunction是EaseOut,那么用戶拖動UISlider的結果很可能是Slider還沒滑動到底,這個View的alpha已經(jīng)變成了0.
為了避免這種情況,當你的Animator是Interactive狀態(tài)的時候,蘋果會自動把你的timingFunction轉(zhuǎn)變?yōu)長inear.
如圖
那么如果你真的希望可交互式動畫的timingFunction不是自動轉(zhuǎn)變?yōu)長iner,能不能做到呢?
答案是可以的。
蘋果在iOS11中為UIViewPropertyAnimator提供了一個Bool值scrubsLinearly,只要設置為No,那么動畫就會按照你設置的timingFunction執(zhí)行了。
第二個問題,動畫執(zhí)行完了怎么辦?
其實在手勢執(zhí)行完畢的時候,調(diào)用animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
會將動畫執(zhí)行完成,但是有一個問題是,動畫一旦執(zhí)行完成,動畫的狀態(tài)就會從Interactive轉(zhuǎn)變?yōu)锳ctive,也就是說,不可以再進行交互了。這時候,你需要把animator的pauseOnCompletion設置為false。那么動畫就會一直保持Interactive狀態(tài)了。
SpringAnimation
說實話,這期session中,讓我比較失望的就是蘋果對于SpringAnimation的支持只是簡單地增加了一個under-damping的概念。并沒有加入springAnimation中很重要的兩個屬性。
- Fricition
- Tension
為什么這兩個屬性非常重要。這里,我需要給大家介紹一個國外非常流行的app。Principle
他是國外做交互式prd的非常好用的一個app,我最近在做的一個app在做交互原型的時候大量的使用了這個app。
我們來看看這個app中對于spring動畫的一些設置。
用damping這個參數(shù)調(diào)spring***的問題就是.....無法當伸手黨,直接拿來參數(shù)用。
所以,目前來說,***用的SpringAnimation還是facebook得pop。
比如...... 一個pop伸手黨的日常是這樣的。
- let alphaSpring = POPSpringAnimation.init(propertyNamed: kPOPViewAlpha)
- alphaSpring?.fromValue = 0.67
- alphaSpring?.toValue = 1
- alphaSpring?.dynamicsFriction = 20.17
- alphaSpring?.dynamicsTension = 381.47
- alphaSpring?.delegate = self
- alphaSpring?.name = "alpha"
- self.pop_add(alphaSpring, forKey: "alpha")
只能說,用pop好省心。
補充
cornerRadius終于可動畫了。
提出兩個問題
- iOS11之前真的沒有支持手勢交互的api么?
- 如果存在這樣的api,那么這個api的原理是什么呢?是怎樣實現(xiàn)無論是UIViewAnimation還是CABasicAnimation都能無縫和手勢關聯(lián)的呢?
這是兩個很有意思的問題,大家有空可以思考一下。