系统学习iOS动画—— BahamaAirLogin(动画的Keys和代理)
关于视图动画和相应的闭包语法的一个棘手问题是,一旦您创建并运行视图动画,您就无法暂停,停止或以任何方式访问它。
但是,使用核心动画,您可以轻松检查在图层上运行的动画,并在需要时停止它们。 此外,您甚至可以在动画上设置委托对象并对动画事件做出反应。
接着上一篇文章的工程,这里先添加一个属性info。
let info = UILabel()
在viewDidLoad里面设置好info的属性
info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)
info.backgroundColor = UIColor.clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = UIColor.white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)
在viewDidAppear里面为info添加一个位置变化以及透明度变化的动画。
let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x + view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")
let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")
接下来我们想要在点击的usernameTextField或者passwordTextField把info的动画移除掉,那么我们就要感知到usernameTextField和passwordTextField被点击了,这里使用UITextFieldDelegate里面的textFieldDidBeginEditing来实现。
在viewDidLoad里面将usernameTextField和passwordTextField 的delegate设为self。
usernameTextField.delegate = self
passwordTextField.delegate = self
然后在textFieldDidBeginEditing回调中移除info的动画。
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let runningAnimations = info.layer.animationKeys() else {
return
}
print(runningAnimations)
info.layer.removeAnimation(forKey: "infoappear")
info.layer.removeAnimation(forKey: "fadein")
}
我们想要标题在移动的动画完成后做一个缩放的动画,我们当然可以使用 DispatchQueue.main.asyncAfter来实现,但是实际上如果用CAAnimationDelegate的话会更好
将flyRight的delegate设为self。
flyRight.delegate = self
遵守CAAnimationDelegate协议然后使用animationDidStop来完成想要的效果:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation,
finished flag: Bool) {
print("animation did finish")
guard let name = anim.value(forKey: "name") as? String else {
return
}
if name == "form" {
//form field found
let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
}
}
}
那么也可以修改animateCloud,让其使用layer animation 结合animationDidStop来达成动画效果,而不用在UIView 的completion block里面重新调用animateCloud执行动画。
animateCloud修改成:
func animateCloud(layer: CALayer) {
//1
let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed
//2
let cloudMove = CABasicAnimation(keyPath: "position.x")
cloudMove.duration = duration
cloudMove.toValue = self.view.bounds.size.width + layer.bounds.width/2
cloudMove.delegate = self
cloudMove.fillMode = .forwards
cloudMove.setValue("cloud", forKey: "name")
cloudMove.setValue(layer, forKey: "layer")
layer.add(cloudMove, forKey: nil)
}
animationDidStop里面添加:
if name == "cloud" {
if let layer = anim.value(forKey: "layer") as? CALayer {
anim.setValue(nil, forKey: "layer")
layer.position.x = -layer.bounds.width/2
delay(0.5) {
self.animateCloud(layer: layer)
}
}
}
完整代码:
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 info = 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.delegate = self
usernameTextField.frame = CGRect(x: 30, y: 202, width: textFieldWidth, height: 40)
passwordTextField.backgroundColor = .white
passwordTextField.layer.cornerRadius = 5
passwordTextField.placeholder = " Password"
passwordTextField.delegate = self
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
info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)
info.backgroundColor = UIColor.clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = UIColor.white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)
}
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)
loginButton.center.y += 30.0
loginButton.alpha = 0.0
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
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(layer: cloud1.layer)
animateCloud(layer: cloud2.layer)
animateCloud(layer: cloud3.layer)
animateCloud(layer: cloud4.layer)
let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x + view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")
let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")
}
@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(layer: CALayer) {
//1
let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed
//2
let cloudMove = CABasicAnimation(keyPath: "position.x")
cloudMove.duration = duration
cloudMove.toValue = self.view.bounds.size.width + layer.bounds.width/2
cloudMove.delegate = self
cloudMove.fillMode = .forwards
cloudMove.setValue("cloud", forKey: "name")
cloudMove.setValue(layer, forKey: "layer")
layer.add(cloudMove, forKey: nil)
}
}
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)
}
}
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation,
finished flag: Bool) {
print("animation did finish")
guard let name = anim.value(forKey: "name") as? String else {
return
}
if name == "form" {
//form field found
let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
}
if name == "cloud" {
if let layer = anim.value(forKey: "layer") as? CALayer {
anim.setValue(nil, forKey: "layer")
layer.position.x = -layer.bounds.width/2
delay(0.5) {
self.animateCloud(layer: layer)
}
}
}
}
}
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let runningAnimations = info.layer.animationKeys() else {
return
}
print(runningAnimations)
info.layer.removeAnimation(forKey: "infoappear")
info.layer.removeAnimation(forKey: "fadein")
}
}