《iOS動(dòng)畫》讀書筆記·前序
《iOS動(dòng)畫》讀書筆記·顯示層動(dòng)畫
《iOS動(dòng)畫》讀書筆記·內(nèi)容層動(dòng)畫
《iOS動(dòng)畫》讀書筆記·轉(zhuǎn)場動(dòng)畫
UIView常用動(dòng)畫合集圖解

UIView顯示層動(dòng)畫效果的實(shí)質(zhì)還是通過修改UIView的各種屬性來實(shí)現(xiàn)的。
UIView動(dòng)畫效果經(jīng)常涉及的屬性
frame bounds center
alpha backgroundColor transform
這里說一下:transform
UIView有一個(gè)特別重要的屬性transform,該屬性繼承自CGAffineTransform,“CG”實(shí)際上是CoreGraphics框架的縮寫??梢?code>transform屬性是核心繪圖框架與UIView之間的橋梁。transform最常用的三種動(dòng)畫分別是縮放、旋轉(zhuǎn)、位移。
動(dòng)畫設(shè)置:閉包形式
UIView.animate(withDuration: 0.5) {
self.loginBT?.center = CGPoint(x: kScreenW/2.0, y: kScreenH/2.0)
}
UIView.animate(withDuration: 0.5, animations: {
}) { (true) in
}
UIView.animate(withDuration: 0.5, delay: 0.2, options: UIView.AnimationOptions.curveEaseOut, animations: {
}) { (true) in
}
動(dòng)畫設(shè)置:方法形式
UIView.beginAnimations(nil, context: nil)//動(dòng)畫開始
UIView.setAnimationDuration(0.5)//動(dòng)畫周期設(shè)置
UIView.setAnimationCurve(UIView.AnimationCurve.easeInOut)//動(dòng)畫屬性
UIView.setAnimationDelay(1)//動(dòng)畫延遲執(zhí)行時(shí)間,比如動(dòng)畫啟動(dòng)之后,實(shí)際展示效果要等1s之后才顯示出來
UIView.setAnimationsEnabled(true)//動(dòng)畫是否能用
UIView.setAnimationRepeatAutoreverses(true)//動(dòng)畫是否有重復(fù)返回效果
UIView.setAnimationRepeatCount(5)//動(dòng)畫重復(fù)次數(shù)
// 動(dòng)畫具體要做的處理
...
UIView.commitAnimations()//動(dòng)畫提交
動(dòng)畫屬性設(shè)置加速、減速效果
public enum AnimationCurve : Int {
case easeInOut //動(dòng)畫開始和結(jié)束時(shí)呈現(xiàn)減速效果
case easeIn //動(dòng)畫開始時(shí)呈現(xiàn)減速效果
case easeOut //動(dòng)畫結(jié)束時(shí)呈現(xiàn)減速效果
case linear //動(dòng)畫整個(gè)周期內(nèi)速度一致、勻速運(yùn)動(dòng)
}
動(dòng)畫回調(diào)方法
delegate 回調(diào)方法
optional public func animationDidStart(_ anim: CAAnimation)
optional public func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
setAnimationDidStop自定義回調(diào)方法
UIView.setAnimationDidStop(#selector(self.animationEnd))
@objc func animationEnd() {
//...
}
初級(jí)動(dòng)畫合集 - 實(shí)戰(zhàn)
這部分內(nèi)容比較簡單,這里主要做介紹,最后提供一個(gè)簡單的組合動(dòng)畫展示一下(效果如下):

開始前,定義全局常量:
let kScreenW = UIScreen.main.bounds.size.width //屏幕寬
let kScreenH = UIScreen.main.bounds.size.height//屏幕高
位置動(dòng)畫
場景:界面上有一個(gè)按鈕,頁面加載完成時(shí)按鈕從底部彈出。首先viewDidLoad()里面初始化一個(gè)button,然后在viewDidAppear()方法里使用閉包形式改變button的frame和center兩種方式實(shí)現(xiàn)這個(gè)效果。
viewDidAppear() 表明所有的視圖已經(jīng)可見
var loginBT:UIButton?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.backgroundColor = UIColor.white
loginBT = UIButton.init(frame: CGRect(x: 20, y: kScreenH, width: self.view.bounds.width-40
, height: 50))
loginBT?.backgroundColor = UIColor(red: 50/255.0, green: 185/255.0, blue: 170/255.0, alpha: 1.0)
loginBT?.setTitle("登錄", for: .normal)
self.view.addSubview(loginBT!)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//1. 改變frame的形式
UIView.animate(withDuration: 0.5) {
self.loginBT?.frame = CGRect(x: self.loginBT!.frame.origin.x, y: kScreenH - 200, width: self.loginBT!.frame.size.width, height: self.loginBT!.frame.size.height)
}
//2. 改變center的形式
UIView.animate(withDuration: 0.5) {
self.loginBT?.center = CGPoint(x: kScreenW/2.0, y: kScreenH/2.0)
}
幾何形狀動(dòng)畫
設(shè)置屬性bounds 或transform(scaleX:)
位置 + 形狀動(dòng)畫
func animateFrame() {
UIView.animate(withDuration: 0.5) {
self.loginBT?.frame = CGRect(x: 50, y: 400, width: self.loginBT!.frame.size.width*0.7, height: self.loginBT!.frame.size.height*1.2)
}
}
淡入淡出動(dòng)畫
設(shè)置alpha透明度實(shí)現(xiàn)這一效果:初始時(shí)透明度設(shè)置為0,即隱藏狀態(tài),在動(dòng)畫執(zhí)行效果中將透明度設(shè)置為1.0
顏色漸變動(dòng)畫
設(shè)置backgroundColor實(shí)現(xiàn)這一效果:在動(dòng)畫執(zhí)行效果中改變顏色值
縮放動(dòng)畫
func transformScale() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(1.0)
self.loginBT?.transform = CGAffineTransform(scaleX: 0.7, y: 1.2)
UIView.commitAnimations()
}
旋轉(zhuǎn)動(dòng)畫
swift中pi表示一個(gè)圓360°,.pi/4就是旋轉(zhuǎn)90°
public static var pi: CGFloat { get }
func transformAngle() {
UIView.animate(withDuration: 1) {
self.loginBT?.transform = CGAffineTransform(rotationAngle: .pi/4)
}
}
位移動(dòng)畫
設(shè)置當(dāng)前view相對(duì)于X,Y軸偏移了多少。如 CGAffineTransform(translationX: 0, y: -200),X軸方向沒有偏移,Y軸方向向上偏移了200
func transformXY() {
UIView.animate(withDuration: 1) {
self.loginBT?.transform = CGAffineTransform(translationX: 0, y: -200)
}
}
組合動(dòng)畫
想讓這個(gè)view飛到屏幕外面去,在飛行過程中旋轉(zhuǎn)它,改變它的大小和透明度
func groupAnimation() {
UIView.animate(withDuration: 1) {
self.loginBT?.frame = CGRect(x: kScreenW, y: 0, width: self.loginBT!.frame.size.width*0.1, height: self.loginBT!.frame.size.height*0.1)
self.loginBT?.transform = CGAffineTransform(rotationAngle: .pi)
self.loginBT?.alpha = 0
}
}
關(guān)鍵幀動(dòng)畫
UIView初級(jí)動(dòng)畫中都是通過修改當(dāng)前UI控件的各種屬性來實(shí)現(xiàn)想要的動(dòng)畫效果,而關(guān)鍵幀動(dòng)畫只需要設(shè)置動(dòng)畫的幾個(gè)關(guān)鍵的顯示幀。
關(guān)鍵方法
@available(iOS 7.0, *)
open class func animateKeyframes(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.KeyframeAnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)
duration // 動(dòng)畫執(zhí)行周期
delay // 動(dòng)畫延遲執(zhí)行時(shí)間
options // 動(dòng)畫執(zhí)行效果
animations // 關(guān)鍵幀添加處
completion // 動(dòng)畫完成回調(diào)
實(shí)現(xiàn)實(shí)例:小飛機(jī)降落
1、添加一張飛機(jī)場背景圖,添加一張小飛機(jī)圖
2、為小飛機(jī)添加關(guān)鍵幀動(dòng)畫
func addKeyframes() {
UIView.animateKeyframes(withDuration: 2, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1/2, animations: {
self.imageViewPlane.frame = CGRect(x: kScreenW-50, y: 300, width: 30, height: 30)
})
UIView.addKeyframe(withRelativeStartTime: 1/2, relativeDuration: 1/2, animations: {
self.imageViewPlane.frame = CGRect(x: kScreenW-100, y: 300, width: 100, height: 100)
})
}) { (finish) in
}
}
常見的效果有下面幾類:
// 運(yùn)算模式:連續(xù)
public static var calculationModeLinear: UIView.KeyframeAnimationOptions { get } // default
// 運(yùn)算模式:離散
public static var calculationModeDiscrete: UIView.KeyframeAnimationOptions { get }
// 運(yùn)算模式:均勻執(zhí)行
public static var calculationModePaced: UIView.KeyframeAnimationOptions { get }
// 運(yùn)算模式:平滑
public static var calculationModeCubic: UIView.KeyframeAnimationOptions { get }
// 運(yùn)算模式:平滑均勻
public static var calculationModeCubicPaced: UIView.KeyframeAnimationOptions { get }
添加關(guān)鍵幀方法:addKeyframe()
@available(iOS 7.0, *)
open class func addKeyframe(withRelativeStartTime frameStartTime: Double, relativeDuration frameDuration: Double, animations: @escaping () -> Void)
這個(gè)方法描述了在什么位置添加一個(gè)持續(xù)時(shí)間多長的關(guān)鍵幀。改方法的幾個(gè)參數(shù)如下:
withRelativeStartTime // 關(guān)鍵幀起始時(shí)間
relativeDuration // 關(guān)鍵幀相對(duì)持續(xù)時(shí)間
animations // 關(guān)鍵幀具體實(shí)現(xiàn)內(nèi)容
示例:

逐幀動(dòng)畫
逐幀動(dòng)畫實(shí)現(xiàn)的動(dòng)畫效果就是將圖片一幀幀的逐幀渲染。這里準(zhǔn)備了飛機(jī)飛行過程中67張靜態(tài)圖片。
基于NSTimer的逐幀動(dòng)畫效果
class ZhuZhenVC: UIViewController {
var imageView:UIImageView?
var timer:Timer?
var index = 0
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(frame: UIScreen.main.bounds)
imageView?.contentMode = UIView.ContentMode.scaleAspectFit
index = 0
self.view.addSubview(self.imageView!)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.refushImage), userInfo: nil, repeats: true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
@objc func refushImage() {
imageView?.image = UIImage(named: "\(index).png")
index += 1
if index == 67 {
timer?.invalidate()
index = 0
imageView?.image = UIImage(named: "\(index).png")
}
}
}
基于CADisplayLink的逐幀動(dòng)畫效果
CADisplayLink 和 NSTimer有什么區(qū)別呢?
iOS設(shè)備的屏幕刷新頻率默認(rèn)是60Hz,而CADisplayLink可以保持和屏幕刷新率相同的頻率將內(nèi)容渲染到屏幕上,因此它的精度非常高。
CADisplayLink使用的時(shí)候需要注冊(cè)到 runloop中,每當(dāng)刷幀頻率到達(dá)的時(shí)候runloop就會(huì)向CADisplayLink指定的target發(fā)送一次指定的selector消息,相應(yīng)的selector中的方法會(huì)被調(diào)用一次。
var displayLink:CADisplayLink?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
displayLink = CADisplayLink(target: self, selector: #selector(refushImage))
displayLink?.preferredFramesPerSecond = 1
displayLink?.add(to: RunLoop.current, forMode: .default)
}
示例

基于draw方法的逐幀動(dòng)畫效果
當(dāng)創(chuàng)建一個(gè)新的view時(shí),其自動(dòng)生成一個(gè)draw()方法,且此方法可以被重寫,一旦draw()被調(diào)用,Cocoa就會(huì)為我們創(chuàng)建一個(gè)圖形上下文,在圖形上下文的所有操作最終都會(huì)反映在當(dāng)前的UIView界面上。按照這個(gè)思路,如果定期調(diào)用draw()方法繪制新的內(nèi)容,就可以實(shí)現(xiàn)逐幀動(dòng)畫效果。
總結(jié)
draw()觸發(fā)的機(jī)制:
(1)使用addSubview會(huì)觸發(fā)layoutSubviews
(2)使用view的frame屬性會(huì)觸發(fā)layoutSubviews(frame更新)
(3)直接調(diào)用layoutSubviews方法會(huì)觸發(fā)layoutSubviews
新建一個(gè)UIview類:
class BlackHoleView: UIView {
var blackHoleRadius:Float = 0
func blackHoleIncrease(_ radius:Float) {
blackHoleRadius = radius
// 調(diào)用setNeedsDisplay()方法實(shí)現(xiàn)draw()方法的調(diào)用
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// Drawing code
let ctx = UIGraphicsGetCurrentContext()!
ctx.addArc(center: CGPoint(x: self.center.x, y: self.center.y),
radius: CGFloat(blackHoleRadius),
startAngle: 0,
endAngle: .pi*2,
clockwise: false)
/*
public func addArc(center: CGPoint, // 當(dāng)前繪制圓形中心點(diǎn)的x,y坐標(biāo)
radius: CGFloat, // 當(dāng)前繪制圓形半徑
startAngle: CGFloat,// 當(dāng)前繪制圓形開始角度
endAngle: CGFloat, // 結(jié)束角度
clockwise: Bool) // true順時(shí)針繪制 false逆時(shí)針繪制
*/
ctx.fillPath()
}
}
在viewController里面的實(shí)現(xiàn)代碼:
var index = 0
var blackHole:BlackHoleView?
override func viewDidLoad() {
super.viewDidLoad()
blackHole = BlackHoleView()
blackHole?.frame = UIScreen.main.bounds
blackHole?.backgroundColor = UIColor.cyan
self.view.addSubview(blackHole!)
timer = Timer.scheduledTimer(timeInterval: 1.0/10, target: self, selector: #selector(self.refushImage), userInfo:nil, repeats: true)
}
@objc func refushImage() {
blackHole?.blackHoleIncrease(Float(index))
index += 1
if index == 30 {
index = 0
}
}
示例:

GIF動(dòng)畫效果
GIF 在iOS中的使用場景有以下三個(gè)方面
(1)GIF圖片分解為單幀圖片
(2)一系列單幀圖片合成GIF圖片
(3)iOS系統(tǒng)上展示GIF動(dòng)畫效果
(1)GIF圖片分解為單幀圖片
#關(guān)鍵過程 GIF - NSData - ImageIO - UIImage - Jpg/Png
整個(gè)過程劃分為5個(gè)模塊、4個(gè)過程,分別如下
(1)本地讀取GIF圖片,將其轉(zhuǎn)換為NSData數(shù)據(jù)類型
(2)將NSData作為ImageIO模塊的輸入
(3)獲取ImageIO的輸出數(shù)據(jù):UIImage
(4)將獲取到的UIImage數(shù)據(jù)存儲(chǔ)為JPG或PNG格式保存到本地
整個(gè)GIF圖片分解的過程中,ImageIO是處理過程的核心部分。它負(fù)責(zé)對(duì)GIF文件格式進(jìn)行分解,并將解析之后的數(shù)據(jù)轉(zhuǎn)換為一幀幀圖片輸出。我們不去深究GIF分解合成算法的具體實(shí)現(xiàn),掌握如何使用它就OK。
func fenJieGIF() {
// (1)本地讀取GIF圖片,將其轉(zhuǎn)換為NSData數(shù)據(jù)類型
let gifPath = Bundle.main.path(forResource: "plane", ofType: "gif")!
let gifData = try! Data(contentsOf: URL(fileURLWithPath: gifPath))
// (2)將NSData作為ImageIO模塊的輸入,遍歷所有GIF子幀
let gifDataSource:CGImageSource = CGImageSourceCreateWithData(gifData as CFData, nil)!
let gifImageCount:Int = CGImageSourceGetCount(gifDataSource)
for i in 0...gifImageCount-1 {
// CGImageSourceCreateImageAtIndex 返回GIF中某一幀圖像的CGImage類型數(shù)據(jù)
let imageref = CGImageSourceCreateImageAtIndex(gifDataSource, i, nil)
// UIImage 類方法,實(shí)例化UIImage實(shí)例對(duì)象
let image = UIImage(cgImage: imageref!, scale: UIScreen.main.scale, orientation: UIImage.Orientation.up)
let imageData:Data = image.pngData()!
var docs = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = docs[0] as String
let imagePath = documentsDirectory + "/\(i)" + ".png"
try? imageData.write(to: URL(fileURLWithPath: imagePath), options: .atomic)
print("\(imagePath)")
}
}
最終分解.gif圖片的每幀圖片可根據(jù)打印出的路徑去查看。
(2)一系列單幀圖片合成GIF圖片
GIF合成分三部分:
(1)加載待處理的序列原始數(shù)據(jù)源
(2)在Document目錄下構(gòu)建GIF文件
(3)設(shè)置GIF文件屬性,利用ImageIO編碼GIF文件
func heChengGIF() {
// Part1:讀取67張圖片
let images:NSMutableArray = NSMutableArray()
for i in 0...66 {
let imagePath = "\(i).png"
let image:UIImage = UIImage(named: imagePath)!
images.add(image)
}
// Part2:在Document目錄下創(chuàng)建gif文件
var docs = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = docs[0] as String
let gifPath = documentsDirectory + "/plane.gif"
print(gifPath)
// 文件路徑由string類型轉(zhuǎn)換為URL類型
let url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, gifPath as CFString, CFURLPathStyle.cfurlposixPathStyle, false)
let destion = CGImageDestinationCreateWithURL(url!, kUTTypeGIF, images.count, nil)
/*
CGImageDestinationCreateWithURL()
方法的作用是創(chuàng)建一個(gè)圖片的目標(biāo)對(duì)象,這里方便理解可以把圖片目標(biāo)對(duì)象比喻為一個(gè)集合體,
集合體中描述了當(dāng)前圖片目標(biāo)對(duì)象的一系列參數(shù),如圖片的URL地址、圖片類型、圖片幀數(shù)、配置參數(shù)等
*/
// Part3:設(shè)置gif圖片屬性,利用67張png圖片構(gòu)建gif
let cgimagePropertiesDic = [kCGImagePropertyGIFDelayTime as String:0.1]//設(shè)置每幀之間播放時(shí)間
let cgimagePropertiesDestDic = [kCGImagePropertyGIFDictionary as String:cgimagePropertiesDic];
for cgimage in images{
CGImageDestinationAddImage(destion!, (cgimage as AnyObject).cgImage!!,cgimagePropertiesDestDic as CFDictionary?);
}// 依次為gif圖像對(duì)象添加每一幀元素
let gifPropertiesDic:NSMutableDictionary = NSMutableDictionary()
gifPropertiesDic.setValue(kCGImagePropertyColorModelRGB, forKey: kCGImagePropertyColorModel as String)// 設(shè)置圖像的彩色空間格式
gifPropertiesDic.setValue(16, forKey: kCGImagePropertyDepth as String)// 設(shè)置圖像的顏色深度
gifPropertiesDic.setValue(1, forKey: kCGImagePropertyGIFLoopCount as String)// 設(shè)置Gif執(zhí)行次數(shù)
let gifDictionaryDestDic = [kCGImagePropertyGIFDictionary as String:gifPropertiesDic]
CGImageDestinationSetProperties(destion!,gifDictionaryDestDic as CFDictionary?);//為gif圖像設(shè)置屬性
CGImageDestinationFinalize(destion!);
}
CGImageDestinationCreateWithURL()
方法的作用是創(chuàng)建一個(gè)圖片的目標(biāo)對(duì)象,這里方便理解可以把圖片目標(biāo)對(duì)象比喻為一個(gè)集合體,
集合體中描述了當(dāng)前圖片目標(biāo)對(duì)象的一系列參數(shù),如圖片的URL地址、圖片類型、圖片幀數(shù)、配置參數(shù)等。
本示例中,將plane.gif的本地文件路徑作為參數(shù)1傳遞給這個(gè)圖片目標(biāo)對(duì)象,參數(shù)2描述了圖片類型為GIF圖片(需要引入框架 import MobileCoreServices),參數(shù)3表明當(dāng)前GIF圖片構(gòu)成的幀數(shù),參數(shù)4暫時(shí)給空值。

最終合成的.gif圖片可根據(jù)打印出的路徑去查看。
(3)iOS系統(tǒng)上展示GIF動(dòng)畫效果
iOS原生不支持直接顯示GIF圖片,故:
(1)先分解GIF圖片為單幀圖片
(2)再展示多幀圖片
func zhanShiGIF() {
/*
iOS原生不支持直接顯示GIF圖片,故:
(1)先分解GIF圖片為單幀圖片
(2)再展示多幀圖片
*/
var images:[UIImage] = []
for i in 0...66 {
let imagePath = "\(i).png"
let image:UIImage = UIImage(named: imagePath)!
images.append(image)
}
let imageView = UIImageView(frame: self.view.bounds)
imageView.contentMode = UIView.ContentMode.center
self.view.addSubview(imageView)
imageView.animationImages = images
imageView.animationDuration = 5
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
如果您有興趣的話
上一節(jié)《iOS動(dòng)畫》讀書筆記·前序
下一節(jié)《iOS動(dòng)畫》讀書筆記·內(nèi)容層動(dòng)畫