美文网首页iOS好文共享
《iOS动画》读书笔记·显示层动画

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

作者: SPIREJ | 来源:发表于2019-03-20 15:28 被阅读0次

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

    相关文章

      网友评论

        本文标题:《iOS动画》读书笔记·显示层动画

        本文链接:https://www.haomeiwen.com/subject/zqttvqtx.html