如何创建一个百分比行为驱动动画

一个很复杂的交互动画实现起来比较困难,在这篇教程中,我会向你展示如何创建一个复杂的百分比行为驱动交互动画

  • Xcode 7.2
  • Swift 2.0
  • 在本教程中,我会假设你已经熟悉CAAnimationGroup

百分比行为驱动动画

让我们先来了解一下 CAMediaTiming的背景知识, CAMediaTiming 是CAAnimation实现的一个协议,但是这个协议同样也被CALayer,以及所有的Core Animation layers的基类所实现,这意味着,如果你为一个Layer设置一个2倍的速度,这个layer的所有动画都会添加自身,并且以两倍的速度运行,执行动画。控制着这个速度的一个animation或者一个layer也可以用来暂停,只要你设置速度为0即可。与timeOffset一起使用,可以像PanGesture,** UIScrollView, slider**一样利用外部输入一个值来控制动画的执行周期。

Switch animation

我们开始创建一个类,继承UIView,它会包含我们的动画。这个动画效果和我们的UISwitch相似。


如何创建一个百分比行为驱动动画_第1张图片
nativeSwitchAnimation.gif
  • 这个动画包含三部分
    • Thumb layer(响应交互层,我的理解)
      从左至右,从右至左,都会有动画效果。开/关
    • 背景+ stroke layer 这个边框颜色是由灰到绿淡出,填充颜色动画是从白到绿。
    • 背景遮罩层 + 做路径动画的白色圆点。

我们来初始化每一个layer,然后把它们添加到主视图的Layer上。

override func setupLayers() {
      super.setupLayers()

      width = self.frame.width   
      height = self.frame.height 

      strokeBackgroundLayer.path = backgroundPath(CGRect(x: 0,y: 0,width:     width,height: height), radius: 50/2).CGPath 
      strokeBackgroundLayer.strokeColor = strokeColor.CGColor 
      strokeBackgroundLayer.fillColor = selectedColor.CGColor 
      strokeBackgroundLayer.lineWidth = 2   

      backgroundLayer.path = backgroundPath(CGRect(x: 1,y: 1,width: width-2,height: height-2), radius: 50/2).CGPath 
      backgroundLayer.fillColor = UIColor.whiteColor().CGColor 

      thumbLayer.path = UIBezierPath(ovalInRect: CGRect(x: thumbInset/2, y: thumbInset/2, width: height-thumbInset, height: height-thumbInset)).CGPath
      thumbLayer.strokeColor = strokeColor.CGColor thumbLayer.fillColor =     UIColor.whiteColor().CGColor 
      thumbLayer.lineWidth = 0.5 thumbLayer.shadowColor = UIColor.blackColor().CGColor
      thumbLayer.shadowOpacity = 0.2 
      thumbLayer.shadowRadius = 1
      thumbLayer.shadowOffset = CGSizeMake(0, 1)

      layer.addSublayer(strokeBackgroundLayer)
      layer.addSublayer(backgroundLayer) 
      layer.addSublayer(thumbLayer) 
}

现在,我们已经准备为每个layer创建动画效果,背景+边框需要2个动画: 边框颜色的变化背景颜色填充的变化,我们可以使用CAAnimationGroup去添加多个动画到同一Layer(图层).

 // MARK: ANIMATION LAYERS 
       // MARK: STROKE 
       func strokeBackgroundAnimations() -> CAAnimationGroup { 

              let groupAnimation = CAAnimationGroup() 
              groupAnimation.duration = animDuration
              groupAnimation.animations = [strokeColorAnimation(), strokeFillColorAnimation()] 
              groupAnimation.removedOnCompletion = false; return groupAnimation
       }

       func strokeColorAnimation()-> CABasicAnimation {

              let strokeAnim = CABasicAnimation(keyPath: "strokeColor") 
              strokeAnim.fromValue = strokeColor.CGColor 
              strokeAnim.toValue = selectedColor.CGColor strokeAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 
              return strokeAnim 
      }

       func strokeFillColorAnimation()-> CABasicAnimation { 

              let fillAnim = CABasicAnimation(keyPath: "fillColor")
              fillAnim.fromValue = UIColor.whiteColor().CGColor 
              fillAnim.toValue = selectedColor.CGColor
              fillAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 
              return fillAnim
       }

The thumb and background only need one animation but let's keep them in a group animation so it's easier if you want to add to them later on.

 // MARK: BACKGROUND

       func backgroundAnimations() -> CAAnimationGroup { 

                let groupAnimation = CAAnimationGroup() 
                groupAnimation.duration = animDuration
                groupAnimation.animations = [backgroundFillAnimation()]  
                groupAnimation.removedOnCompletion = false;
                return groupAnimation 
      } 

      func backgroundFillAnimation()-> CAKeyframeAnimation { 

                let endPath = backgroundPath(CGRect(x: 1, y: 1, width: width-2, height: height-2), radius: 0) 
                let beginPath = backgroundPath(CGRect(x: width/2, y: height/2, width: 0, height: 0), radius: 0)
                let fillAnim = CAKeyframeAnimation(keyPath: "path") 
                fillAnim.values = [ endPath.CGPath, beginPath.CGPath]  
                fillAnim.keyTimes = [ NSNumber(float: 0.0), NSNumber(float: 1.0)] 
                fillAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
                return fillAnim
          } 

        func backgroundPath(frame: CGRect, radius: CGFloat) -> UIBezierPath { 
                //// Rectangle Drawing
                 let rectanglePath = UIBezierPath(roundedRect: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height), cornerRadius: frame.height/2)
                 return rectanglePath
           }
 // MARK: THUMB 

        func thumbAnimations() -> CAAnimationGroup {
                 let groupAnimation = CAAnimationGroup() 
                 groupAnimation.duration = animDuration 
                 groupAnimation.animations = [thumbPositionAnimation()]
                 groupAnimation.removedOnCompletion = false
                 return groupAnimation 
          } 
        
          func thumbPositionAnimation()-> CABasicAnimation {
                  let posAnim = CABasicAnimation(keyPath: "position")
                  posAnim.fromValue = NSValue(CGPoint:CGPointMake(0, 0))
                  posAnim.toValue = NSValue(CGPoint:CGPointMake( (width - height), 0)) 
                  posAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) return posAnim
           }

现在我们已经准备好我们所需要的动画了。我们要把这些动画添加到它们各自的图层上。但是当它们不开始的时候,我们也不得不停止这些动画。我们可以创建一个用来盛放做动画的CALayer的数组,以便于这个类尽可能的重用。
var layerWithAnims : [CALayer]!

让我们在setupLayer方法后面,把所有的图层添加到数组中
self.layerWithAnims = [strokeBackgroundLayer, backgroundLayer, thumbLayer]
下面的代码会清除每一个图层的动画效果,然后再依次为它们添加动画.

 //override and add your layers with animation

       func startAllAnimations(){ 

           for layer in self.layerWithAnims{
                 layer.speed = 0
           } //ADD YOUR ANIMATIONS TO THE LAYER

           strokeBackgroundLayer.addAnimation(strokeBackgroundAnimations(), forKey: "strokeBackgroundAnimations")
           backgroundLayer.addAnimation(backgroundAnimations(), forKey: "backgroundAnimations") 
           thumbLayer.addAnimation(thumbAnimations(), forKey: "thumbAnimations") 
        }

交互动画

我们可以使用 timeOffset属性来控制这些动画.让我们来创建一个记录进度的属性,然后我们来设置动画的进度.如果这些图层上没有添加动画,我们可以调用startAllAnimation()来给这些Layer(图层) 添加动画.但是要确保这些图层被添加动画之前是没有动画的.
如果动画已经添加完成,我们可以设置每个layer(图层)的timeoffset作为当前动画的进度.
那么动画所需要的时间就等于: 进度*当前的timeOffset.

let offset = progress * CGFloat(animDuration)

   var progress: CGFloat = 0  { 
           didSet { 
               if(!self.animationAdded) { 
                      startAllAnimations() 
                      self.animationAdded = true
                       for layer in self.layerWithAnims { 
                          layer.speed = 0 layer.timeOffset = 0 
                       }
                     } else{
                          let offset = progress * CGFloat(animDuration)
                          for layer in self.layerWithAnims {
                                layer.timeOffset = CFTimeInterval(offset)
                          }
                     }
            }
     }

现在,你可以通过设置进度来控制动画了.例如 你可以使用panGesture或Slider来获取用户输入和设置动画的进度。
他已经写了一个自定义UISwitch的行为驱动动画的lib.这里有一个具有更加复杂动画效果的例子。 github地址: WACustomSwitch,灵感来源于DayNight switch

如何创建一个百分比行为驱动动画_第2张图片
WACustomSwitch.gif

不想看我的可以直接查看英文原版,原文链接: http://iostuts.io/2015/10/15/how-to-make-amazing-custom-switch/

PS: 无聊中,翻译一篇英文博客,也是这个demo挺棒的,激发了我的兴趣,锻炼一下英文阅读能力.顺便也可以增长一下见识.翻译的不好,欢迎吐槽,哈哈。谢谢.

你可能感兴趣的:(如何创建一个百分比行为驱动动画)