《iOS动画》读书笔记·显示层动画
《iOS动画》读书笔记·前序
《iOS动画》读书笔记·显示层动画
《iOS动画》读书笔记·内容层动画
《iOS动画》读书笔记·转场动画
UIView常用动画合集图解
UIView常用动画合集.jpgUIView显示层动画效果的实质还是通过修改UIView的各种属性来实现的。
UIView动画效果经常涉及的属性
frame
bounds
center
alpha
backgroundColor
transform
这里说一下:transform
UIView
有一个特别重要的属性transform
,该属性继承自CGAffineTransform
,“CG”实际上是CoreGraphics
框架的缩写。可见transform
属性是核心绘图框架与UIView
之间的桥梁。transform
最常用的三种动画分别是缩放、旋转、位移
。
动画设置:闭包形式
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
}
动画设置:方法形式
UIView.beginAnimations(nil, context: nil)//动画开始
UIView.setAnimationDuration(0.5)//动画周期设置
UIView.setAnimationCurve(UIView.AnimationCurve.easeInOut)//动画属性
UIView.setAnimationDelay(1)//动画延迟执行时间,比如动画启动之后,实际展示效果要等1s之后才显示出来
UIView.setAnimationsEnabled(true)//动画是否能用
UIView.setAnimationRepeatAutoreverses(true)//动画是否有重复返回效果
UIView.setAnimationRepeatCount(5)//动画重复次数
// 动画具体要做的处理
...
UIView.commitAnimations()//动画提交
动画属性设置加速、减速效果
public enum AnimationCurve : Int {
case easeInOut //动画开始和结束时呈现减速效果
case easeIn //动画开始时呈现减速效果
case easeOut //动画结束时呈现减速效果
case linear //动画整个周期内速度一致、匀速运动
}
动画回调方法
delegate 回调方法
optional public func animationDidStart(_ anim: CAAnimation)
optional public func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
setAnimationDidStop自定义回调方法
UIView.setAnimationDidStop(#selector(self.animationEnd))
@objc func animationEnd() {
//...
}
初级动画合集 - 实战
这部分内容比较简单,这里主要做介绍,最后提供一个简单的组合动画展示一下(效果如下):
组合动画示例.gif开始前,定义全局常量:
let kScreenW = UIScreen.main.bounds.size.width //屏幕宽
let kScreenH = UIScreen.main.bounds.size.height//屏幕高
位置动画
场景:界面上有一个按钮,页面加载完成时按钮从底部弹出。首先viewDidLoad()
里面初始化一个button
,然后在viewDidAppear()
方法里使用闭包形式改变button
的frame
和center
两种方式实现这个效果。
viewDidAppear() 表明所有的视图已经可见
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)
}
几何形状动画
设置属性bounds
或transform(scaleX:)
位置 + 形状动画
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)
}
}
淡入淡出动画
设置alpha
透明度实现这一效果:初始时透明度设置为0,即隐藏状态,在动画执行效果中将透明度设置为1.0
颜色渐变动画
设置backgroundColor
实现这一效果:在动画执行效果中改变颜色值
缩放动画
func transformScale() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(1.0)
self.loginBT?.transform = CGAffineTransform(scaleX: 0.7, y: 1.2)
UIView.commitAnimations()
}
旋转动画
swift中pi
表示一个圆360°,.pi/4
就是旋转90°
public static var pi: CGFloat { get }
func transformAngle() {
UIView.animate(withDuration: 1) {
self.loginBT?.transform = CGAffineTransform(rotationAngle: .pi/4)
}
}
位移动画
设置当前view相对于X,Y
轴偏移了多少。如 CGAffineTransform(translationX: 0, y: -200)
,X轴方向没有偏移,Y轴方向向上偏移了200
func transformXY() {
UIView.animate(withDuration: 1) {
self.loginBT?.transform = CGAffineTransform(translationX: 0, y: -200)
}
}
组合动画
想让这个view飞到屏幕外面去,在飞行过程中旋转它,改变它的大小和透明度
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
}
}
关键帧动画
UIView
初级动画中都是通过修改当前UI控件的各种属性来实现想要的动画效果,而关键帧动画只需要设置动画的几个关键的显示帧。
关键方法
@available(iOS 7.0, *)
open class func animateKeyframes(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.KeyframeAnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)
duration // 动画执行周期
delay // 动画延迟执行时间
options // 动画执行效果
animations // 关键帧添加处
completion // 动画完成回调
实现实例:小飞机降落
1、添加一张飞机场背景图,添加一张小飞机图
2、为小飞机添加关键帧动画
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
}
}
常见的效果有下面几类:
// 运算模式:连续
public static var calculationModeLinear: UIView.KeyframeAnimationOptions { get } // default
// 运算模式:离散
public static var calculationModeDiscrete: UIView.KeyframeAnimationOptions { get }
// 运算模式:均匀执行
public static var calculationModePaced: UIView.KeyframeAnimationOptions { get }
// 运算模式:平滑
public static var calculationModeCubic: UIView.KeyframeAnimationOptions { get }
// 运算模式:平滑均匀
public static var calculationModeCubicPaced: UIView.KeyframeAnimationOptions { get }
添加关键帧方法:addKeyframe()
@available(iOS 7.0, *)
open class func addKeyframe(withRelativeStartTime frameStartTime: Double, relativeDuration frameDuration: Double, animations: @escaping () -> Void)
这个方法描述了在什么位置添加一个持续时间多长的关键帧。改方法的几个参数如下:
withRelativeStartTime // 关键帧起始时间
relativeDuration // 关键帧相对持续时间
animations // 关键帧具体实现内容
示例:
关键帧动画示例.gif
逐帧动画
逐帧动画实现的动画效果就是将图片一帧帧的逐帧渲染。这里准备了飞机飞行过程中67张静态图片。
基于NSTimer的逐帧动画效果
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的逐帧动画效果
CADisplayLink 和 NSTimer有什么区别呢?
iOS设备
的屏幕刷新频率默认是60Hz
,而CADisplayLink
可以保持和屏幕刷新率相同的频率将内容渲染到屏幕上,因此它的精度非常高。
CADisplayLink
使用的时候需要注册到 runloop
中,每当刷帧频率到达的时候runloop
就会向CADisplayLink
指定的target
发送一次指定的selector消息
,相应的selector中的方法会被调用一次。
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)
}
示例
104.gif
基于draw方法的逐帧动画效果
当创建一个新的view时,其自动生成一个draw()
方法,且此方法可以被重写
,一旦draw()
被调用,Cocoa就会为我们创建一个图形上下文
,在图形上下文的所有操作最终都会反映在当前的UIView
界面上。按照这个思路,如果定期调用draw()
方法绘制新的内容,就可以实现逐帧动画效果。
总结
draw()
触发的机制:
(1)使用addSubview
会触发layoutSubviews
(2)使用view
的frame
属性会触发layoutSubviews(frame更新)
(3)直接调用layoutSubviews
方法会触发layoutSubviews
新建一个UIview类:
class BlackHoleView: UIView {
var blackHoleRadius:Float = 0
func blackHoleIncrease(_ radius:Float) {
blackHoleRadius = radius
// 调用setNeedsDisplay()方法实现draw()方法的调用
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, // 当前绘制圆形中心点的x,y坐标
radius: CGFloat, // 当前绘制圆形半径
startAngle: CGFloat,// 当前绘制圆形开始角度
endAngle: CGFloat, // 结束角度
clockwise: Bool) // true顺时针绘制 false逆时针绘制
*/
ctx.fillPath()
}
}
在viewController里面的实现代码:
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
}
}
示例:
重写draw()方法的逐帧动画示例.gif
GIF动画效果
GIF 在iOS中的使用场景有以下三个方面
(1)GIF图片分解为单帧图片
(2)一系列单帧图片合成GIF图片
(3)iOS系统上展示GIF动画效果
(1)GIF图片分解为单帧图片
#关键过程 GIF - NSData - ImageIO - UIImage - Jpg/Png
整个过程划分为5个模块、4个过程,分别如下
(1)本地读取GIF
图片,将其转换为NSData
数据类型
(2)将NSData
作为ImageIO
模块的输入
(3)获取ImageIO
的输出数据:UIImage
(4)将获取到的UIImage
数据存储为JPG或PNG格式保存到本地
整个GIF图片分解的过程中,ImageIO
是处理过程的核心部分。它负责对GIF文件格式进行分解,并将解析之后的数据转换为一帧帧图片输出。我们不去深究GIF分解合成算法的具体实现,掌握如何使用它就OK。
func fenJieGIF() {
// (1)本地读取GIF图片,将其转换为NSData数据类型
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类型数据
let imageref = CGImageSourceCreateImageAtIndex(gifDataSource, i, nil)
// UIImage 类方法,实例化UIImage实例对象
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
图片的每帧图片可根据打印出的路径去查看。
(2)一系列单帧图片合成GIF图片
GIF合成分三部分:
(1)加载待处理的序列原始数据源
(2)在Document
目录下构建GIF文件
(3)设置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目录下创建gif文件
var docs = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = docs[0] as String
let gifPath = documentsDirectory + "/plane.gif"
print(gifPath)
// 文件路径由string类型转换为URL类型
let url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, gifPath as CFString, CFURLPathStyle.cfurlposixPathStyle, false)
let destion = CGImageDestinationCreateWithURL(url!, kUTTypeGIF, images.count, nil)
/*
CGImageDestinationCreateWithURL()
方法的作用是创建一个图片的目标对象,这里方便理解可以把图片目标对象比喻为一个集合体,
集合体中描述了当前图片目标对象的一系列参数,如图片的URL地址、图片类型、图片帧数、配置参数等
*/
// Part3:设置gif图片属性,利用67张png图片构建gif
let cgimagePropertiesDic = [kCGImagePropertyGIFDelayTime as String:0.1]//设置每帧之间播放时间
let cgimagePropertiesDestDic = [kCGImagePropertyGIFDictionary as String:cgimagePropertiesDic];
for cgimage in images{
CGImageDestinationAddImage(destion!, (cgimage as AnyObject).cgImage!!,cgimagePropertiesDestDic as CFDictionary?);
}// 依次为gif图像对象添加每一帧元素
let gifPropertiesDic:NSMutableDictionary = NSMutableDictionary()
gifPropertiesDic.setValue(kCGImagePropertyColorModelRGB, forKey: kCGImagePropertyColorModel as String)// 设置图像的彩色空间格式
gifPropertiesDic.setValue(16, forKey: kCGImagePropertyDepth as String)// 设置图像的颜色深度
gifPropertiesDic.setValue(1, forKey: kCGImagePropertyGIFLoopCount as String)// 设置Gif执行次数
let gifDictionaryDestDic = [kCGImagePropertyGIFDictionary as String:gifPropertiesDic]
CGImageDestinationSetProperties(destion!,gifDictionaryDestDic as CFDictionary?);//为gif图像设置属性
CGImageDestinationFinalize(destion!);
}
CGImageDestinationCreateWithURL()
方法的作用是创建一个图片的目标对象,这里方便理解可以把图片目标对象比喻为一个集合体,
集合体中描述了当前图片目标对象的一系列参数,如图片的URL地址、图片类型、图片帧数、配置参数等。
本示例中,将plane.gif的本地文件路径作为参数1传递给这个图片目标对象,参数2描述了图片类型为GIF图片(需要引入框架 import MobileCoreServices
),参数3表明当前GIF图片构成的帧数,参数4暂时给空值。
最终合成的.gif
图片可根据打印出的路径去查看。
(3)iOS系统上展示GIF动画效果
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()
}
如果您有兴趣的话
上一节《iOS动画》读书笔记·前序
下一节《iOS动画》读书笔记·内容层动画