系统学习iOS动画—— BahamaAirLogin(Layer 动画)

571 阅读5分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

在我之前的文章 —— 系统学习iOS动画—— BahamaAirLogin(UIKit动画) 中,用的是UIView.animate来进行alpha的变化实现添加透明度变化动画。layer 动画的工作方式很像UIView 动画;只需在定义的时间段内,在起始值和结束值之间设置属性的动画,并让Core Animation处理其间的渲染。然而,layer动画比UIView动画有更多的可动画属性;在设计效果时,这给了开发者很多选择和灵活性。许多专门的CALayer子类添加了可以在动画中使用的其他属性。这篇文章就将把上篇文章的一些UIView动画改成Layer动画。

首先修改的是标题,usernameTextField 以及passwordTextField的进场效果。 这里创建一个CABasicAnimation,然后设置其fromValue,toValue以及间隔,然后设置不同的开始时间。这里记得把viewDidAppear里面对这三个视图的center.x 的处理去掉。

let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
titleLabel.layer.add(flyRight, forKey: nil)

flyRight.beginTime = CACurrentMediaTime() + 0.3
flyRight.fillMode = .both
usernameTextField.layer.add(flyRight, forKey: nil)

flyRight.beginTime = CACurrentMediaTime() + 0.4
passwordTextField.layer.add(flyRight, forKey: nil)

接着处理cloud的透明度变化的动画,把在viewWillAppear处理could alpha的代码去掉,然后去掉viewDidAppear对cloud透明度的动画,之后添加代码:

  let fadeIn = CABasicAnimation(keyPath: "opacity")
    fadeIn.fromValue = 0.0
    fadeIn.toValue = 1.0
    fadeIn.duration = 0.5
    fadeIn.fillMode = .backwards
    fadeIn.beginTime = CACurrentMediaTime() + 0.5
    cloud1.layer.add(fadeIn, forKey: nil)

    fadeIn.beginTime = CACurrentMediaTime() + 0.7
    cloud2.layer.add(fadeIn, forKey: nil)

    fadeIn.beginTime = CACurrentMediaTime() + 0.9
    cloud3.layer.add(fadeIn, forKey: nil)

    fadeIn.beginTime = CACurrentMediaTime() + 1.1
    cloud4.layer.add(fadeIn, forKey: nil)

封装tintBackgroundColor和roundCorners来添加背景色和圆角的动画。

func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
  let tint = CABasicAnimation(keyPath: "backgroundColor")
  tint.fromValue = layer.backgroundColor
  tint.toValue = toColor.cgColor
  tint.duration = 0.5
  layer.add(tint, forKey: nil)
  layer.backgroundColor = toColor.cgColor
}

func roundCorners(layer: CALayer, toRadius: CGFloat) {
  let round = CABasicAnimation(keyPath: "cornerRadius")
  round.fromValue = layer.cornerRadius
  round.toValue = toRadius
  round.duration = 0.5
  layer.add(round, forKey: nil)
  layer.cornerRadius = toRadius
}

并且在handleLogin中添加

let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
roundCorners(layer: loginButton.layer, toRadius: 25.0)

以及在resetForm中添加

	let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
	tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
	roundCorners(layer: self.loginButton.layer, toRadius: 10.0)

CABasicAnimation各参数意义:

  • fromValue: 动画的开始值(Any类型, 根据动画不同可以是CGPoint、NSNumber等)
  • toValue: 动画的结束值, 和fromValue类似
  • beginTime: 动画的开始时间
  • duration : 动画的持续时间
  • repeatCount : 动画的重复次数
  • fillMode: 用于表示动画在开始和结束时的状态
    • kCAFillModeRemoved: 这个是fillMode的默认值,表示在到达beginTime时才显示动画的第一帧,动画结束时,删除CALayer做的变化
    • kCAFillModeBackwards: 无论是否到达beginTime,动画开始后,立刻显示动画的第一帧
    • kCAFillModeForwards: 在到达beginTime时显示动画的第一帧,在动画结束时,保持动画最后一帧的状态,直到动画被删除。
    • kCAFillModeBoth: 相当于kCAFillModeBackwards与kCAFillModeForwards的共同合集,即无论是否到达beginTime,动画开始后,立刻显示动画的第一帧,并且在动画结束时保持最后一帧的状态,直到动画被删除。
  • isRemovedOnCompletion: 完成后是否删除动画
  • autoreverses: 执行的动画按照原动画返回执行
  • path:关键帧动画中的执行路径
  • values: 关键帧动画中的关键点数组
  • animations: 组动画中的动画数组
  • delegate : 动画代理, 封装了动画的执行和结束方法
  • timingFunction: 控制动画的显示节奏, 系统提供五种值选择,分别是:
    • 1.kCAMediaTimingFunctionDefault( 默认,中间快)
    • 2.kCAMediaTimingFunctionLinear (线性动画)
    • 3.kCAMediaTimingFunctionEaseIn (先慢后快 慢进快出)
    • 4.kCAMediaTimingFunctionEaseOut (先块后慢快进慢出)
    • 5.kCAMediaTimingFunctionEaseInEaseOut (先慢后快再慢)
  • type: 过渡动画的动画类型,系统提供了多种过渡动画, 分别是:
    • 1: fade (淡出 默认)
    • 2: moveIn (覆盖原图)
    • 3: push (推出)
    • 4: fade (淡出 默认)
    • 5: reveal (底部显示出来)
    • 6: cube (立方旋转)
    • 7: suck (吸走)
    • 8: oglFlip (水平翻转 沿y轴)
    • 9: ripple (滴水效果)
    • 10: curl (卷曲翻页 向上翻页)
    • 11: unCurl (卷曲翻页返回 向下翻页)
    • 12: caOpen (相机开启)
    • 13: caClose (相机关闭)
  • subtype : 过渡动画的动画方向, 系统提供了四种,分别是:
    • 1.fromLeft( 从左侧)
    • 2.fromRight (从右侧)
    • 3.fromTop (有上面)
    • 4.fromBottom (从下面)

完整代码:

import UIKit

class ViewController: UIViewController {
        
    let screenWidth = UIScreen.main.bounds.size.width
    let screenHeight = UIScreen.main.bounds.size.height
    let titleLabel = UILabel()
    let backgroundImage = UIImageView()
    let usernameTextField = TextField()
    let passwordTextField = TextField()
    let loginButton = UIButton()
    let cloud1 = UIImageView()
    let cloud2 = UIImageView()
    let cloud3 = UIImageView()
    let cloud4 = UIImageView()
    let spinner = UIActivityIndicatorView(style: .whiteLarge)
    
    let status = UIImageView(image: UIImage(named: "banner"))
    let label = UILabel()
    let messages = ["Connecting ...", "Authorizing ...", "Sending credentials ...", "Failed"]

    var statusPosition = CGPoint.zero


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        view.addSubview(backgroundImage)
        view.addSubview(titleLabel)
        view.addSubview(usernameTextField)
        view.addSubview(passwordTextField)
        view.addSubview(loginButton)
        view.addSubview(cloud1)
        view.addSubview(cloud2)
        view.addSubview(cloud3)
        view.addSubview(cloud4)
        loginButton.addSubview(spinner)
        
        let textFieldWidth = screenWidth - 60
        let buttonWidth = 260
        
        backgroundImage.image = UIImage(named: "bg-sunny")
        backgroundImage.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
      
        titleLabel.text = "Bahama Login"
        titleLabel.textColor = .white
        titleLabel.font = UIFont.systemFont(ofSize: 28)
        let titleWidth = titleLabel.intrinsicContentSize.width
        titleLabel.frame = CGRect(x: (screenWidth - titleWidth) / 2 , y: 120, width: titleWidth, height: titleLabel.intrinsicContentSize.height)
        
        usernameTextField.backgroundColor = .white
        usernameTextField.layer.cornerRadius = 5
        usernameTextField.placeholder = "  Username"
        usernameTextField.frame = CGRect(x: 30, y: 202, width: textFieldWidth, height: 40)
        
        passwordTextField.backgroundColor = .white
        passwordTextField.layer.cornerRadius = 5
        passwordTextField.placeholder = "  Password"
        passwordTextField.frame = CGRect(x: 30, y: 263, width: textFieldWidth, height: 40)
        
        loginButton.frame = CGRect(x: (Int(screenWidth) - buttonWidth) / 2, y: 343, width: buttonWidth, height: 50)
        loginButton.setTitle("Login", for: .normal)
        loginButton.setTitleColor(.red, for: .normal)
        loginButton.layer.cornerRadius = 5
        loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
        loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
        
        spinner.frame = CGRect(x: -20.0, y: 6.0, width: 20.0, height: 20.0)
        spinner.startAnimating()
        spinner.alpha = 0.0
        
        
        cloud1.frame = CGRect(x: -120, y: 79, width: 160, height: 50)
        cloud1.image = UIImage(named: "bg-sunny-cloud-1")
        
        cloud2.frame = CGRect(x: 256, y: 213, width: 160, height: 50)
        cloud2.image = UIImage(named: "bg-sunny-cloud-2")
        
        
        cloud3.frame = CGRect(x: 284, y: 503, width: 74, height: 35)
        cloud3.image = UIImage(named: "bg-sunny-cloud-3")
        
        
        cloud4.frame = CGRect(x:22 , y: 545, width: 115, height: 50)
        cloud4.image = UIImage(named: "bg-sunny-cloud-4")
        
        status.isHidden = true
        status.center = loginButton.center
        view.addSubview(status)

        label.frame = CGRect(x: 0.0, y: 0.0, width: status.frame.size.width, height: status.frame.size.height)
        label.font = UIFont(name: "HelveticaNeue", size: 18.0)
        label.textColor = UIColor(red: 0.89, green: 0.38, blue: 0.0, alpha: 1.0)
        label.textAlignment = .center
        status.addSubview(label)

        statusPosition = status.center
        
       
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

       
        
        let flyRight = CABasicAnimation(keyPath: "position.x")
        flyRight.fromValue = -view.bounds.size.width/2
        flyRight.toValue = view.bounds.size.width/2
        flyRight.duration = 0.5
        titleLabel.layer.add(flyRight, forKey: nil)

        flyRight.beginTime = CACurrentMediaTime() + 0.3
        flyRight.fillMode = .both
        usernameTextField.layer.add(flyRight, forKey: nil)

        flyRight.beginTime = CACurrentMediaTime() + 0.4
        passwordTextField.layer.add(flyRight, forKey: nil)
        
        
     
//
//        cloud1.alpha = 0.0
//        cloud2.alpha = 0.0
//        cloud3.alpha = 0.0
//        cloud4.alpha = 0.0
        
        loginButton.center.y += 30.0
        loginButton.alpha = 0.0
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
//        UIView.animate(withDuration: 0.5) {
//            self.titleLabel.center.x += self.view.bounds.width
//        }
//        
//        UIView.animate(withDuration: 0.5, delay: 0.3, options: [], animations: {
//            self.usernameTextField.center.x += self.view.bounds.width
//        }, completion: nil
//        )
//        
//        UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
//            self.passwordTextField.center.x += self.view.bounds.width
//        }, completion: nil)
        
//        UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
//            self.cloud1.alpha = 1.0
//
//        }, completion: nil)
//        UIView.animate(withDuration: 0.5, delay: 0.7, options: [], animations: {
//            self.cloud2.alpha = 1.0
//
//        }, completion: nil)
//        UIView.animate(withDuration: 0.5, delay: 0.9, options: [], animations: {
//            self.cloud3.alpha = 1.0
//
//        }, completion: nil)
//        UIView.animate(withDuration: 0.5, delay: 1.1, options: [], animations: {
//            self.cloud4.alpha = 1.0
//
//        }, completion: nil)
        let fadeIn = CABasicAnimation(keyPath: "opacity")
        fadeIn.fromValue = 0.0
        fadeIn.toValue = 1.0
        fadeIn.duration = 0.5
        fadeIn.fillMode = .backwards
        fadeIn.beginTime = CACurrentMediaTime() + 0.5
        cloud1.layer.add(fadeIn, forKey: nil)

        fadeIn.beginTime = CACurrentMediaTime() + 0.7
        cloud2.layer.add(fadeIn, forKey: nil)

        fadeIn.beginTime = CACurrentMediaTime() + 0.9
        cloud3.layer.add(fadeIn, forKey: nil)

        fadeIn.beginTime = CACurrentMediaTime() + 1.1
        cloud4.layer.add(fadeIn, forKey: nil)
        UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5,
          initialSpringVelocity: 0.0,
          animations: {
            self.loginButton.center.y -= 30.0
            self.loginButton.alpha = 1.0
          },
          completion: nil
        )
        
        animateCloud(cloud1)
        animateCloud(cloud2)
        animateCloud(cloud3)
        animateCloud(cloud4)
    }
    
    @objc func handleLogin() {
        view.endEditing(true)
        
        UIView.animate(withDuration: 1.5, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: [], animations: {
          self.loginButton.bounds.size.width += 80.0
        }, completion: nil)
        
        UIView.animate(withDuration: 0.33, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: {
          self.loginButton.center.y += 60.0
          self.spinner.center = CGPoint(
            x: 40.0,
            y: self.loginButton.frame.size.height/2
          )
          self.spinner.alpha = 1.0
        }, completion: { _ in
            self.showMessage(index:0)
        })
        
        let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
        tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
        roundCorners(layer: loginButton.layer, toRadius: 25.0)
    }
    func showMessage(index: Int) {
      label.text = messages[index]

      UIView.transition(with: status, duration: 0.33, options: [.curveEaseOut, .transitionFlipFromTop], animations: {
        self.status.isHidden = false
      }, completion: { _ in
        //transition completion
        delay(2.0) {
          if index < self.messages.count-1 {
            self.removeMessage(index: index)

          } else {
            self.resetForm()
          }
        }
      })
    }
    
    func removeMessage(index: Int) {
      UIView.animate(withDuration: 0.33, delay: 0.0, options: [], animations: {
        self.status.center.x += self.view.frame.size.width
      }, completion: { _ in
        self.status.isHidden = true
        self.status.center = self.statusPosition

        self.showMessage(index: index+1)
      })
    }
    
    func resetForm() {
      UIView.transition(with: status, duration: 0.2, options: .transitionFlipFromTop, animations: {
        self.status.isHidden = true
        self.status.center = self.statusPosition
      }, completion: { _ in
        let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
        tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
        roundCorners(layer: self.loginButton.layer, toRadius: 10.0)
      })

      UIView.animate(withDuration: 0.2, delay: 0.0, options: [], animations: {
        self.spinner.center = CGPoint(x: -20.0, y: 16.0)
        self.spinner.alpha = 0.0
        self.loginButton.bounds.size.width -= 80.0
        self.loginButton.center.y -= 60.0
      }, completion: nil)
    }
    
    func animateCloud(_ cloud: UIImageView) {
      let cloudSpeed = 60.0 / view.frame.size.width
      let duration = (view.frame.size.width - cloud.frame.origin.x) * cloudSpeed
      UIView.animate(withDuration: TimeInterval(duration), delay: 0.0, options: .curveLinear, animations: {
        cloud.frame.origin.x = self.view.frame.size.width
      }, completion: { _ in
        cloud.frame.origin.x = -cloud.frame.size.width
        self.animateCloud(cloud)
      })
    }

}

func delay(_ seconds: Double, completion: @escaping ()->Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion)
}

func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
  let tint = CABasicAnimation(keyPath: "backgroundColor")
  tint.fromValue = layer.backgroundColor
  tint.toValue = toColor.cgColor
  tint.duration = 0.5
  layer.add(tint, forKey: nil)
  layer.backgroundColor = toColor.cgColor
}

func roundCorners(layer: CALayer, toRadius: CGFloat) {
  let round = CABasicAnimation(keyPath: "cornerRadius")
  round.fromValue = layer.cornerRadius
  round.toValue = toRadius
  round.duration = 0.5
  layer.add(round, forKey: nil)
  layer.cornerRadius = toRadius
}

class TextField: UITextField {

    let padding = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)

    override open func textRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }

    override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }

    override open func editingRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }
}