所有示例代碼均可以在 Animations-Demo 下載到
iOS 中實(shí)現(xiàn)動(dòng)畫有好幾種方式,UIView 無疑是最簡單的一種,但是所有的動(dòng)畫歸根結(jié)底還是 layer 層的動(dòng)畫。UIView 層面的動(dòng)畫只是對(duì) layer 層部分屬性的封裝。我們可以直接對(duì) UIView 的 alpha 、bounds 、center 、frame、transform、backgroundColor(如果view沒有實(shí)現(xiàn)draw(_:))。上面這些屬性看起來不多,但是足夠滿足大部分日常開發(fā)動(dòng)畫。
在 UIView 中執(zhí)行一個(gè)動(dòng)畫非常簡單,系統(tǒng)已經(jīng)幫我們封裝好了一切,你只需要將你想要?jiǎng)赢嫷膶傩苑诺?animations 的閉包中即可。
UIView.animate(withDuration: 0.4) {
self.v.backgroundColor = UIColor.red
}
閉包中可以同時(shí)執(zhí)行多個(gè)屬性的動(dòng)畫,也可以是多個(gè)view的動(dòng)畫
UIView.animate(withDuration: 0.4) {
self.v.backgroundColor = UIColor.red
self.v.center.y += 100
self.v2.alpha = 0
}
如果我們想讓一個(gè)屬性在 animations 閉包中執(zhí)行,但是又不要執(zhí)行動(dòng)畫,可以這樣。
UIView.animate(withDuration: 0.4) {
self.v.backgroundColor = UIColor.red
UIView.performWithoutAnimation {
self.v2.alpha = 0
}
}
放在 performWithoutAnimation 閉包中就會(huì)不執(zhí)行動(dòng)畫了, 這個(gè)在有時(shí)候做項(xiàng)目的時(shí)候某個(gè)功能總是會(huì)莫名其妙的調(diào)一下或者執(zhí)行一個(gè)很奇怪的動(dòng)畫,這時(shí)候可以把那段 code 放在這個(gè)閉包中,就不會(huì)有動(dòng)畫了。
iOS 9 開始 , UIVisualEffectView 的 effect 屬性也是可以動(dòng)畫的。動(dòng)畫前設(shè)置為 nil ,在animations: 的block 中設(shè)置 effect
let effect = UIBlurEffect(style: .dark)
effectView.effect = nil
UIView.animate(withDuration: 1) {
self.effectView.effect = effect
}

UIView 動(dòng)畫 options
UIView 動(dòng)畫比較完整的版本并不是上面那么簡短,還有很多其他的參數(shù)可以配置
animate(withDuration:, delay: , options: , animations: , completion: )
- withDuration:動(dòng)畫的持續(xù)時(shí)間,也可理解為動(dòng)畫的執(zhí)行速度,持續(xù)時(shí)間越小速度越快
- delay:動(dòng)畫開始之前的延時(shí),默認(rèn)是無延時(shí)。
- options:一個(gè)附加選項(xiàng),
UIViewAnimationOptions可以指定多個(gè) - animations:執(zhí)行動(dòng)畫的閉包
- completion:動(dòng)畫完成后執(zhí)行的閉包,可以為nil,可以在這里鏈接下一個(gè)動(dòng)畫。
下面是一些主要的options (UIViewAnimationOptions) :
- 動(dòng)畫執(zhí)行對(duì)應(yīng)的曲線(緩沖): 動(dòng)畫執(zhí)行過程速度的改變,會(huì)有一個(gè)加速度或者一個(gè)減速度。
.curveEaseIn.curveEaseOut.curveEaseInOut.curveLinear
-
.repeat: 指定這個(gè)選項(xiàng)后,動(dòng)畫會(huì)無限重復(fù) -
.autoreverse:往返動(dòng)畫,從開始執(zhí)行到結(jié)束后,又從結(jié)束返回開始。
但是這里會(huì)有個(gè)問題,如果我們讓一個(gè)view 移動(dòng)100pt。使用 .autoreverse , 代碼如下
let opts = UIViewAnimationOptions.autoreverse
UIView.animate(withDuration: 1, delay: 0, options: opts, animations: {
self.view2.center.x -= 100
}, completion: nil)

發(fā)現(xiàn)很順利的往返之后,又跳了一下,是因?yàn)槲覀僾iew2的center 其實(shí)已經(jīng)改變了。如果想讓它回到原位,只需要在完成時(shí)候指定它的位置即可
let xorig = self.view2.center.x
let opts = UIViewAnimationOptions.autoreverse
UIView.animate(withDuration: 1, delay: 0, options: opts, animations: {
self.view2.center.x -= 100
}, completion: { _ in
self.view2.center.x = xorig
})

如果想讓這個(gè)動(dòng)畫無限次循環(huán),只需要加一個(gè)option
let opts: UIViewAnimationOptions = [.autoreverse , .repeat]
如果需要執(zhí)定循環(huán)次數(shù)
let xorig = self.view2.center.x
let opts: UIViewAnimationOptions = UIViewAnimationOptions.autoreverse
UIView.animate(withDuration: 1, delay: 0, options: opts, animations: {
UIView.setAnimationRepeatCount(5)
self.view2.center.x -= 100
}, completion: { _ in
self.view2.center.x = xorig
})
這樣就會(huì)只循環(huán)五次
還有一些options指定如果另一個(gè)動(dòng)畫已經(jīng)作用在這個(gè)view上時(shí),該怎么辦。
-
.beginFromCurrentState從上次動(dòng)畫的當(dāng)前狀態(tài)繼續(xù)這次的動(dòng)畫,立即執(zhí)行上次動(dòng)畫的完成b閉包。會(huì)使用 presentation layer 決定從哪里開始。如果可能的化,會(huì)混合兩次的動(dòng)畫。 -
.overrideInheritedDuration不繼承別的動(dòng)畫的持續(xù)時(shí)間(默認(rèn)是繼承) -
.overrideInheritedCurve不繼承別的動(dòng)畫的曲線(默認(rèn)是繼承)
iOS 8之后.beginFromCurrentState 就很少用到了,在 iOS 7 時(shí)候下面的動(dòng)畫是會(huì)跳一下,現(xiàn)在都很平滑了
UIView.animate(withDuration: 0.5) {
self.view3.center.x -= 100
}
UIView.animate(withDuration: 0.5) {
self.view3.center.y += 100
}

取消view的動(dòng)畫
一旦一個(gè)動(dòng)畫開始執(zhí)行,在執(zhí)行的過程中我們?cè)趺慈∠??下面有一個(gè)執(zhí)行時(shí)間很長的動(dòng)畫。
self.pOrig = self.view4.center
self.pFinal = self.view4.center
self.pFinal.x -= 100
UIView.animate(withDuration: 4) {
self.view4.center = self.pFinal
}
這個(gè)動(dòng)畫執(zhí)行很漫長,我中途想取消怎么辦 ?
- 調(diào)用 layer 層的
removeAllAnimations
self.view4.layer.removeAllAnimations()

[圖片上傳中...(pic_02.gif-92d0a1-1511337092782-0)]
這種方式會(huì)跳動(dòng)一下,很不好。
- 我們可以記錄用一個(gè)0.1秒的動(dòng)畫到終點(diǎn),先拿到當(dāng)前的位置。
self.view4.layer.position = self.view4.layer.presentation()!.position
self.view4.layer.removeAllAnimations()
UIView.animate(withDuration: 0.1) {
self.view4.center = self.pFinal
}

這樣看起來 smooth 多了。
transform
view的 transform 非常簡單,也比較常用,就旋轉(zhuǎn)平移縮放,可以疊加在一起使用。
UIView.animate(withDuration: 1.2) {
self.view5.transform = CGAffineTransform.identity
.translatedBy(x: -100, y: 0)
.rotated(by:CGFloat(Double.pi/4))
.scaledBy(x: 0.5, y: 0.5)
}

如果需要回到原來的位置 用 self.view5.transform = CGAffineTransform.identity 即可
自定義 animation property
我們可以在自己自定義的view上自定義一個(gè)可以動(dòng)畫的屬性。例如加一個(gè)swing 屬性,設(shè)置為true 則 center.x 加 100 , false 則 center.x 減 100
class MyView: UIView {
var swing: Bool = true {
didSet{
var p = self.center
p.x = self.swing ? p.x + 100 : p.x - 100
UIView.animate(withDuration: 0) {
self.center = p
}
}
}
}
然后在動(dòng)畫的時(shí)候只需要使用swing屬性就可以了
UIView.animate(withDuration: 0.4) {
self.view1.swing = !self.view1.swing
}

Spring 彈性動(dòng)畫
彈性動(dòng)畫一般有一個(gè)很快的初速度,在結(jié)束的時(shí)候也會(huì)有一個(gè)擺動(dòng)。類似彈簧的效果。
UIView.animate(withDuration: 1 , delay: 0 , usingSpringWithDamping: 0.3 , initialSpringVelocity: 8 , options: [] , animations: {
self.view2.center.x -= 100
}, completion: nil)

usingSpringWithDamping 小于1 ,動(dòng)畫在最終位置都會(huì)有一個(gè)搖擺。值越小搖晃的越緩和。initialSpringVelocity 表示一個(gè)初始速度, 動(dòng)畫執(zhí)行快慢由他和duration共同決定。這個(gè)需要根絕實(shí)際情況多多調(diào)試。
Keyframe 關(guān)鍵幀動(dòng)畫
什么意思呢?就是我們可以把動(dòng)畫分成一個(gè)一個(gè)小的階段,然后在將這些結(jié)合在一起。使用 UIView.animateKeyframes(withDuration:,delay:,options:,animations:,completion:) 然后在 animations 的閉包中調(diào)用 UIView.addKeyframe(withRelativeStartTime: , relativeDuration: , animations:) 添加關(guān)鍵幀 。可以多次調(diào)用指定多個(gè)點(diǎn)。 startTime 是從0 - 1 的相對(duì)時(shí)間 ,相對(duì)于整體動(dòng)畫的時(shí)間??磦€(gè)例子 。
var p = self.view3.center
let dur = 0.25
var start = 0.0
let dx: CGFloat = -100
let dy: CGFloat = 50
var dir: CGFloat = 1
UIView.animateKeyframes(withDuration: 4, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: start , relativeDuration: dur , animations: {
p.x += dx*dir
p.y += dy
self.view3.center = p
})
start += dur
dir *= -1
UIView.addKeyframe(withRelativeStartTime: start , relativeDuration: dur , animations: {
p.x += dx*dir
p.y += dy
self.view3.center = p
})
start += dur
dir *= -1
UIView.addKeyframe(withRelativeStartTime: start , relativeDuration: dur , animations: {
p.x += dx*dir
p.y += dy
self.view3.center = p
})
start += dur
dir *= -1
UIView.addKeyframe(withRelativeStartTime: start , relativeDuration: dur , animations: {
p.x += dx*dir
p.y += dy
self.view3.center = p
})
}, completion: nil)

上面的示例,一共有四個(gè)關(guān)鍵幀。每個(gè)relativeDuration 0.25, 表示占總時(shí)長的1/4。四段等時(shí)長的幀動(dòng)畫。我們上面并沒有指定options,這里使用的是UIViewKeyframeAnimationOptions,默認(rèn)是 .calculationModeLinear . 當(dāng)然我們也可以指定其他的option,這個(gè)作用可以自己去嘗試。我們的關(guān)鍵幀動(dòng)畫的必報(bào)里也是可以指定多個(gè)屬性、或者多個(gè)view的動(dòng)畫。
Transitions 過渡
過度動(dòng)畫強(qiáng)調(diào)的是view改變內(nèi)容。一般有兩個(gè)方法
UIView.transition(with:, duration:, options:, animations:, completion:)UIView.transition(from: , to:, duration:, options:, completion:)
過渡動(dòng)畫的類型是一個(gè)options (UIViewAnimationOptions)
-
.transitionFlipFromLeft,.transitionFlipFromRight -
.transitionFlipFromTop,.transitionFlipFromBottom -
.transitionCurlUp,.transitionCurlDown .transitionCrossDissolve
來看一個(gè)例子,我們來修改下 UIImageView 的內(nèi)容
UIView.transition(with: self.imageView , duration: 0.6 , options: .transitionFlipFromLeft , animations: {
if self.imageView.image == #imageLiteral(resourceName: "rabbit") {
self.imageView.image = #imageLiteral(resourceName: "elephant")
}else{
self.imageView.image = #imageLiteral(resourceName: "rabbit")
}
}, completion: nil)
從swift 3 開始 圖片對(duì)象只要在資源里能找到都可以直接輸出來,這里copy過來顯示的是#imageLiteral(resourceName: "rabbit")
實(shí)際是這樣的

運(yùn)行效果:

我們也可以對(duì)自定義view的draw rect 最transition。
class MyView1: UIView {
var reverse = false
override func draw(_ rect: CGRect) {
let f = self.bounds.insetBy(dx: 10, dy: 10)
let context = UIGraphicsGetCurrentContext()
if self.reverse {
context?.strokeEllipse(in: f)
}else{
context?.stroke(f)
}
}
}
動(dòng)畫部分只需要重新繪制即可。
self.view4.reverse = !self.view4.reverse
UIView.transition(with: self.view4 , duration: 0.6 , options: .transitionFlipFromLeft , animations: {
self.view4.setNeedsDisplay()
}, completion: nil)

默認(rèn)情況下,一個(gè)視圖的子視圖在transition動(dòng)畫期間改變layout,這個(gè)改變是不會(huì)有動(dòng)畫的,將在transition結(jié)束的時(shí)候直接改變,如果想要做動(dòng)畫改變,需要加上.allowAnimatedContent option
UIView.transition(from: , to:, duration:, options:, completion:) 這個(gè)方法需要兩個(gè)view ,第一個(gè)會(huì)被第二個(gè)替換掉。整個(gè)transition動(dòng)畫會(huì)在他們的superview進(jìn)行動(dòng)畫。有兩種可能的情況。
刪除一個(gè)subview , 添加另一個(gè)
如果.showHideTransitionViews不包含在options中 ,那么第二個(gè)view在我們開始動(dòng)畫時(shí),并不在視圖層級(jí)。transition動(dòng)畫 remove將第一個(gè)view從superview上remove掉,將第二個(gè)view添加到相同的superview上。隱藏一個(gè)subview , 顯示另一個(gè)
如果.showHideTransitionViews包含在options中 , 那么兩個(gè)subview一開始都在視圖層級(jí)中。第一個(gè)view的isHidden是false , 第二個(gè)為true 。 transition動(dòng)畫會(huì)對(duì)調(diào)兩個(gè)view的isHidden屬性。
let lab2 = UILabel(frame: self.label1.frame)
lab2.text = self.label1.text == "Hello" ? "World" : "Hello"
lab2.textColor = UIColor.white
lab2.sizeToFit()
UIView.transition(from: self.label1 , to: lab2 , duration: 0.8 , options: .transitionFlipFromLeft , completion: { _ in
self.label1 = lab2
})

ImageView 和 Image 動(dòng)畫
在 UIImageView 上執(zhí)行動(dòng)畫非常簡單,只需要提供animationImages 屬性。一個(gè)UIImage 數(shù)組。這個(gè)數(shù)組代碼一個(gè)一個(gè)的幀,當(dāng)我們調(diào)用 startAnimating 方法的時(shí)候,這個(gè)數(shù)組的圖片就會(huì)輪流播放。animationDuration 決定了播放的速度。animationRepeatCount指定重復(fù)次數(shù) (默認(rèn)是0 , 代表無限重復(fù)),或者調(diào)用stopAnimating 方法停止動(dòng)畫。
例子 :
let rabbit = #imageLiteral(resourceName: "rabbit")
UIGraphicsBeginImageContextWithOptions(rabbit.size , false, 0)
let empty = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
let arr = [rabbit,empty,rabbit,empty,rabbit]
imageView.animationImages = arr
imageView.animationDuration = 2
imageView.animationRepeatCount = 3
imageView.startAnimating()

UIImage 有一些類方法為 UIImageView 構(gòu)造 可以動(dòng)畫的image :
-
UIImage.animatedImage(with:, duration:)
直接指定了image數(shù)組和duration。 -
UIImage.animatedImageNamed(, duration: )
提供一個(gè)單個(gè)的image name , 系統(tǒng)會(huì)自動(dòng)在后面加 "0" (如果失敗則"1") 。使這個(gè)image成為第一個(gè)image。最后一位數(shù)字累加。(知道沒有圖片或者到達(dá)”1024“) -
UIImage.animatedResizableImageNamed(, capInsets: , duration: )
跟上面的方式差不多,但是同時(shí)對(duì)每個(gè)image做了拉伸或者平鋪。 圖像本身也有resizableImage(withCapInsets: , resizingMode: )方法可以縮放(指定某個(gè)區(qū)域的拉伸或者平鋪)
let im = UIImage.animatedImageNamed("voice", duration: 2)
imageView2.image = im
其中voice1-3 已經(jīng)命名好,放在Assets.xcassets

我們?cè)僭谝粋€(gè) button上畫一個(gè)圓從大到小的動(dòng)畫
var arr = [UIImage]()
let w : CGFloat = 18
for i in 0 ..< 6 {
UIGraphicsBeginImageContextWithOptions(CGSize(width: w, height: w), false, 0)
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(UIColor.red.cgColor)
let ii = CGFloat(i)
let rect = CGRect(x: ii, y:ii, width: w-ii*2, height: w-ii*2)
context.addEllipse(in: rect)
context.fillPath()
let im = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
arr.append(im)
}
let im = UIImage.animatedImage(with: arr, duration: 0.5)
self.button.setImage(im, for: .normal)
