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