这是要达成的效果:
先添加所需要的部件:
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
}
}
}