创建自定义动画按钮(独家翻译)


layout: post
title: 创建自定义动画按钮
description: 独家翻译
image: assets/images/pic02.jpg



被追波设计的启发,我们将要去创造一个自定义的动画按钮。我希望通过这个教程,当说到去编码动效,你能至少理解一些重要的步骤。

理解动效

首先,在我们打开Xcode,跟变换(transform),图层(CALayer)的子类们等等玩耍之前,我们需要弄懂这个我们将要去构造实际动画模型是什么,这是非常重要的。盯着这个模型看一会儿,然后思考你将提供说明来简单的描述发生了什么。EZGIF提供一些非常好用,用于gif图的工具,例如,改变动画速度,然后在慢速度中查看动效。
下面的慢速度的图可能有帮助:


让我们开始打破动效图层,然后命名他们来获得一个更方便的引用:
1:star 2: Fill 3: Ring
1:star 2: Fill 3: Ring

好了,现在我们已经标记了这些图层,我们可以用帧分解来描述每一层的发生情况。下面是一个动效步骤的简单描述:
1.星图层和环图层几乎同步增长。
2.星图层和环图层停止增长,然后用比第一步更快的速度开始缩小到星图层的中心。
3.填充圆从星图层中心开始增长。
4.星图层增长到初始大小,并且填充色改变了。
5.填充圆增长到步骤二里的大小,然后缩小到初始大小。

创建路径

在iOS中CGPaths代表的路径并在代码中创建他们是一个痛苦而且耗时的任务。幸运的是的这里有个工具叫PaintCode使这个任务变得很容易。
所有你需要做的就是创建基于软件Adobe lllustrator或者Sketch的矢量图形,导出为SVG文件,导入PaintCode,然后PaintCode会提供你这个图形OC和Swift的代码。

这下面的代码代表星图层:

    var star = UIBezierPath()
    star.moveToPoint(CGPointMake(112.79, 119))
    star.addCurveToPoint(CGPointMake(107.75, 122.6),controlPoint1: CGPointMake(113.41, 122.8), controlPoint2:       CGPointMake(111.14, 124.42))
    star.addLineToPoint(CGPointMake(96.53, 116.58))
    star.addCurveToPoint(CGPointMake(84.14, 116.47), controlPoint1: CGPointMake(93.14, 114.76), controlPoint2: CGPointMake(87.56, 114.71))
    star.addLineToPoint(CGPointMake(72.82, 122.3))
    star.addCurveToPoint(CGPointMake(67.84, 118.62), controlPoint1: CGPointMake(69.4, 124.06), controlPoint2: CGPointMake(67.15, 122.41))
    star.addLineToPoint(CGPointMake(70.1, 106.09))
    star.addCurveToPoint(CGPointMake(66.37, 94.27), controlPoint1: CGPointMake(70.78, 102.3), controlPoint2: CGPointMake(69.1, 96.98))
    star.addLineToPoint(CGPointMake(57.33, 85.31))
    star.addCurveToPoint(CGPointMake(59.29, 79.43), controlPoint1: CGPointMake(54.6, 82.6), controlPoint2: CGPointMake(55.48, 79.95))
    star.addLineToPoint(CGPointMake(71.91, 77.71))
    star.addCurveToPoint(CGPointMake(81.99, 70.51), controlPoint1: CGPointMake(75.72, 77.19), controlPoint2: CGPointMake(80.26, 73.95))
    star.addLineToPoint(CGPointMake(87.72, 59.14))
    star.addCurveToPoint(CGPointMake(93.92, 59.2), controlPoint1: CGPointMake(89.46, 55.71), controlPoint2: CGPointMake(92.25, 55.73))
    star.addLineToPoint(CGPointMake(99.46, 70.66))
    star.addCurveToPoint(CGPointMake(109.42, 78.03), controlPoint1: CGPointMake(101.13, 74.13), controlPoint2: CGPointMake(105.62, 77.44))
    star.addLineToPoint(CGPointMake(122, 79.96))
    star.addCurveToPoint(CGPointMake(123.87, 85.87), controlPoint1: CGPointMake(125.81, 80.55), controlPoint2: CGPointMake(126.64, 83.21))
    star.addLineToPoint(CGPointMake(114.67, 94.68))
    star.addCurveToPoint(CGPointMake(110.75, 106.43), controlPoint1: CGPointMake(111.89, 97.34), controlPoint2: CGPointMake(110.13, 102.63))
    star.addLineToPoint(CGPointMake(112.79, 119))

我们可以在iOS中手动创建简单的图形,例如圆图层:

    let circle = UIBezierPath(ovalInRect: inFrame)

现在我们可以创建自定义的按钮了。

连接图层

Xcode6 带来一个叫做活动视图(live views)的新科技,这使得处理自定义布局代码更容易,并且不用运行或者构建视图就能提供立即的可视化的反馈。所以让我们使用活动视图!为你的类使用活动视图非常简单:
1.放置关键词@IBDesignable 在类声明之上
2.放置关键词@IBInspectable 在一个你想在故事版的属性检查器上改变的变量上。
3.重写 layoutSubviews().这是一个你将添加子视图和子图层的地方。
为了我们的星星按钮,我们将创建一个UIButton 的子类并且遵循下面的步骤:

    @IBDesignable
    class StarButton: UIButton
    {
      private var starShape: CAShapeLayer!
      private var outerRingShape: CAShapeLayer!
      private var fillRingShape: CAShapeLayer!

      @IBInspectable
      var lineWidth: CGFloat = 1 {
        didSet {
        updateLayerProperties()
       }
    }

    @IBInspectable
    var favoriteColor: UIColor = UIColor(hex:"eecd34") {
      didSet {
        updateLayerProperties()
      }
    }

    @IBInspectable
    var notFavoriteColor: UIColor = UIColor(hex:"9e9b9b") {
      didSet {
        updateLayerProperties()
      }
    }

    @IBInspectable
    var starFavoriteColor: UIColor = UIColor(hex:"9e9b9b") {
      didSet {
        updateLayerProperties()
      }
    }

    var isFavorite: Bool = false {
      didSet {
        return self.isFavorite ? favorite() : notFavorite()
      }
    }

    private func updateLayerProperties()
    {
      if fillRingShape != nil
      {
        fillRingShape.fillColor = favoriteColor.CGColor
      }

      if outerRingShape != nil
      {
        outerRingShape.lineWidth = lineWidth
        outerRingShape.strokeColor = notFavoriteColor.CGColor
      }

      if starShape != nil
      {
        starShape.fillColor = isFavorite ? starFavoriteColor.CGColor :   notFavoriteColor.CGColor
      }
    }

    override func layoutSubviews()
    {
      super.layoutSubviews()
      updateLayerProperties()
     }
    }

转到故事版,创建一个按钮,把它设置成自定义,去掉上面默认的文本,然后设置成我们的StarButton类。



在属性检查器上设置可视化属性。



然后看!

动效

创建动画总是一点点尝试和失败。当然,即使你有动画背景,你也不会流畅的构建动效。为了节约时间,我会直接跳向代码。为了创建动效,我已经把这个动效分成五个步骤。我们要处理的主要属性是用来转换大小的CATransform3D,用来改变阿尔法值的opacity,用来改变颜色的fillColor.
我们假定我们用这些按钮来点赞,然后这样命名函数。


    private func favorite()
    {
    // 1. Star grows
    var starGoUp = CATransform3DIdentity
    starGoUp = CATransform3DScale(starGoUp, 1.5, 1.5, 1.5)

    // 2. Star stop growing and starts shrinking
    var starGoDown = CATransform3DIdentity
    starGoDown = CATransform3DScale(starGoDown, 0.01, 0.01, 0.01)

    // Configure a keyframe animation with both transforms (grow and shrink)
    let starKeyFrames = CAKeyframeAnimation(keyPath: "transform")
    starKeyFrames.values = [
      NSValue(CATransform3D:CATransform3DIdentity),
      NSValue(CATransform3D:starGoUp),
      NSValue(CATransform3D:starGoDown)
    ]
    starKeyFrames.keyTimes = [0.0,0.4,0.6]
    starKeyFrames.duration = 0.4
    starKeyFrames.beginTime = CACurrentMediaTime() + 0.05
    starKeyFrames.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)

    // This is VERY important when you're working with relative time, remove and odd things will happen
    starKeyFrames.fillMode =  kCAFillModeBackwards
    starKeyFrames.setValue(favoriteKey, forKey: starKey)

    // Let the notification tell us when it's over
    starKeyFrames.delegate = self
    starShape.addAnimation(starKeyFrames, forKey: favoriteKey)
    starShape.transform = starGoDown

    // 1. Ring grows
    var grayGoUp = CATransform3DIdentity
    grayGoUp = CATransform3DScale(grayGoUp, 1.5, 1.5, 1.5)

    // 2. Ring stop growing and starts shrinking
    var grayGoDown = CATransform3DIdentity
    grayGoDown = CATransform3DScale(grayGoDown, 0.01, 0.01, 0.01)

    let outerCircleAnimation = CAKeyframeAnimation(keyPath: "transform")
    outerCircleAnimation.values = [
      NSValue(CATransform3D:CATransform3DIdentity),
      NSValue(CATransform3D:grayGoUp),
      NSValue(CATransform3D:grayGoDown)
    ]
    outerCircleAnimation.keyTimes = [0.0,0.4,0.6]
    outerCircleAnimation.duration = 0.4
    outerCircleAnimation.beginTime = CACurrentMediaTime() + 0.01
    outerCircleAnimation.fillMode =  kCAFillModeBackwards
    outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)

    outerRingShape.addAnimation(outerCircleAnimation, forKey: "Gray circle Animation")
    outerRingShape.transform = grayGoDown

    // 3. Fill Circle grows from Star's center.
    var favoriteFillGrow = CATransform3DIdentity
    favoriteFillGrow = CATransform3DScale(favoriteFillGrow, 1.5, 1.5, 1.5)

    // 5. Fill Circle grows until reach the size of step 2 and shrink back to the initial size.
    let fillCircleAnimation = CAKeyframeAnimation(keyPath: "transform")

    fillCircleAnimation.values = [
      NSValue(CATransform3D:fillRingShape.transform),
      NSValue(CATransform3D:favoriteFillGrow),
      NSValue(CATransform3D:CATransform3DIdentity)
    ]
    fillCircleAnimation.keyTimes = [0.0,0.4,0.6]
    fillCircleAnimation.duration = 0.4
    fillCircleAnimation.beginTime = CACurrentMediaTime() + 0.22
    fillCircleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
    fillCircleAnimation.fillMode =  kCAFillModeBackwards

    let favoriteFillOpacity = CABasicAnimation(keyPath: "opacity")
    favoriteFillOpacity.toValue = 1
    favoriteFillOpacity.duration = 1
    favoriteFillOpacity.beginTime = CACurrentMediaTime()
    favoriteFillOpacity.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
    favoriteFillOpacity.fillMode =  kCAFillModeBackwards

    fillRingShape.addAnimation(favoriteFillOpacity, forKey: "Show fill circle")
    fillRingShape.addAnimation(fillCircleAnimation, forKey: "fill circle Animation")
    fillRingShape.transform = CATransform3DIdentity
    }

当第一部分的动效结束,第二部分的动效会触发

    private func endFavorite()
    {
    // just a helper to run this piece of code with default actions disabled
    executeWithoutActions {
    self.starShape.fillColor = self.starFavoriteColor.CGColor
    self.starShape.opacity = 1
    self.fillRingShape.opacity = 1
    self.outerRingShape.transform = CATransform3DIdentity
    self.outerRingShape.opacity = 0
    }

    // 4. Star grows to it's initial size, and the filling color is changed.
    let starAnimations = CAAnimationGroup()
    var starGoUp = CATransform3DIdentity
    starGoUp = CATransform3DScale(starGoUp, 2, 2, 2)

    let starKeyFrames = CAKeyframeAnimation(keyPath: "transform")
    starKeyFrames.values = [
    NSValue(CATransform3D: starShape.transform),
    NSValue(CATransform3D:starGoUp),
    NSValue(CATransform3D:CATransform3DIdentity)
    ]
    starKeyFrames.keyTimes = [0.0,0.4,0.6]
    starKeyFrames.duration = 0.2
    starKeyFrames.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

    starShape.addAnimation(starKeyFrames, forKey: nil)
    starShape.transform = CATransform3DIdentity
    }  

对于取消收藏,这个动效没有秘密:

    private func notFavorite()
    {
    let starFillColor = CABasicAnimation(keyPath: "fillColor")
    starFillColor.toValue = notFavoriteColor.CGColor
    starFillColor.duration = 0.3

    let starOpacity = CABasicAnimation(keyPath: "opacity")
    starOpacity.toValue = 0.5
    starOpacity.duration = 0.3

    let starGroup = CAAnimationGroup()
    starGroup.animations = [starFillColor, starOpacity]

    starShape.addAnimation(starGroup, forKey: nil)
    starShape.fillColor = notFavoriteColor.CGColor
    starShape.opacity = 0.5

    let fillCircle = CABasicAnimation(keyPath: "opacity")
    fillCircle.toValue = 0
    fillCircle.duration = 0.3
    fillCircle.setValue(notFavoriteKey, forKey: starKey)
    fillCircle.delegate = self

    fillRingShape.addAnimation(fillCircle, forKey: nil)
    fillRingShape.opacity = 0

    let outerCircle = CABasicAnimation(keyPath: "opacity")
    outerCircle.toValue = 0.5
    outerCircle.duration = 0.3

    outerRingShape.addAnimation(outerCircle, forKey: nil)
    outerRingShape.opacity = 0.5
    }

一个附赠品

因为Swift在iOS社区是一个真正的明星,我们庆祝一下!
按照下面这个CGPath修改这个星图层路径:

    var swiftPath = UIBezierPath()
    swiftPath.moveToPoint(CGPointMake(376.2, 283.2))
    swiftPath.addCurveToPoint(CGPointMake(349.8, 238.4),controlPoint1: CGPointMake(367.4, 258.4), controlPoint2: CGPointMake(349.8, 238.4))
    swiftPath.addCurveToPoint(CGPointMake(236.5, 0), controlPoint1: CGPointMake(349.8, 238.4), controlPoint2: CGPointMake(399.7, 105.6))
    swiftPath.addCurveToPoint(CGPointMake(269, 180.8), controlPoint1: CGPointMake(303.7, 101.6), controlPoint2: CGPointMake(269, 180.8))
    swiftPath.addCurveToPoint(CGPointMake(181.29, 117.07), controlPoint1: CGPointMake(269, 180.8), controlPoint2: CGPointMake(211.4, 140.8))
    swiftPath.addCurveToPoint(CGPointMake(85, 33.6), controlPoint1: CGPointMake(151.18, 93.35), controlPoint2: CGPointMake(85, 33.6))
    swiftPath.addCurveToPoint(CGPointMake(145, 117.07), controlPoint1: CGPointMake(85, 33.6), controlPoint2: CGPointMake(128.15, 96.31))
    swiftPath.addCurveToPoint(CGPointMake(185.78, 163.66), controlPoint1: CGPointMake(161.85, 137.84), controlPoint2: CGPointMake(185.78, 163.66))
    swiftPath.addCurveToPoint(CGPointMake(136.36, 129.42), controlPoint1: CGPointMake(185.78, 163.66), controlPoint2: CGPointMake(161.07, 147.39))
    swiftPath.addCurveToPoint(CGPointMake(34.6, 50.4), controlPoint1: CGPointMake(111.65, 111.46), controlPoint2: CGPointMake(34.6, 50.4))
    swiftPath.addCurveToPoint(CGPointMake(133.8, 169.2), controlPoint1: CGPointMake(34.6, 50.4), controlPoint2: CGPointMake(82.69, 119.24))
    swiftPath.addCurveToPoint(CGPointMake(214.6, 244), controlPoint1: CGPointMake(184.91, 219.16), controlPoint2: CGPointMake(214.6, 244))
    swiftPath.addCurveToPoint(CGPointMake(129.8, 264.8), controlPoint1: CGPointMake(214.6, 244), controlPoint2: CGPointMake(196.2, 263.2))
    swiftPath.addCurveToPoint(CGPointMake(0, 221), controlPoint1: CGPointMake(63.4, 266.4), controlPoint2: CGPointMake(0, 221))
    swiftPath.addCurveToPoint(CGPointMake(206.6, 339.2), controlPoint1: CGPointMake(0, 221), controlPoint2: CGPointMake(62.5, 339.2))
    swiftPath.addCurveToPoint(CGPointMake(325, 304.8), controlPoint1: CGPointMake(270.6, 339.2), controlPoint2: CGPointMake(288.93, 304.8))
    swiftPath.addCurveToPoint(CGPointMake(383.3, 339.2), controlPoint1: CGPointMake(361.07, 304.8), controlPoint2: CGPointMake(381.7, 340))
    swiftPath.addCurveToPoint(CGPointMake(376.2, 283.2), controlPoint1: CGPointMake(384.9, 338.4), controlPoint2: CGPointMake(385, 308))
    return swiftPath.CGPath

这就可以了!偶也!Swift!


就是这个!我希望你能理解多一点关于创建动效。去我的Github找到完整的工程。当我添加一些新图形的时候,我会升级代码,所以注意这个仓库!如果你喜欢这个项目,请分享并且粉一下这个git仓库!

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,643评论 4 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,594评论 25 707
  • 如何你想某事正确,自己动手做吧。——Charles-Guillaume Étienne 前一章介绍了隐式动画的概念...
    liril阅读 1,254评论 0 3
  • 一,https://stackoverflow.com/questions/41275442/testing-vo...
    LX2014阅读 282评论 0 0
  • 樱桃产业研究。
    樂樂827阅读 148评论 0 0