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

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

  • 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相似。


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

WACustomSwitch.gif

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,461评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,092评论 5 13
  • 书写的很好,翻译的也棒!感谢译者,感谢感谢! iOS-Core-Animation-Advanced-Techni...
    钱嘘嘘阅读 2,289评论 0 6
  • Core Animation Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,...
    45b645c5912e阅读 3,013评论 0 21
  • 海面行驶的船,因为它有坚硬的支柱。列车奔驰千里,因为它有引导的铁轨。雄鹰在蓝天翱翔,因为它有灵活的翅膀。人亦是如此...
    Lx良熙阅读 378评论 0 0