抽丝剥茧,一个有趣的动画

本文授权转载,作者:HenryCheng(微博
一、前言
随着开发者的增多和时间的累积,AppStore已经有非常多的应用了,每年都有很多新的APP产生。但是我们手机上留存的应用有限,所以如何吸引用户,成为产品设计的一项重要内容。其中炫酷的动画效果是重要内容之一,我们会发现很多好的应用上面都有许多很炫的效果。可能一提到炫酷的动画,很多人都很头疼,因为动画并不是那么好做,实现一个好的动画需要时间、耐心和好的思路。下面我们就以一个有趣的动画(如下图)为例,抽丝剥茧,看看到底是怎么实现的!

1466346770878893.gif

二、分析
上面图中的动画第一眼看起来的确是有点复杂,但是我们来一步步分析,就会发现其实并不是那么难。仔细看一下就会发现,大致步骤如下:

  • 1、先出来一个圆
  • 2、圆形在水平和竖直方向上被挤压,呈椭圆形状的一个过程,最后恢复成圆形
  • 3、圆形的左下角、右下角和顶部分别按顺序凸出一小部分
  • 4、圆和凸出部分形成的图形旋转一圈后变成三角形
  • 5、三角形的左边先后出来两条宽线,将三角形围在一个矩形中
  • 6、矩形由底部向上被波浪状填满
  • 7、被填满的矩形放大至全屏,弹出Welcome
    动画大致就分为上面几个步骤,拆分后我们一步步来实现其中的效果(下面所示步骤中以Swift代码为例,demo中分别有Objective-C和Swift的实现)。
    三、实现圆形以及椭圆的渐变
    首先,我们创建了一个新工程后,然后新建了一个名AnimationView的类继承UIView,这个是用来显示动画效果的一个view。然后先添加CircleLayer(圆形layer),随后实现由小变大的效果。
    class AnimationView: UIView {
    let circleLayer = CircleLayer()
    override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = UIColor.clearColor()
    addCircleLayer()
    }
    required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    }
    /**
    add circle layer
    /
    func addCircleLayer() {
    self.layer.addSublayer(circleLayer)
    circleLayer.expand()
    }
    }
    其中expand()这个方法如下
    /
    *
    expand animation function
    */
    func expand() {
    let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path)
    expandAnimation.fromValue = circleSmallPath.CGPath
    expandAnimation.toValue = circleBigPath.CGPath
    expandAnimation.duration = KAnimationDuration
    expandAnimation.fillMode = kCAFillModeForwards
    expandAnimation.removedOnCompletion = false
    self.addAnimation(expandAnimation, forKey: nil)
    }

运行效果如下


1466346840901497.gif

第一步做好了,接下来就是呈椭圆形状的变化了,仔细分析就比如一个弹性小球,竖直方向捏一下,水平方向捏一下这样的效果。这其实就是一个组合动画,如下

/**
wobbl group animation
*/
func wobbleAnimate() {
// 1、animation begin from bigPath to verticalPath
let animation1: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)    
animation1.fromValue = circleBigPath.CGPath   
animation1.toValue = circleVerticalSquishPath.CGPath
animation1.beginTime = KAnimationBeginTime
animation1.duration = KAnimationDuration
// 2、animation vertical to horizontal
let  animation2: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)
animation2.fromValue = circleVerticalSquishPath.CGPath
animation2.toValue = circleHorizontalSquishPath.CGPath
animation2.beginTime = animation1.beginTime + animation1.duration
animation2.duration = KAnimationDuration
// 3、animation horizontal to vertical
let  animation3: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)
animation3.fromValue = circleHorizontalSquishPath.CGPath
animation3.toValue = circleVerticalSquishPath.CGPath
animation3.beginTime = animation2.beginTime + animation2.duration
animation3.duration = KAnimationDuration
// 4、animation vertical to bigPath
let  animation4: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)
animation4.fromValue = circleVerticalSquishPath.CGPath
animation4.toValue = circleBigPath.CGPath
animation4.beginTime = animation3.beginTime + animation3.duration
animation4.duration = KAnimationDuration
// 5、group animation
let animationGroup: CAAnimationGroup = CAAnimationGroup()
animationGroup.animations = [animation1, animation2, animation3, animation4]
animationGroup.duration = 4 * KAnimationDuration
animationGroup.repeatCount = 2
addAnimation(animationGroup, forKey: nil)
}

上面代码中实现了从 圆 → 椭圆(x方向长轴)→ 椭圆(y方向长轴)→ 圆这一系列的变化,最后组合成一个动画。这一步实现后效果如下

1466346876652751.gif

四、实现圆形边缘的凸出部分
关于这个凸出部分,乍一看可能感觉会比较难实现,看起来挺复杂的。其实实现的原理很简单,仔细分析我们会发现这三个凸出部分连起来刚好是一个三角形,那么第一步我们就在之前的基础上先加一个三角形的layer,如下

import UIKit

class TriangleLayer: CAShapeLayer {
    let paddingSpace: CGFloat = 30.0
    override init() {
      super.init()
      fillColor = UIColor.colorWithHexString("#009ad6").CGColor
      strokeColor = UIColor.colorWithHexString("#009ad6").CGColor
      lineWidth = 7.0
      path = smallTrianglePath.CGPath
    }
    required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }
    var smallTrianglePath: UIBezierPath {
      let smallPath = UIBezierPath()
      smallPath.moveToPoint(CGPointMake(5.0 + paddingSpace, 95.0))
      smallPath.addLineToPoint(CGPointMake(50.0, 12.5 + paddingSpace))
      smallPath.addLineToPoint(CGPointMake(95.0 - paddingSpace, 95.0))
      smallPath.closePath()
      return smallPath
    }
}
1466346913677135.png

然后设置圆角
1
2

lineCap = kCALineCapRound

lineJoin = kCALineJoinRound

1466346937206965.png

下面就是来做凸出部分了,原理其实很简单,就是将现在这个三角形保持中心不变,左边向左延伸即可


1466346949882231.gif

然后同理,保持中心不变分别按顺序向右和向上拉伸


1466346960871367.gif

具体过程是这样的
        /**
        triangle animate function
        */
        func triangleAnimate() {
        // left
          let triangleAnimationLeft: CABasicAnimation = CABasicAnimation(keyPath: "path")   
          triangleAnimationLeft.fromValue = smallTrianglePath.CGPath
          triangleAnimationLeft.toValue = leftTrianglePath.CGPath
          triangleAnimationLeft.beginTime = 0.0
          triangleAnimationLeft.duration = 0.3
        // right
          let triangleAnimationRight: CABasicAnimation = CABasicAnimation(keyPath: "path")
          triangleAnimationRight.fromValue = leftTrianglePath.CGPath
          triangleAnimationRight.toValue = rightTrianglePath.CGPath
          triangleAnimationRight.beginTime = triangleAnimationLeft.beginTime + triangleAnimationLeft.duration
          triangleAnimationRight.duration = 0.25
        // top
          let triangleAnimationTop: CABasicAnimation = CABasicAnimation(keyPath: "path")
          triangleAnimationTop.fromValue = rightTrianglePath.CGPath
          triangleAnimationTop.toValue = topTrianglePath.CGPath
          triangleAnimationTop.beginTime = triangleAnimationRight.beginTime + triangleAnimationRight.duration
          triangleAnimationTop.duration = 0.20
        // group
          let triangleAnimationGroup: CAAnimationGroup = CAAnimationGroup()
          triangleAnimationGroup.animations = [triangleAnimationLeft, triangleAnimationRight, triangleAnimationTop]
          triangleAnimationGroup.duration = triangleAnimationTop.beginTime + triangleAnimationTop.duration
          triangleAnimationGroup.fillMode = kCAFillModeForwards
          triangleAnimationGroup.removedOnCompletion = false
          addAnimation(triangleAnimationGroup, forKey: nil)
        }

我们接下来把三角形的颜色改一下

1466346987587957.gif

这里颜色相同了我们就可以看到了这个凸出的这个效果,调到正常速率(为了演示,把动画速率调慢了) ,联合之前所有的动作,到现在为止,效果是这样的
1466346997443563.gif

到现在为止,看上去还不错,差不多已经完成一半了,继续下一步!
五、实现旋转和矩形
旋转来说很简单了,大家估计都做过旋转动画,这里就是把前面形成的图形旋转一下(当然要注意设置锚点anchorPoint)

        /** 
        self transform z
        */
        func transformRotationZ() {
          self.layer.anchorPoint = CGPointMake(0.5, 0.65)
          let rotationAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
          rotationAnimation.toValue = CGFloat(M_PI * 2)
          rotationAnimation.duration = 0.45
          rotationAnimation.removedOnCompletion = true
          layer.addAnimation(rotationAnimation, forKey: nil)
        }
1466347034403843.gif

旋转之后原图形被切成了一个三角形,思路就是把原来的大圆,按着这个大三角形的内切圆剪切一下即可

/**

     
contract animation function

     
*/

func contract() {

        
let contractAnimation: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
contractAnimation.fromValue = circleBigPath.CGPath

        
contractAnimation.toValue = circleSmallPath.CGPath

        
contractAnimation.duration = KAnimationDuration

        
contractAnimation.fillMode = kCAFillModeForwards

        
contractAnimation.removedOnCompletion = 
false

        
addAnimation(contractAnimation, forKey: nil)

    
}
1466347061781547.gif

接下来就是画矩形,新建一个RectangleLayer,划线


/**

     
line stroke color change with custom color

     
- parameter color: custom color

     
*/

func strokeChangeWithColor(color: UIColor) {

        
strokeColor = color.CGColor

        
let strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: 
"strokeEnd"
)

        
strokeAnimation.fromValue = 0.0

        
strokeAnimation.toValue = 1.0

        
strokeAnimation.duration = 0.4

        
addAnimation(strokeAnimation, forKey: nil)

    
}
1466347085312463.gif

最后面就是经典的水波纹动画了,不多说,直接上代码
![WavetAnimation.gif](http:
//upload-images.jianshu.io/upload_images/571495-856dc8f307d16f60.gif?imageMogr2/auto-orient/strip)

func animate() {

         
/// 1

        
let waveAnimationPre: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
waveAnimationPre.fromValue = wavePathPre.CGPath

        
waveAnimationPre.toValue = wavePathStarting.CGPath

        
waveAnimationPre.beginTime = 0.0

        
waveAnimationPre.duration = KAnimationDuration

         
/// 2

        
let waveAnimationLow: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
waveAnimationLow.fromValue = wavePathStarting.CGPath

        
waveAnimationLow.toValue = wavePathLow.CGPath

        
waveAnimationLow.beginTime = waveAnimationPre.beginTime + waveAnimationPre.duration

        
waveAnimationLow.duration = KAnimationDuration

         
/// 3

        
let waveAnimationMid: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
waveAnimationMid.fromValue = wavePathLow.CGPath

        
waveAnimationMid.toValue = wavePathMid.CGPath

        
waveAnimationMid.beginTime = waveAnimationLow.beginTime + waveAnimationLow.duration

        
waveAnimationMid.duration = KAnimationDuration

         
/// 4

        
let waveAnimationHigh: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
waveAnimationHigh.fromValue = wavePathMid.CGPath

        
waveAnimationHigh.toValue = wavePathHigh.CGPath

        
waveAnimationHigh.beginTime = waveAnimationMid.beginTime + waveAnimationMid.duration

        
waveAnimationHigh.duration = KAnimationDuration

         
/// 5

        
let waveAnimationComplete: CABasicAnimation = CABasicAnimation(keyPath: 
"path"
)

        
waveAnimationComplete.fromValue = wavePathHigh.CGPath

        
waveAnimationComplete.toValue = wavePathComplete.CGPath

        
waveAnimationComplete.beginTime = waveAnimationHigh.beginTime + waveAnimationHigh.duration

        
waveAnimationComplete.duration = KAnimationDuration

         
/// group animation

        
let arcAnimationGroup: CAAnimationGroup = CAAnimationGroup()

        
arcAnimationGroup.animations = [waveAnimationPre, waveAnimationLow, waveAnimationMid, waveAnimationHigh, waveAnimationComplete]

        
arcAnimationGroup.duration = waveAnimationComplete.beginTime + waveAnimationComplete.duration

        
arcAnimationGroup.fillMode = kCAFillModeForwards

        
arcAnimationGroup.removedOnCompletion = 
false

        
addAnimation(arcAnimationGroup, forKey: nil)

    
}
1466347106531532.gif

找几个点控制水波形状,画出贝塞尔曲线即可,到这里基本就完成了。接下来最后一步,放大,并弹出Welcome


func expandView() {

        
backgroundColor = UIColor.colorWithHexString(
"#40e0b0"
)

        
frame = CGRectMake(frame.origin.x - blueRectangleLayer.lineWidth,

                           
frame.origin.y - blueRectangleLayer.lineWidth,

                           
frame.size.width + blueRectangleLayer.lineWidth * 2,

                           
frame.size.height + blueRectangleLayer.lineWidth * 2)

        
layer.sublayers = nil

        
UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {

            
self.frame = self.parentFrame

            
}, completion: { finished 
in

                
self.delegate?.completeAnimation()

        
})

    
}

放大完以后设置代理,然后在主的vc中添加Welcome这个Label


// MARK: -

// MARK: AnimationViewDelegate

func completeAnimation() {

        
// 1

        
animationView.removeFromSuperview()

        
view.backgroundColor = UIColor.colorWithHexString(
"#40e0b0"
)

        
// 2

        
let label: UILabel = UILabel(frame: view.frame)

        
label.textColor = UIColor.whiteColor()

        
label.font = UIFont(name: 
"HelveticaNeue-Thin"
, size: 50.0)

        
label.textAlignment = NSTextAlignment.Center

        
label.text = 
"Welcome"

        
label.transform = CGAffineTransformScale(label.transform, 0.25, 0.25)

        
view.addSubview(label)

        
// 3

        
UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.1, options: UIViewAnimationOptions.CurveEaseInOut,animations: ({

                
label.transform = CGAffineTransformScale(label.transform, 4.0, 4.0)

            
}), completion: { finished 
in

                
self.addTouchButton()

        
})

    
}

到现在为止,动画全部完成

1466347148388339.gif

六、最后
同样,还是提供了两个版本(Objective-C & Swift),你可以在这里查看源码!
一直对动画比较感兴趣,希望多研究多深入,有什么意见或者建议的话,可以留言或者私信,如果觉得还好的话,请star支持,谢谢!

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

推荐阅读更多精彩内容