系统学习iOS动画—— Flight Info(keyframe-animations)

这是要达成的效果:
请添加图片描述
先添加所需要的部件:

class ViewController: UIViewController {
  
   
    let screenWidth = UIScreen.main.bounds.size.width
    let screenHeight = UIScreen.main.bounds.size.height
    
    let backgroundImageView = UIImageView()
    let summaryIcon = UIImageView()
    let summaryLabel = UILabel()
    let flightLabel = UILabel()
    let gateLabel = UILabel()
    let flightInfoLabel = UILabel()
    let gateInfoLabel = UILabel()
    let departingLabel = UILabel()
    let arrivingLabel = UILabel()
    let planeImageView = UIImageView()
    let flightStatusLabel = UILabel()
    let statusBanner = UIImageView()
    

    override func viewDidLoad() {
        super.viewDidLoad()
        let blackView = UIView()
        blackView.backgroundColor = .black
        blackView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: 80)
        // Do any additional setup after loading the view.
        view.addSubview(backgroundImageView)
        view.addSubview(blackView)
        view.addSubview(summaryIcon)
        view.addSubview(summaryLabel)
        view.addSubview(flightLabel)
        view.addSubview(gateLabel)
        view.addSubview(flightInfoLabel)
        view.addSubview(gateInfoLabel)
        view.addSubview(departingLabel)
        view.addSubview(planeImageView)
        view.addSubview(arrivingLabel)
        view.addSubview(statusBanner)
        view.addSubview(flightStatusLabel)
 
        
        backgroundImageView.image = UIImage(named: "bg-snowy")
        backgroundImageView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
        
        summaryIcon.image = UIImage(named: "icon-blue-arrow")
        summaryIcon.frame = CGRect(x: 70, y: 44, width: 18, height: 18)
        
        summaryLabel.font = UIFont.systemFont(ofSize: 18)
        summaryLabel.frame = CGRect(x: 0, y: 44, width: screenWidth, height: 20)
        summaryLabel.textColor = .white
        summaryLabel.textAlignment = .center
        
        flightLabel.text = "Flight"
        flightLabel.font = UIFont.systemFont(ofSize: 18)
        flightLabel.frame = CGRect(x: 42, y: 91, width: screenWidth, height: 25)
        flightLabel.textColor = UIColor.white.withAlphaComponent(0.8)
        
        gateLabel.text = "Gate"
        gateLabel.font = UIFont.systemFont(ofSize: 18)
        gateLabel.frame = CGRect(x: screenWidth - 75, y: 91, width: screenWidth, height: 25)
        gateLabel.textColor = UIColor.white.withAlphaComponent(0.8)
        
        flightInfoLabel.font = UIFont.systemFont(ofSize: 24)
        flightInfoLabel.frame = CGRect(x: 23, y: 137, width: screenWidth, height: 25)
        flightInfoLabel.textColor = .white
        
        gateInfoLabel.font = UIFont.systemFont(ofSize: 24)
        gateInfoLabel.frame = CGRect(x: screenWidth - 100, y: 137, width: screenWidth, height: 25)
        gateInfoLabel.textColor = .white
        
        departingLabel.font = UIFont.systemFont(ofSize: 30)
        departingLabel.frame = CGRect(x: 23, y: 341, width: screenWidth, height: 25)
        departingLabel.textColor = .systemYellow
        
        arrivingLabel.font = UIFont.systemFont(ofSize: 30)
        arrivingLabel.frame = CGRect(x: screenWidth - 100, y: 341, width: screenWidth, height: 25)
        arrivingLabel.textColor = .systemYellow
        
        planeImageView.image = UIImage(named: "plane")
        planeImageView.frame = CGRect(x: view.center.x - 50, y: 341, width: 88, height: 35)
        
        statusBanner.image = UIImage(named: "banner")
        statusBanner.frame = CGRect(x: view.center.x - 95, y: 526, width: 191, height: 50)
        
        flightStatusLabel.font = UIFont.systemFont(ofSize: 30)
     
        flightStatusLabel.frame = CGRect(x: view.center.x - 95, y: 526, width: screenWidth, height: 50)
        flightStatusLabel.center = statusBanner.center
        flightStatusLabel.textAlignment = .center
        flightStatusLabel.textColor = .brown
    }
}

然后创建一个FlightData 并且创建两个model以方便后面给label等赋值。

struct FlightData {
  let summary: String
  let flightNr: String
  let gateNr: String
  let departingFrom: String
  let arrivingTo: String
  let weatherImageName: String
  let showWeatherEffects: Bool
  let isTakingOff: Bool
  let flightStatus: String
}

//
// Pre- defined flights
//

let londonToParis = FlightData(
  summary: "01 Apr 2015 09:42",
  flightNr: "ZY 2014",
  gateNr: "T1 A33",
  departingFrom: "LGW",
  arrivingTo: "CDG",
  weatherImageName: "bg-snowy",
  showWeatherEffects: true,
  isTakingOff: true,
  flightStatus: "Boarding")

let parisToRome = FlightData(
  summary: "01 Apr 2015 17:05",
  flightNr: "AE 1107",
  gateNr: "045",
  departingFrom: "CDG",
  arrivingTo: "FCO",
  weatherImageName: "bg-sunny",
  showWeatherEffects: false,
  isTakingOff: false,
  flightStatus: "Delayed")

创建一个changeFlight方法,在这里面为label赋值

   func changeFlight(to data: FlightData, animated: Bool = false) {
        summaryLabel.text = data.summary
        backgroundImageView.image =  UIImage(named: data.weatherImageName)
        flightInfoLabel.text = data.flightNr
        gateInfoLabel.text = data.gateNr
        departingLabel.text = data.departingFrom
        arrivingLabel.text = data.arrivingTo
        flightStatusLabel.text = data.flightStatus
    }

这样运行后就得到:
在这里插入图片描述
接下来要处理动画。首先处理背景图等转换,这里使用UIView.transition方法来实现,UIView.transition主要用来添加视图转换动画,如翻页效果等。添加一个fade方法来进行背景图等转换。

  func fade(imageView: UIImageView, toImage: UIImage, showEffects: Bool) {
      UIView.transition(with: imageView, duration: 1.0, options: .transitionCrossDissolve, animations: {
        imageView.image = toImage
      }, completion: nil)

    }

在changeFlight中添加判断,并且这里调用delay方法来进行每三秒一次的转换。这个方法在第一篇的文章中有,实际上就是 DispatchQueue.main.asyncAfter的封装。

  func changeFlight(to data: FlightData, animated: Bool = false) {
        summaryLabel.text = data.summary
        
        if animated {
            fade(imageView: backgroundImageView,
                 toImage: UIImage(named: data.weatherImageName)!,
                 showEffects: data.showWeatherEffects)
        } else {
            backgroundImageView.image =  UIImage(named: data.weatherImageName)
            flightInfoLabel.text = data.flightNr
            gateInfoLabel.text = data.gateNr
            departingLabel.text = data.departingFrom
            arrivingLabel.text = data.arrivingTo
            flightStatusLabel.text = data.flightStatus
        }
    }
 delay(seconds: 3.0) {
      self.changeFlight(to: data.isTakingOff ? parisToRome : londonToParis, animated: true)
    }

这样背景图等切换就做好了。接下来要做航班号、登机口号和航班状态的动画。
创建一个cubeTransition方法来实现动画,在fade方法后面添加下面的代码。这里的direction是用来判断向上还是向下的动画,然后后面则是cubeTransition方法的调用。

let direction: AnimationDirection = data.isTakingOff ? .positive : .negative

cubeTransition(label: flightInfoLabel, text: data.flightNr, direction: direction)
cubeTransition(label: gateInfoLabel, text: data.gateNr, direction: direction)
cubeTransition(label: flightStatus, text: data.flightStatus,  direction: direction)

在cubeTransition方法里面,需要创建一个临时label,将其text设为传进来的值,通过transform改变其y的大小,为其添加从小变大的动画,而为传进来的label添加一个从大到小的动画。动画结束后,将label 的text设为传进来的值,然后大小还原,移除临时label。

 func cubeTransition(label: UILabel, text: String, direction: AnimationDirection) {
      let auxLabel = UILabel(frame: label.frame)
      auxLabel.text = text
      auxLabel.font = label.font
      auxLabel.textAlignment = label.textAlignment
      auxLabel.textColor = label.textColor
      auxLabel.backgroundColor = label.backgroundColor

      let auxLabelOffset = CGFloat(direction.rawValue) * label.frame.size.height/2.0
      auxLabel.transform = CGAffineTransform(translationX: 0.0, y: auxLabelOffset)
          .scaledBy(x: 1.0, y: 0.1)

      label.superview?.addSubview(auxLabel)

      UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: {
        auxLabel.transform = .identity
          label.transform = CGAffineTransform(translationX: 0.0, y: -auxLabelOffset)
            .scaledBy(x: 1.0, y: 0.1)
      }, completion: { _ in
        label.text = auxLabel.text
        label.transform = .identity

        auxLabel.removeFromSuperview()
      })
    }

然后为航班的出发和抵达label添加动画。创建一个moveLabel方法,这里和cubeTransition方法差不多,不过这里只是做位置的移动而不做大小的改变。

   func moveLabel(label: UILabel, text: String, offset: CGPoint) {
      let auxLabel = UILabel(frame: label.frame)
      auxLabel.text = text
      auxLabel.font = label.font
      auxLabel.textAlignment = label.textAlignment
      auxLabel.textColor = label.textColor
      auxLabel.backgroundColor = .clear

      auxLabel.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
      auxLabel.alpha = 0
      view.addSubview(auxLabel)

      UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
        label.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
        label.alpha = 0.0
      }, completion: nil)

      UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseIn, animations: {
        auxLabel.transform = .identity
        auxLabel.alpha = 1.0
      }, completion: {_ in
        //clean up
        auxLabel.removeFromSuperview()
        label.text = text
        label.alpha = 1.0
        label.transform = .identity
      })
    }

然后添加飞机的动画。创建一个planeDepart方法,这里面使用animateKeyframes方法,通过x变大以及y变小达到向右向上移动的效果,然后通过CGAffineTransform(rotationAngle: -.pi / 8)来变换图片的角度。 完了之后从新设置从屏幕的左边进入,最后回到原位。

func planeDepart() {
      let originalCenter = planeImageView.center

      UIView.animateKeyframes(withDuration: 1.5, delay: 0.0, animations: {
        //add keyframes
        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
          self.planeImageView.center.x += 80.0
          self.planeImageView.center.y -= 10.0
        })

        UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.4) {
          self.planeImageView.transform = CGAffineTransform(rotationAngle: -.pi / 8)
        }

        UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25) {
          self.planeImageView.center.x += 100.0
          self.planeImageView.center.y -= 50.0
          self.planeImageView.alpha = 0.0
        }

        UIView.addKeyframe(withRelativeStartTime: 0.51, relativeDuration: 0.01) {
          self.planeImageView.transform = .identity
          self.planeImageView.center = CGPoint(x: 0.0, y: originalCenter.y)
        }

        UIView.addKeyframe(withRelativeStartTime: 0.55, relativeDuration: 0.45) {
          self.planeImageView.alpha = 1.0
          self.planeImageView.center = originalCenter
        }
      }, completion: nil)
    }

最后添加summary的动画。简单的使用UIView.animateKeyframes添加一个向上向下的动画。

 func summarySwitch(to summaryText: String) {
      UIView.animateKeyframes(withDuration: 1.0, delay: 0.0, animations: {
        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.45) {
          self.summaryLabel.center.y -= 100.0
        }
        UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.45) {
          self.summaryLabel.center.y += 100.0
        }
      }, completion: nil)

      delay(seconds: 0.5) {
        self.summaryLabel.text = summaryText
      }
    }

接下来就差最后一个动画啦,也就是下雪的特效。下雪的特效需要用到CAEmitterLayer,CAEmitterLayer是QuartzCore提供的粒子引擎, 可用于制作美观的粒子特效。
代码

import QuartzCore

class SnowView: UIView {
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    let emitter = layer as! CAEmitterLayer
    emitter.emitterPosition = CGPoint(x: bounds.size.width / 2, y: 0)
    emitter.emitterSize = bounds.size
    emitter.emitterShape = .rectangle
    
    let emitterCell = CAEmitterCell()
    emitterCell.contents = UIImage(named: "flake.png")!.cgImage
    emitterCell.birthRate = 200
    emitterCell.lifetime = 3.5
    emitterCell.color = UIColor.white.cgColor
    emitterCell.redRange = 0.0
    emitterCell.blueRange = 0.1
    emitterCell.greenRange = 0.0
    emitterCell.velocity = 10
    emitterCell.velocityRange = 350
    emitterCell.emissionRange = CGFloat(Double.pi/2)
    emitterCell.emissionLongitude = CGFloat(-Double.pi)
    emitterCell.yAcceleration = 70
    emitterCell.xAcceleration = 0
    emitterCell.scale = 0.33
    emitterCell.scaleRange = 1.25
    emitterCell.scaleSpeed = -0.25
    emitterCell.alphaRange = 0.5
    emitterCell.alphaSpeed = -0.15
    
    emitter.emitterCells = [emitterCell]
  }
  
  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override class var layerClass: AnyClass {
    return CAEmitterLayer.self
  }
}

这样一个粒子发射器就完成啦。然后要添加到视图上。
声明一个snowView

  var snowView = SnowView(frame: CGRect(x: -150, y:-100, width: 300, height: 50))

在viewDidLoad里面添加一个snowClipView作为snowView的父view。

  let snowClipView = UIView(frame: view.frame.offsetBy(dx: 0, dy: 50))
    snowClipView.clipsToBounds = true
    snowClipView.addSubview(snowView)
    view.addSubview(snowClipView)

在changeFlight里面的else里面添加

snowView.isHidden = !data.showWeatherEffects

在fade方法里面添加

 UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
      self.snowView.alpha = showEffects ? 1.0 : 0.0
    }, completion: nil)

这样这个动画就完成啦。

CAEmitterLayer各参数作用:

  • emitterPosition:发射形状的中心。默认为(0,0,0)。
  • emitterSize:发射形状的大小。默认为(0,0,0),根据“发射形状”属性,某些值可能会被忽略。
  • emitterShape:发射形状类型,有:point (默认), line, rectangle, circle ,cuboid 和 sphere。

CAEmitterCell各参数作用:

  • contents:单元格内容,通常是CGImageRef,默认为nil。
  • birthRate:每秒创建的发射对象数。默认值为0。
  • lifetime:以秒为单位的每个发射对象的寿命,指定为平均值值和平均值的范围(lifetimeRange)。两个值都默认为零。
  • color:每个发射物体的平均颜色,“颜色”默认为不透明白色.
  • redRange, blueRange, greenRange:距离平均颜色的范围,默认为(0,0,0,0)。
  • velocity: 每个发射物体的初始平均速度,默认为0
  • velocityRange:每个发射物体的初始平均速度范围,默认为0.
  • emissionRange:定义发射角周围的圆锥的角度(以弧度为单位),发射的物体均匀地分布在这个圆锥上。
  • emissionLongitude:xy平面上与x轴的角度,通常称为方位角或θ,默认为零。
  • yAcceleration,xAcceleration:应用于发射对象的加速度矢量,默认为零。
  • scale:应用于每个发射对象的比例因子,定义为平均值,默认为1.
  • scaleRange:应用于每个发射对象的比例因子,平均值范围,默认为0.
  • scaleSpeed:每秒钟放大原始尺寸的比例。
  • alphaRange:alpha的范围。
  • alphaSpeed:每秒钟改变原始alpha的值。
  • emitterCells:发射器的数组
    完整代码
import UIKit
import QuartzCore

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

class ViewController: UIViewController {

  enum AnimationDirection: Int {
    case positive = 1
    case negative = -1
  }

  @IBOutlet var bgImageView: UIImageView!
  
  @IBOutlet var summaryIcon: UIImageView!
  @IBOutlet var summary: UILabel!
  
  @IBOutlet var flightNr: UILabel!
  @IBOutlet var gateNr: UILabel!
  @IBOutlet var departingFrom: UILabel!
  @IBOutlet var arrivingTo: UILabel!
  @IBOutlet var planeImage: UIImageView!
  
  @IBOutlet var flightStatus: UILabel!
  @IBOutlet var statusBanner: UIImageView!
  
  var snowView = SnowView(frame: CGRect(x: -150, y:-100, width: 300, height: 50))
  
  //MARK: view controller methods
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    //adjust ui
    summary.addSubview(summaryIcon)
    summaryIcon.center.y = summary.frame.size.height/2
    
    //add the snow effect layer
    
    let snowClipView = UIView(frame: view.frame.offsetBy(dx: 0, dy: 50))
    snowClipView.clipsToBounds = true
    snowClipView.addSubview(snowView)
    view.addSubview(snowClipView)
    
    //start rotating the flights
    changeFlight(to: londonToParis)
  }
  
  //MARK: custom methods
  
  func changeFlight(to data: FlightData, animated: Bool = false) {
    
    // populate the UI with the next flight's data
    if animated {
      fade(imageView: bgImageView,
           toImage: UIImage(named: data.weatherImageName)!,
           showEffects: data.showWeatherEffects)

      let direction: AnimationDirection = data.isTakingOff ? .positive : .negative

      cubeTransition(label: flightNr, text: data.flightNr, direction: direction)
      cubeTransition(label: gateNr, text: data.gateNr, direction: direction)

      let offsetDeparting = CGPoint(x: CGFloat(direction.rawValue * 80), y: 0.0)
      moveLabel(label: departingFrom, text: data.departingFrom, offset: offsetDeparting)

      let offsetArriving = CGPoint(x: 0.0, y: CGFloat(direction.rawValue * 50))
      moveLabel(label: arrivingTo, text: data.arrivingTo, offset: offsetArriving)

      cubeTransition(label: flightStatus, text: data.flightStatus,  direction: direction)

      planeDepart()
      summarySwitch(to: data.summary)

    } else {
      bgImageView.image = UIImage(named: data.weatherImageName)
      snowView.isHidden = !data.showWeatherEffects

      flightNr.text = data.flightNr
      gateNr.text = data.gateNr
      departingFrom.text = data.departingFrom
      arrivingTo.text = data.arrivingTo
      flightStatus.text = data.flightStatus

      summary.text = data.summary
    }
    
    // schedule next flight
    delay(seconds: 3.0) {
      self.changeFlight(to: data.isTakingOff ? parisToRome : londonToParis, animated: true)
    }
  }

  func fade(imageView: UIImageView, toImage: UIImage, showEffects: Bool) {
    UIView.transition(with: imageView, duration: 1.0, options: .transitionCrossDissolve, animations: {
      imageView.image = toImage
    }, completion: nil)

    UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
      self.snowView.alpha = showEffects ? 1.0 : 0.0
    }, completion: nil)
  }

  func cubeTransition(label: UILabel, text: String, direction: AnimationDirection) {
    let auxLabel = UILabel(frame: label.frame)
    auxLabel.text = text
    auxLabel.font = label.font
    auxLabel.textAlignment = label.textAlignment
    auxLabel.textColor = label.textColor
    auxLabel.backgroundColor = label.backgroundColor

    let auxLabelOffset = CGFloat(direction.rawValue) * label.frame.size.height/2.0
    auxLabel.transform = CGAffineTransform(translationX: 0.0, y: auxLabelOffset)
      .scaledBy(x: 1.0, y: 0.1)

    label.superview?.addSubview(auxLabel)

    UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: {
      auxLabel.transform = .identity
      label.transform = CGAffineTransform(translationX: 0.0, y: -auxLabelOffset)
        .scaledBy(x: 1.0, y: 0.1)
    }, completion: { _ in
      label.text = auxLabel.text
      label.transform = .identity

      auxLabel.removeFromSuperview()
    })
  }

  func moveLabel(label: UILabel, text: String, offset: CGPoint) {
    let auxLabel = UILabel(frame: label.frame)
    auxLabel.text = text
    auxLabel.font = label.font
    auxLabel.textAlignment = label.textAlignment
    auxLabel.textColor = label.textColor
    auxLabel.backgroundColor = UIColor.clear

    auxLabel.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
    auxLabel.alpha = 0
    view.addSubview(auxLabel)

    UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
      label.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
      label.alpha = 0.0
    }, completion: nil)

    UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseIn, animations: {
      auxLabel.transform = .identity
      auxLabel.alpha = 1.0
    }, completion: {_ in
      //clean up
      auxLabel.removeFromSuperview()
      label.text = text
      label.alpha = 1.0
      label.transform = .identity
    })
  }

  func planeDepart() {
    let originalCenter = planeImage.center

    UIView.animateKeyframes(withDuration: 1.5, delay: 0.0, animations: {
      //add keyframes
      UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
        self.planeImage.center.x += 80.0
        self.planeImage.center.y -= 10.0
      })

      UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.4) {
        self.planeImage.transform = CGAffineTransform(rotationAngle: -.pi / 8)
      }

      UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25) {
        self.planeImage.center.x += 100.0
        self.planeImage.center.y -= 50.0
        self.planeImage.alpha = 0.0
      }

      UIView.addKeyframe(withRelativeStartTime: 0.51, relativeDuration: 0.01) {
        self.planeImage.transform = .identity
        self.planeImage.center = CGPoint(x: 0.0, y: originalCenter.y)
      }

      UIView.addKeyframe(withRelativeStartTime: 0.55, relativeDuration: 0.45) {
        self.planeImage.alpha = 1.0
        self.planeImage.center = originalCenter
      }
    }, completion: nil)
  }

  func summarySwitch(to summaryText: String) {
    UIView.animateKeyframes(withDuration: 1.0, delay: 0.0, animations: {
      UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.45) {
        self.summary.center.y -= 100.0
      }
      UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.45) {
        self.summary.center.y += 100.0
      }
    }, completion: nil)

    delay(seconds: 0.5) {
      self.summary.text = summaryText
    }
  }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值