美文网首页动画相关iOS动画Ios
iOS动画指南 - 3.Layer Animations的进阶使

iOS动画指南 - 3.Layer Animations的进阶使

作者: Dariel | 来源:发表于2016-06-28 22:49 被阅读3103次

    本篇预备知识

    • 这一系列是讲iOS开发中动画的使用,所以得基本熟悉iOS开发.
    • 代码都是基于swift的,所以也得了解swift啊.
    • 这一篇是在前一篇的基础上写的,所以得知道Layer Animations的基本使用吧!

    概述

    上一篇iOS动画指南 - 2.Layer Animations的基本使用中介绍了Layer Animations的一些基本使用,这一篇我们通过几个小的例子深入了解Layer Animations的用法,所以相比上篇,这篇无论是从篇幅还是连贯性都会有点大,大家准备上车吧.

    文章大纲

    1. 可以在多个值之间变幻的CAKeyframeAnimation.
    2. 可以画出各种形状的CAShapeLayer.


      DOG VS FOX
    3. 可以给文字添加效果的CAGradientLayer.


      滑动解锁效果
    4. 有轨迹的下拉刷新.


      模拟下拉刷新
    5. 可以无限复制的CAReplicatorLayer.


      CAReplicatorLayer

    1. CAKeyframeAnimation

    开发中情况多种多样,从一个值到另一个值的fromValue和toValue属性并不能高效的满足开发需要,比如我们要将一个view一次经过三个点呢?难道分为两次去做,那太麻烦了.对的,可以用CAKeyframeAnimation去实现,CAKeyframeAnimation有个属性values是个数组完美替代了fromValue,toValue,我们可以把三个点放进values数组,解决问题.

            let flight = CAKeyframeAnimation(keyPath: "position")
            flight.duration = 2.0
            // 无限重复
            flight.repeatCount = MAXFLOAT
            
            //  注意:不能将CGPoint直接赋值给values需要转换,数组中的元素可以使结构体
            // .map { NSValue(CGPoint: $0)}可以将数组中的每一个CGPoint转化为NSValue
            flight.values = [
                CGPoint(x: 50.0, y: 100.0),
                CGPoint(x: view.frame.width-50, y: 160),
                CGPoint(x: 50.0, y: view.center.y),
                CGPoint(x: 50.0, y: 100.0)
                ].map { NSValue(CGPoint: $0)}
            
                // 等价于上面代码
    //        flight.values = [
    //            NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0)),
    //            NSValue(CGPoint: CGPoint(x: view.frame.width-50, y: 160)),
    //            NSValue(CGPoint: CGPoint(x: 50.0, y: view.center.y)),
    //            NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0)),
    //            ]
    
            flight.keyTimes = [0.0, 0.33, 0.66, 1.0]
            dogImageView.layer.addAnimation(flight, forKey: nil)
    

    或者我们可以做一下view的左右晃动,不添加在上面位移基础上,单独去实现:

            let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
            wobble.duration = 2.5
            wobble.repeatCount = MAXFLOAT
            // 会依次遍历数组中每一个值
            wobble.values = [0.0, -M_PI_4/4, 0.0, M_PI_4/4, 0.0]
            // 为values中的值设置时间,keyTimes按照百分比来的,[0,1]之间
            wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
            dogImageView.layer.addAnimation(wobble, forKey: nil)
    
    

    2. CAShapeLayer

    使用CAShapeLayer可以绘制各种图形.
    比如用来画圆:

            let circleLayer = CAShapeLayer()
            let maskLayer = CAShapeLayer()
            
            circleLayer.path = UIBezierPath(ovalInRect: dogImageView.bounds).CGPath
            circleLayer.fillColor = UIColor.clearColor().CGColor
            maskLayer.path = circleLayer.path
            // 超出maskLayer部分裁剪掉
            dogImageView.layer.mask = maskLayer
            dogImageView.layer.addSublayer(circleLayer)
    

    接下来让我们来看下:


    DOG VS FOX

    由于git图片是循环播放的,所以很难分辨动画的开始和结束,动画的开始其实是这样的:


    这是AvatarView的层级:


    • photoLayer : 是用来放置图片的.
    • circleLayer: 是用来绘制圆形的.
    • maskLayer: 是用来裁剪图片的.
    • label: 用于设置名字.

    让我们来分析下步骤:

    1. 设置两张图片的圆角
    2. 两张图片向中间移动,完成后将图片变成方角
    3. 在两张图片在中间的时候,将两张图片做一个椭圆的碰撞效果
    4. 后退,图片返回到开始的位置,完成后执行步骤1

    实现:
    我们对控件做了封装,具体看源码.
    1 . 在AvatarView中的didMoveToWindow方法中将新建好的几个属性添加进去

     override func didMoveToWindow() {
            layer.addSublayer(photoLayer)
            photoLayer.mask = maskLayer
            layer.addSublayer(circleLayer)
            addSubview(label)
            
        }
    

    2 . 重写layoutSubviews方法,设置图片的圆角

        override func layoutSubviews() {
            photoLayer.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
            circleLayer.path = UIBezierPath(ovalInRect: bounds).CGPath
            circleLayer.strokeColor = UIColor.whiteColor().CGColor
            circleLayer.lineWidth = lineWidth
            circleLayer.fillColor = UIColor.clearColor().CGColor
            maskLayer.path = circleLayer.path
            maskLayer.position = CGPoint(x: 0.0, y: 0.0)
            
            label.frame = CGRect(x: 0.0, y: bounds.size.height + 10.0, width: bounds.size.width, height: 24.0)
        }
    

    3 . 定义外部控制方法func boundsOffset: boundsOffset: morphSize用于传入偏移位置,以及图片碰撞时候需要设置的尺寸:

     func boundsOffset(boundsOffset:CGFloat, morphSize: CGSize) {
    }
    

    4 . 在boundsOffset方法中设置图片往中间位移:

            // 前进
            UIView.animateWithDuration(animationDuration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: {
                self.frame.origin.x = boundsOffset
                }, completion: {_ in
                    // 将圆角图片变成方角图片
                    self.animateToSquare()
            })
    

    5 . 当图片在中间的时候会有一个碰撞效果:

            // 碰撞效果
            let morphedFrame = (originalCenter.x > boundsOffset) ?
                CGRect(x: 0.0, y: bounds.height - morphSize.height,
                    width: morphSize.width, height: morphSize.height):
    
                CGRect(x: bounds.width - morphSize.width,
                    y: bounds.height - morphSize.height,
                    width: morphSize.width, height: morphSize.height)
            
            let morphAnimation = CABasicAnimation(keyPath: "path")
            morphAnimation.duration = animationDuration
            morphAnimation.toValue = UIBezierPath(ovalInRect: morphedFrame).CGPath
            morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            
            circleLayer.addAnimation(morphAnimation, forKey:nil)
            maskLayer.addAnimation(morphAnimation, forKey: nil)
    

    6 . 返回到初始位置:

             // 后退
            UIView.animateWithDuration(animationDuration, delay: animationDuration, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, options: [], animations: {
                self.center = originalCenter
                }, completion: {_ in
                    delay(seconds: 0.1) {
                        if !self.isSquare {
                                self.boundsOffset(boundsOffset, morphSize: morphSize)
                        }
                    }
            })    
    

    7 . 将圆角图片变成方角图片.严格意义上这不是最后一步,第四步有一个self.animateToSquare()

    func animateToSquare() {
            isSquare = true
            let squarePath = UIBezierPath(rect: bounds).CGPath
            let morph = CABasicAnimation(keyPath: "path")
            morph.duration = 0.25
            morph.fromValue = circleLayer.path
            morph.toValue = squarePath
            
            circleLayer.addAnimation(morph, forKey: nil)
            maskLayer.addAnimation(morph, forKey: nil)
            
            circleLayer.path = squarePath
            maskLayer.path = squarePath
        }
    

    大体步骤就这些,下面我们就可以在viewController.swift中使用了

    1. 创建两个AvatarView,设置好图片,大小,位置.
    2. 调用AvatarView的boundsOffset方法,设置偏移位置,以及图片碰撞时候需要的尺寸.
            let avatarSize = avatar1.frame.size
            let morphSize = CGSize(
                width: avatarSize.width * 0.85,
                height: avatarSize.height * 1.05)
            let bounceXOffset: CGFloat = view.frame.size.width/2.0 - avatar1.lineWidth*2 - avatar1.frame.width
            avatar2.boundsOffset(bounceXOffset, morphSize:morphSize)
            avatar1.boundsOffset(avatar1.frame.origin.x - bounceXOffset, morphSize:morphSize)
    

    3. CAGradientLayer

    我们几乎每天都会看到的iPhone滑动来解锁的文字效果是怎么实现的呢?
    一步一步来吧 >.<!

    1. 搞一个懒加载来设置CAGradientLayer
        lazy var gradientLayer: CAGradientLayer = {
            let gradientLayer = CAGradientLayer()
            
            // 设置开始位置和结束位置
            gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
            gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
            
            // 从左到右依次的这几种颜色,颜色是渐变的
            let colors = [
                UIColor.blackColor().CGColor,
                UIColor.whiteColor().CGColor,
                UIColor.blackColor().CGColor
            ]
            gradientLayer.colors = colors
            
            // 颜色的位置
            let locations = [0.25, 0.5, 0.75]
            gradientLayer.locations = locations
            
            return gradientLayer
        }()
    

    新建一个view,随便设置位置尺寸,将gradientLayer添加到view上面

            let gradientView = UIView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 80))
            gradientView.backgroundColor = UIColor.lightGrayColor()
            gradientLayer.frame = gradientView.bounds
            gradientView.layer.addSublayer(gradientLayer)
            view.addSubview(gradientView)
    

    然后我们就可以看到这样的效果:



    怎么样是不是有点那么回事了!我们还要让它动起来
    2 . 为gradientLayer添加动画效果
    CABasicAnimation有一个locations属性,通过修改颜色位置形成动画.

            let gradientAnimation = CABasicAnimation(keyPath: "locations")
            gradientAnimation.fromValue = [0.0, 0.0, 0.25]
            gradientAnimation.toValue = [0.75, 1.0, 1.0]
            gradientAnimation.duration = 3.0
            gradientAnimation.repeatCount = Float.infinity
            gradientLayer.addAnimation(gradientAnimation, forKey: nil)
    

    git掉帧这么严重将就着看吧!
    3 . 修改白色区域的大小
    这边我们调整白色区域将其*3倍,更好看一点.
    4 . 考虑到代码的可复用性,我们对代码做了封装,自定义了一个view:GradientLabel ,view里面有一个text属性,通过图形上下文把字符串绘制到view上面.

    
     // 设置字体属性
        lazy var textAttributes: [String: AnyObject] = {
            let style = NSMutableParagraphStyle()
            style.alignment = .Center
            
            return [
                NSFontAttributeName:UIFont(name: "HelveticaNeue-Thin", size: 28.0)!,
                NSParagraphStyleAttributeName:style
            ]
        }()
    
        @IBInspectable var text: String! {
            didSet {
                
                setNeedsDisplay()
                UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
                text.drawInRect(bounds, withAttributes: textAttributes)
                let image = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()
                
                let maskLayer = CALayer()
                maskLayer.backgroundColor = UIColor.clearColor().CGColor
                maskLayer.frame = CGRectOffset(bounds, bounds.size.width, 0)
                maskLayer.contents = image.CGImage
                
                gradientLayer.mask = maskLayer
            }
        }
    

    然后外面就可以这样使用了

     override func viewDidLoad() {
            super.viewDidLoad()
            let label = GradientLabel()
            label.center = view.center
            label.bounds = CGRect(x: 0, y: 0, width: 239, height: 44)
            label.text = "> 滑动来解锁"
            view.addSubview(label)
            view.backgroundColor = UIColor.darkGrayColor()
        }
    
    滑动解锁效果

    4. 有轨迹的下拉刷新.

    下拉刷新几乎每个APP都会用到,由于有现成的第三方框架,所以要自己动手实现的情况并不是很多,但有时候需求要求自定义,所以了解下原理吧!
    原理很简单:其实就是通过代理方法监听tableView的滚动,当完成刷新就恢复原来的样子.
    这次我们要做一个有特效的.

    1. 使用CAShapeLayer绘制一个带有虚线的圆
            let ovalShapeLayer: CAShapeLayer = CAShapeLayer()
            let airplaneLayer: CALayer = CALayer()
            // 白色的圈
            ovalShapeLayer.strokeColor = UIColor.whiteColor().CGColor
            ovalShapeLayer.fillColor = UIColor.clearColor().CGColor
            ovalShapeLayer.lineWidth = 4.0
            ovalShapeLayer.lineDashPattern = [2, 3]
            let refreshRadius = frame.size.height/2 * 0.8
            ovalShapeLayer.path = UIBezierPath(ovalInRect: CGRect(x: frame.size.width/2 - refreshRadius, y:frame.size.height/2 - refreshRadius , width: 2*refreshRadius, height: 2*refreshRadius)).CGPath
            layer.addSublayer(ovalShapeLayer)
    

    然后在开始位置添加飞机图片

            // 添加飞机图片
            let airplaneImage = UIImage(named: "airplane")
            airplaneLayer.contents = airplaneImage?.CGImage
            airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0, width: (airplaneImage?.size.width)!, height: airplaneImage!.size.height)
            airplaneLayer.position = CGPoint(x: frame.size.width/2 + frame.size.height/2 * 0.8, y: frame.size.height/2)
            layer.addSublayer(airplaneLayer)
    

    2 . 设置开始刷新,结束刷新动画

        // 开始刷新
        func beginRefreshing() {
            isRefreshing = true
            UIView.animateWithDuration(0.3, animations: {
                var newInsets = self.scrollView!.contentInset
                newInsets.top += self.frame.size.height
                self.scrollView!.contentInset = newInsets
            })
            
            let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
            strokeStartAnimation.fromValue = -0.5
            strokeStartAnimation.toValue = 1.0
            
            let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
            strokeEndAnimation.fromValue = 0.0
            strokeEndAnimation.toValue = 1.0
            
            let strokeAnimationGroup = CAAnimationGroup()
            strokeAnimationGroup.duration = 1.5
            strokeAnimationGroup.repeatDuration = 5.0
            strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation]
            ovalShapeLayer.addAnimation(strokeAnimationGroup, forKey: nil)
            
            let flightAnimation = CAKeyframeAnimation(keyPath: "position")
            flightAnimation.path = ovalShapeLayer.path
            flightAnimation.calculationMode = kCAAnimationPaced
            
            let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
            airplaneOrientationAnimation.fromValue = 0
            airplaneOrientationAnimation.toValue = 2 * M_PI
            
            
            let flightAnimationGroup = CAAnimationGroup()
            flightAnimationGroup.duration = 1.5
            flightAnimationGroup.repeatDuration = 5.0
            flightAnimationGroup.animations = [flightAnimation, airplaneOrientationAnimation]
            airplaneLayer.addAnimation(flightAnimationGroup, forKey: nil)
            
        }
        
        // 结束刷新
        func endRefreshing() {
            isRefreshing = false
            
            UIView.animateWithDuration(0.3, delay:0.0, options: .CurveEaseOut ,animations: {
                var newInsets = self.scrollView!.contentInset
                newInsets.top -= self.frame.size.height
                self.scrollView!.contentInset = newInsets
                }, completion: {_ in
            })
        }
    

    3 . 在tabelView的scrollView方法中根据偏移量设置动画的开始和结束

     func scrollViewDidScroll(scrollView: UIScrollView) {
            
            let offsetY = CGFloat( max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
            self.progress = min(max( offsetY / frame.size.height, 0.0), 1.0)
            
            if !refreshing {
                redrawFromProgress(progress)
            }
        }
        func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
            if !refreshing && self.progress >= 1.0 {
                delegate?.refreshViewDidRefresh(self)
                beginRefreshing() 
            }
        }
    
    模拟下拉刷新

    5. 可以无限复制的CAReplicatorLayer

    CALayer的子类CAReplicatorLayer通过它可以对其创建的对象进行复制,从而做出复杂的效果.
    1 . 创建一个CAReplicatorLayer,然后进行复制.

            let replicator = CAReplicatorLayer()
            let dot = CALayer()
            
            let dotLength : CGFloat = 6.0
            let dotOffset : CGFloat = 8.0
            
            replicator.frame = view.bounds
            view.layer.addSublayer(replicator)
            
            dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: dotLength)
            dot.backgroundColor = UIColor.lightGrayColor().CGColor
            dot.borderColor = UIColor(white: 1.0, alpha: 1.0).CGColor
            dot.borderWidth = 0.5
            dot.cornerRadius = 1.5
            replicator.addSublayer(dot)
            
            // 进行复制
            replicator.instanceCount = Int(view.frame.size.width / dotOffset)
            replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0.0, 0.0)
    

    2 . 让它动起来,并且让每一个dot做一点延迟.

            let move = CABasicAnimation(keyPath: "position.y")
            move.fromValue = dot.position.y
            move.toValue = dot.position.y - 50.0
            move.duration = 1.0
            move.repeatCount = 10
            dot.addAnimation(move, forKey: nil)
            // 延迟 0.02秒
            replicator.instanceDelay = 0.02
    

    3 . 将2中的代码注释掉,做出这样的一个效果

            replicator.instanceDelay = 0.02
    
            let scale = CABasicAnimation(keyPath: "transform")
            scale.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
            scale.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.4, 15, 1.0))
            scale.duration = 0.33
            scale.repeatCount = Float.infinity
            scale.autoreverses = true
            scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            dot.addAnimation(scale, forKey: "dotScale")
    

    4 .添加一个渐变色

            let fade = CABasicAnimation(keyPath: "opacity")
            fade.fromValue = 1.0
            fade.toValue = 0.2
            fade.duration = 0.33
            fade.beginTime = CACurrentMediaTime() + 0.33
            fade.repeatCount = Float.infinity
            fade.autoreverses = true
            fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            dot.addAnimation(fade, forKey: "dotOpacity")
    

    5 . 添加渐变的颜色

          
            let tint = CABasicAnimation(keyPath: "backgroundColor")
            tint.fromValue = UIColor.magentaColor().CGColor
            tint.toValue = UIColor.cyanColor().CGColor
            tint.duration = 0.66
            tint.beginTime = CACurrentMediaTime() + 0.28
            tint.fillMode = kCAFillModeBackwards
            tint.repeatCount = Float.infinity
            tint.autoreverses = true
            tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            dot.addAnimation(tint, forKey: "dotColor")
    
    1. 设置成上下摇摆
            let initialRotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
            initialRotation.fromValue = 0.0
            initialRotation.toValue = 0.01
            initialRotation.duration = 0.33
            initialRotation.removedOnCompletion = false
            initialRotation.fillMode = kCAFillModeForwards
            initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            replicator.addAnimation(initialRotation, forKey: "initialRotation")
    
            let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
            rotation.fromValue = 0.01
            rotation.toValue = -0.01
            rotation.duration = 0.99
            rotation.beginTime = CACurrentMediaTime() + 0.33
            rotation.repeatCount = Float.infinity
            rotation.autoreverses = true
            rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            replicator.addAnimation(rotation, forKey: "replicatorRotation")
    

    本文整理自 : iOS.Animations.by.Tutorials.v2.0
    源码 : https://github.com/DarielChen/DemoCode
    如有疑问,欢迎留言 :-D

    相关文章

      网友评论

      本文标题:iOS动画指南 - 3.Layer Animations的进阶使

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