iOS好文共享

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

2019-03-20  本文已影响0人  SPIREJ

《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动画》读书笔记·内容层动画

上一篇下一篇

猜你喜欢

热点阅读