美文网首页v2panda的技术专题iOS进阶iOS开发
iOS复杂动画之抽丝剥茧(Objective-C & S

iOS复杂动画之抽丝剥茧(Objective-C & S

作者: HenryCheng | 来源:发表于2016-06-16 16:00 被阅读10211次

    一、前言


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

    HWLoadingAnimation.gif

    二、分析


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

    • 1、先出来一个圆
    • 2、圆形在水平和竖直方向上被挤压,呈椭圆形状的一个过程,最后恢复成圆形
    • 3、圆形的左下角、右下角和顶部分别按顺序凸出一小部分
    • 4、圆和凸出部分形成的图形旋转一圈后变成三角形
    • 5、三角形的左边先后出来两条宽线,将三角形围在一个矩形中
    • 6、矩形由底部向上被波浪状填满
    • 7、被填满的矩形放大至全屏,弹出Welcome

    动画大致就分为上面几个步骤,拆分后我们一步步来实现其中的效果(下面所示步骤中以Swift代码为例,demo中分别有Objective-CSwift的实现)。

    三、实现圆形以及椭圆的渐变


    首先,我们创建了一个新工程后,然后新建了一个名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)
        }
    

    运行效果如下

    CircleLayer.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方向长轴)→ 圆这一系列的变化,最后组合成一个动画。这一步实现后效果如下

    WobbleAnimation.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
        }
    }
    
    addTriangleLayer.png

    然后设置圆角

            lineCap = kCALineCapRound
            lineJoin = kCALineJoinRound
    
    roundTriangle.png

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

    TriangleLayerAnimate.gif

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

    TriangleLayerAnimateGroup.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)
        }
    

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

    TriangleLayerAnimateGroup2.gif

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

    FrontAnimateGroup.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)
        }
    
    RotationAnimation.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)
        }
    
    ContractAnimation.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)
        }
        
    
    RectangletAnimation.gif

    最后面就是经典的水波纹动画了,不多说,直接上代码

    
        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)
        }
    
    WavetAnimation.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()
            })
    
        }
    

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

    HWLoadingAnimation.gif

    六、最后


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

    原效果出处

    相关文章

      网友评论

      • 浮生的半夏:作者的代码思路很清晰 这个抽丝剥茧 按照作者的写一遍动画基本掌握了 下载的demo中有个小bug - (UIBezierPath *)wavePathStarting {
        if (!_wavePathStarting) {
        _wavePathStarting = [[UIBezierPath alloc] init];
        [_wavePathStarting moveToPoint:CGPointMake(0.0, 100.0)];
        [_wavePathStarting addLineToPoint:CGPointMake(0.0, 80.0)];
        [_wavePathStarting addCurveToPoint:CGPointMake(60.0, 80.0 应该为100 不然又断层的感觉) controlPoint1:CGPointMake(30.0, 70.0) controlPoint2:CGPointMake(40.0, 90.0)];
        [_wavePathStarting addLineToPoint:CGPointMake(100.0, 100.0)];
        [_wavePathStarting addLineToPoint:CGPointMake(0, 100.0)];
        [_wavePathStarting closePath];
        }
        return _wavePathStarting;
        }
        非常感谢作者的分享:smiley: :smiley:
      • swfit小学生:感谢楼主,
        提个小bug:swift 3.0 :
        需要 Extension.swift : var cString = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased()
        HenryCheng:@swfit小学生 swift3.0我还没看,等有时间更新,谢谢支持
      • 阳光大道在前方:github上下载了一下, 照着模仿了一遍, 感觉特炫酷!大神啊....溜溜溜.....
      • 刘栋:哇塞 还是希望多出OC版的 或者俩个都有的版本 :clap:
      • d17f23262784: :+1:
        HenryCheng:@天上牛在飞 thank you
      • CodeGeass:厉害,动画玩得6
      • 一缕殇流化隐半边冰霜:今天看到一个复杂动画,又想起你这篇文章了,再回来膜拜一下,按照你的思路拆分一遍,感觉思路很清晰!感觉这篇文章绝对可以进动画学习经典文章了!:+1::+1::+1:
        HenryCheng:@一缕殇流化隐半边冰霜 :smiley::smiley::smiley:
      • 大墙66370:写错了 :joy: 应该是if (self = [super init])
        系统自动生成初始化方法 貌似是这样的
        - (instancetype)init
        {
        self = [super init];
        if (self) {
        }
        return self;
        }
        if(self = [super init]) 这个是上面的简写.
        先把 [super init] 赋值给self if赋值成功了 在做一下操作
        if (self == [super init]) 这样写 直接判断 是否相等? 我以前老师是按照 上面的教的.一般人也是按照if (self = [super init])写的吧.

        谢谢大神的回复.



        大墙66370:@晋先森 你都这样说了,我先不回答是不是接触开发才一个月这个问题.你好意思不解释一下这个问题why if (self == [super init])和if (self = [super init]) 都可以
        HenryCheng:@大墙66370 这两种写法是一样的,没区别的
      • HenryCheng:oc 篇 初始化 CAShapeLayer 的时候 init 方法 if (self == [super init]) 不是 if(self = [super =init])吗? 这个我没看明白什么意思。。。。。triangleAnimationLeft.fillMode = kCAFillModeForwards; triangleAnimationLeft.removedOnCompletion = NO;这两个是做完动画保留最后状态的,所以你做完之后有所操作的话,就要设置
      • 大墙66370:很好,真的很好,实用不装逼,不炫技.能让人理解.
        oc 篇 初始化 CAShapeLayer 的时候 init 方法 if (self == [super init]) 不是 if(self = [super =init])吗?
        还有 在出现 三个三角的那个 TriangleLayer层的 在出现 左边 那个叫的 CABasicAnimation 不用加上 下面这两行吗? 同理 右 和上
        triangleAnimationLeft.fillMode = kCAFillModeForwards; triangleAnimationLeft.removedOnCompletion = NO;
      • 一剑书生:请问博主,写一个这样的动画大概花多久时间啊?写文章又花了多久呢? :smiley:
        HenryCheng:@一剑书生 那得看动画的复杂程度了,写文章的话我是比较久的
      • 45826397f73f:大神,动画效果简直屌爆了,持续关注中...
      • 66b94c12b08c:大神好,我是动画小白。有个疑问,这里面创建的NSTimer 对象你是如何处理释放的。
      • pengrain:壮士 ! 等等。。。我要粉你
      • d9557f883fd8:酷~杭州的啊
        HenryCheng:@zhnnnnn 嗯哼
      • 垚子:,
      • 木木烈少:赞,不过我好久之前看过一篇英文文章,和这个动画一模一样,这是?
        月咏蝴蝶:@木木少烈 你应该是在raywenderlich看到的吧,我也感觉这个动画效果有点面熟
        木木烈少:@HenryCheng :+1:
        HenryCheng:@木木少烈 嗯,这个就是基于那个动画一步步分析的
      • dd25f9257b81:动画玩的溜~~~期待下一篇
      • xxttw:犀利的哭
      • Ryan文濤:动画大神,必须给你点个赞:+1:🏻
        HenryCheng:@Ryan文濤 谢谢
        Ryan文濤:@HenryCheng 期待你下一篇如此吊的动画讲解。我会时刻关注:smile::smile::smile:
        HenryCheng:@Ryan文濤 :joy:
      • 原来可以这样:6666 多模仿 多学原理 多看思路! 就凭这个动画 必须加个关注
        HenryCheng:@原来可以这样 :smile:
      • code_间特门:看来动画这东西 都是好多模块组合而成的,并不是一个东西变来变去
        HenryCheng:@code_间特门 得分情况吧
      • 王德夫:思路清晰,跳槽否
        HenryCheng:@王德夫 :smiley: :smiley: :smiley:
      • Meng张梦莹:这些代码需要在哪里编译执行:smiley:
      • 拓跋鼠:有毫炫酷 :stuck_out_tongue_closed_eyes:
      • Nixon:请问 CAShapeLayer 的 Path 做动画的时候,有什么办法可以让他AutoLayout呢?
      • dfc63a852abb:不错不错
      • eae8518bc399:写的好详细:+1:
        HenryCheng:@Pasco_iOS 有帮助就好 :smile:
      • 万八量化: :smiley: 炫酷
        HenryCheng:@JK77 :blush:
      • 一缕殇流化隐半边冰霜:动画大神:+1::+1::+1:
        HenryCheng:@一缕殇流化隐半边冰霜 :joy: :joy:
      • 健健锅:多来些这方面,动画的
        健健锅:@HenryCheng 每次面试总是会被问到,动画方面你有什么独特的想法。直接蒙蔽
        HenryCheng:@健健锅 :blush: 阔以的
      • Joy___:大神又出新作
        HenryCheng:@Martin_Joy :joy: :joy: :joy:

      本文标题:iOS复杂动画之抽丝剥茧(Objective-C & S

      本文链接:https://www.haomeiwen.com/subject/uhvudttx.html