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

本篇预备知识

  • 这一系列是讲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中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 4,713评论 4 22
  • Core Animation Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,...
    庄子黑黑阅读 2,228评论 0 20
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 3,144评论 2 10
  • 显式动画 显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。 属性动画 ...
    清风沐沐阅读 810评论 0 3
  • 1. 《你好,旧时光》是八月长安笔下的青春,也是我们每个人的美好! 很久很久以前,写作文的时候经常用上这一句话“趁...
    半粒苦涩半粒糖阅读 55评论 0 0