iOS複雜動畫之抽絲剝繭(Objective C & Swift)

HenryCheng發表於2019-03-01

一、前言


隨著開發者的增多和時間的累積,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支援,謝謝!

原效果出處

相關文章