《iOS动画》读书笔记·显示层动画

《iOS动画》读书笔记·前序
《iOS动画》读书笔记·显示层动画
《iOS动画》读书笔记·内容层动画
《iOS动画》读书笔记·转场动画

UIView常用动画合集图解

UIView常用动画合集.jpg

UIView显示层动画效果的实质还是通过修改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()方法里使用闭包形式改变buttonframecenter两种方式实现这个效果。

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)
}

几何形状动画

设置属性boundstransform(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)使用viewframe属性会触发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暂时给空值。

CGImageDestination.png

最终合成的.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动画》读书笔记·内容层动画

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容

  • 1 CALayer IOS SDK详解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi阅读 5,137评论 3 23
  • 在iOS实际开发中常用的动画无非是以下四种:UIView动画,核心动画,帧动画,自定义转场动画。 1.UIView...
    请叫我周小帅阅读 3,083评论 1 23
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,105评论 5 13
  • 书写的很好,翻译的也棒!感谢译者,感谢感谢! iOS-Core-Animation-Advanced-Techni...
    钱嘘嘘阅读 2,293评论 0 6
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32