本文主要是介绍系统学习iOS动画 —— 动画组, 时间控制, 图层弹簧动画,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1. 动画组和时间控制
动画组可以对动画进行分组,可以向组中添加多个动画并同时调整持续时间,代理和timingFunction等属性。 对动画进行分组会产生简化的代码,并确保所有动画将作为一个实体单元同步。
这里创建一个CAAnimationGroup
let formGroup = CAAnimationGroup()formGroup.duration = 0.5formGroup.fillMode = .backwards
然后创建一个向右移动的动画和变化透明度的动画
let flyRight = CABasicAnimation(keyPath: "position.x")flyRight.fromValue = -view.bounds.size.width/2flyRight.toValue = view.bounds.size.width/2let fadeFieldIn = CABasicAnimation(keyPath: "opacity")fadeFieldIn.fromValue = 0.25fadeFieldIn.toValue = 1.0
为formGroup的animations赋值
formGroup.animations = [flyRight, fadeFieldIn]
然后为控件添加上这个动画
heading.layer.add(formGroup, forKey: nil)formGroup.delegate = selfformGroup.setValue("form", forKey: "name")formGroup.setValue(username.layer, forKey: "layer")formGroup.beginTime = CACurrentMediaTime() + 0.3username.layer.add(formGroup, forKey: nil)formGroup.setValue(password.layer, forKey: "layer")formGroup.beginTime = CACurrentMediaTime() + 0.4password.layer.add(formGroup, forKey: nil)
接下来为登陆按钮添加动画组
let groupAnimation = CAAnimationGroup()groupAnimation.beginTime = CACurrentMediaTime() + 0.5groupAnimation.duration = 0.5groupAnimation.fillMode = .backwardslet scaleDown = CABasicAnimation(keyPath: "transform.scale")scaleDown.fromValue = 3.5scaleDown.toValue = 1.0let rotate = CABasicAnimation(keyPath: "transform.rotation")rotate.fromValue = .pi / 4.0rotate.toValue = 0.0let fade = CABasicAnimation(keyPath: "opacity")fade.fromValue = 0.0fade.toValue = 1.0groupAnimation.animations = [scaleDown, rotate, fade]groupAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)loginButton.layer.add(groupAnimation, forKey: nil)
2. 图层弹簧动画
下面解释来自于Andy_Ron
2.1 阻尼谐振子
阻尼谐振子,Damped harmonic oscillators(直译就是,逐渐衰弱的振荡器),可以理解为逐渐衰减的振动。
UIKit API简化了弹簧动画的制作,不需要了解它们的原理就可以很方便的使用。 但是,由于您现在是核心动画专家,因此您需要深入研究细节。
钟摆,理想状况下钟摆是不停的摆动,像下面的一样:
对应的运动轨迹图就像:
但现实中由于能量的损耗,钟摆的摇摆的幅度会逐渐减小:
对应的运动轨迹:
这就是一个阻尼谐振子 。
钟摆停下来所需的时间长度,以及最终振荡器图形的方式取决于振荡系统的以下参数:
-
阻尼(damping):由于空气摩擦、机械摩擦和其他作用在系统上的外部减速力。
-
质量(mass):摆锤越重,摆动的时间越长。
-
刚度(stiffness):振荡器的“弹簧”越硬(钟摆的“弹簧”是指地球的引力),钟摆摆动越困难,系统停下来也越快。想象一下,如果在月球或木星上使用这个钟摆;在低重力和高重力情况下的运动将是完全不同的。
-
初始速度(initial velocity):推一下钟摆。
2.2 视图弹簧动画 vs 图层弹簧动画
UIKit以动态方式调整所有其他变量,使系统在给定的持续时间内稳定下来。 这就是为什么UIKit弹簧动画有时有点被迫 停下来的感觉。 如果仔细观察会发现UIKit动画有点不太自然。
幸运的是,核心允许通过CASpringAnimation类为图层属性创建合适的弹簧动画。 CASpringAnimation在幕后为UIKit创建弹簧动画,但是当我们直接调用它时,可以设置系统的各种变量,让动画自己稳定下来。 这种方法的缺点是不能设置固定的持续时间(duration);持续时间取决于提供的其它变量,然后系统计算所得。
CASpringAnimation的一些属性(对应之前振荡系统的参数):
- damping 阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快
- mass 质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大
- stiffness 刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快
- initialVelocity 初始速率,动画视图的初始速度大小。速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反
2.3 图层弹簧动画应用
将之前在animationDidStop的动画
let pulse = CABasicAnimation(keyPath: "transform.scale")pulse.fromValue = 1.25pulse.toValue = 1.0pulse.duration = 0.25layer?.add(pulse, forKey: nil)
修改为
let pulse = CASpringAnimation(keyPath: "transform.scale")pulse.damping = 7.5pulse.fromValue = 1.25pulse.toValue = 1.0pulse.duration = pulse.settlingDurationlayer?.add(pulse, forKey: nil)
运行后发现动画明显自然很多
继续将其他的动画也修改好:
将tintBackgroundColor里面的动画改为:
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {let tint = CASpringAnimation(keyPath: "backgroundColor")tint.damping = 5.0tint.initialVelocity = -10.0tint.fromValue = layer.backgroundColortint.toValue = toColor.cgColortint.duration = tint.settlingDurationlayer.add(tint, forKey: nil)layer.backgroundColor = toColor.cgColor
}
将roundCorners里面的动画改为:
func roundCorners(layer: CALayer, toRadius: CGFloat) {let round = CASpringAnimation(keyPath: "cornerRadius")round.damping = 5.0round.fromValue = layer.cornerRadiusround.toValue = toRadiusround.duration = round.settlingDurationlayer.add(round, forKey: nil)layer.cornerRadius = toRadius
}
在textFieldDidEndEditing里面添加判断,如果输入小于5,那么就添加动画。
func textFieldDidEndEditing(_ textField: UITextField) {guard let text = textField.text else { return }if text.count < 5 {// add animations herelet jump = CASpringAnimation(keyPath: "position.y")jump.initialVelocity = 100.0jump.mass = 10.0jump.stiffness = 1500.0jump.damping = 50.0jump.fromValue = textField.layer.position.y + 1.0jump.toValue = textField.layer.position.yjump.duration = jump.settlingDurationtextField.layer.add(jump, forKey: nil)textField.layer.borderWidth = 3.0textField.layer.borderColor = UIColor.clear.cgColorlet flash = CASpringAnimation(keyPath: "borderColor")flash.damping = 7.0flash.stiffness = 200.0flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColorflash.toValue = UIColor.white.cgColorflash.duration = flash.settlingDurationtextField.layer.add(flash, forKey: nil)textField.layer.cornerRadius = 5}}
2.4 弹簧动画属性
CASpringAnimation预定义的弹簧动画属性的默认值分别是:
damping: 10.0
mass: 1.0
stiffness: 100.0
initialVelocity: 0.0
initialVelocity
: 附着在弹簧上的物体的初始速度。默认为零,表示一个不动的对象。负值表示对象远离弹簧附着点,正值表示对象向弹簧附着点移动。mass
:附着在弹簧末端的物体的质量。必须大于0。默认为1。数值越大,对象跳跃的更高,并且稳定下来的时间更久了。stiffness
: 弹簧刚度系数。必须大于0。默认为100。让跳跃高度降低。damping
: 阻尼系数。必须大于或等于0,默认为10。增加阻尼系数可以让动画更快地稳定下来。
完整代码:
//
// ViewController.swift
// AirLoginAnimation
//
// Created by aibus on 2021/10/27.
//import UIKitclass ViewController: UIViewController {let screenWidth = UIScreen.main.bounds.size.widthlet screenHeight = UIScreen.main.bounds.size.heightlet 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: UIActivityIndicatorView.Style.large)let status = UIImageView(image: UIImage(named: "banner"))let label = UILabel()let info = UILabel()let messages = ["Connecting ...", "Authorizing ...", "Sending credentials ...", "Failed"]var statusPosition = CGPoint.zerooverride 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 - 60let buttonWidth = 260backgroundImage.image = UIImage(named: "bg-sunny")backgroundImage.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)titleLabel.text = "Bahama Login"titleLabel.textColor = .whitetitleLabel.font = UIFont.systemFont(ofSize: 28)let titleWidth = titleLabel.intrinsicContentSize.widthtitleLabel.frame = CGRect(x: (screenWidth - titleWidth) / 2 , y: 120, width: titleWidth, height: titleLabel.intrinsicContentSize.height)usernameTextField.backgroundColor = .whiteusernameTextField.layer.cornerRadius = 5usernameTextField.placeholder = " Username"usernameTextField.delegate = selfusernameTextField.frame = CGRect(x: 30, y: 202, width: textFieldWidth, height: 40)passwordTextField.backgroundColor = .whitepasswordTextField.layer.cornerRadius = 5passwordTextField.placeholder = " Password"passwordTextField.delegate = selfpasswordTextField.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 = 5loginButton.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.0cloud1.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 = truestatus.center = loginButton.centerview.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 = .centerstatus.addSubview(label)statusPosition = status.centerinfo.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)info.backgroundColor = UIColor.clearinfo.font = UIFont(name: "HelveticaNeue", size: 12.0)info.textAlignment = .centerinfo.textColor = UIColor.whiteinfo.text = "Tap on a field and enter username and password"view.insertSubview(info, belowSubview: loginButton)}override func viewWillAppear(_ animated: Bool) {super.viewWillAppear(animated)let formGroup = CAAnimationGroup()formGroup.duration = 0.5formGroup.fillMode = .backwardslet flyRight = CABasicAnimation(keyPath: "position.x")flyRight.fromValue = -view.bounds.size.width/2flyRight.toValue = view.bounds.size.width/2let fadeFieldIn = CABasicAnimation(keyPath: "opacity")fadeFieldIn.fromValue = 0.25fadeFieldIn.toValue = 1.0formGroup.animations = [flyRight, fadeFieldIn]titleLabel.layer.add(formGroup, forKey: nil)formGroup.delegate = selfformGroup.setValue("form", forKey: "name")formGroup.setValue(usernameTextField.layer, forKey: "layer")formGroup.beginTime = CACurrentMediaTime() + 0.3usernameTextField.layer.add(formGroup, forKey: nil)formGroup.setValue(passwordTextField.layer, forKey: "layer")formGroup.beginTime = CACurrentMediaTime() + 0.4passwordTextField.layer.add(formGroup, forKey: nil)}override func viewDidAppear(_ animated: Bool) {super.viewDidAppear(animated)let fadeIn = CABasicAnimation(keyPath: "opacity")fadeIn.fromValue = 0.0fadeIn.toValue = 1.0fadeIn.duration = 0.5fadeIn.fillMode = .backwardsfadeIn.beginTime = CACurrentMediaTime() + 0.5cloud1.layer.add(fadeIn, forKey: nil)fadeIn.beginTime = CACurrentMediaTime() + 0.7cloud2.layer.add(fadeIn, forKey: nil)fadeIn.beginTime = CACurrentMediaTime() + 0.9cloud3.layer.add(fadeIn, forKey: nil)fadeIn.beginTime = CACurrentMediaTime() + 1.1cloud4.layer.add(fadeIn, forKey: nil)let groupAnimation = CAAnimationGroup()groupAnimation.beginTime = CACurrentMediaTime() + 0.5groupAnimation.duration = 0.5groupAnimation.fillMode = .backwardslet scaleDown = CABasicAnimation(keyPath: "transform.scale")scaleDown.fromValue = 3.5scaleDown.toValue = 1.0let rotate = CABasicAnimation(keyPath: "transform.rotation")rotate.fromValue = .pi / 4.0rotate.toValue = 0.0let fade = CABasicAnimation(keyPath: "opacity")fade.fromValue = 0.0fade.toValue = 1.0groupAnimation.animations = [scaleDown, rotate, fade]groupAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)loginButton.layer.add(groupAnimation, forKey: 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.widthflyLeft.toValue = info.layer.position.xflyLeft.duration = 5.0info.layer.add(flyLeft, forKey: "infoappear")let fadeLabelIn = CABasicAnimation(keyPath: "opacity")fadeLabelIn.fromValue = 0.2fadeLabelIn.toValue = 1.0fadeLabelIn.duration = 4.5info.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.0self.spinner.center = CGPoint(x: 40.0,y: self.loginButton.frame.size.height/2)self.spinner.alpha = 1.0}, completion: { _ inself.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 completiondelay(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: { _ inself.status.isHidden = trueself.status.center = self.statusPositionself.showMessage(index: index+1)})}func resetForm() {UIView.transition(with: status, duration: 0.2, options: .transitionFlipFromTop, animations: {self.status.isHidden = trueself.status.center = self.statusPosition}, completion: { _ inlet 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.0self.loginButton.bounds.size.width -= 80.0self.loginButton.center.y -= 60.0}, completion: nil)}func animateCloud(layer: CALayer) {//1let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed//2let cloudMove = CABasicAnimation(keyPath: "position.x")cloudMove.duration = durationcloudMove.toValue = self.view.bounds.size.width + layer.bounds.width/2cloudMove.delegate = selfcloudMove.fillMode = .forwardscloudMove.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 = CASpringAnimation(keyPath: "backgroundColor")tint.damping = 5.0tint.initialVelocity = -10.0tint.fromValue = layer.backgroundColortint.toValue = toColor.cgColortint.duration = tint.settlingDurationlayer.add(tint, forKey: nil)layer.backgroundColor = toColor.cgColor
}func roundCorners(layer: CALayer, toRadius: CGFloat) {let round = CASpringAnimation(keyPath: "cornerRadius")round.damping = 5.0round.fromValue = layer.cornerRadiusround.toValue = toRadiusround.duration = round.settlingDurationlayer.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 foundlet layer = anim.value(forKey: "layer") as? CALayeranim.setValue(nil, forKey: "layer")let pulse = CASpringAnimation(keyPath: "transform.scale")pulse.damping = 7.5pulse.fromValue = 1.25pulse.toValue = 1.0pulse.duration = pulse.settlingDurationlayer?.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/2delay(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")}func textFieldDidEndEditing(_ textField: UITextField) {guard let text = textField.text else { return }if text.count < 5 {// add animations herelet jump = CASpringAnimation(keyPath: "position.y")jump.initialVelocity = 100.0jump.mass = 10.0jump.stiffness = 1500.0jump.damping = 50.0jump.fromValue = textField.layer.position.y + 1.0jump.toValue = textField.layer.position.yjump.duration = jump.settlingDurationtextField.layer.add(jump, forKey: nil)textField.layer.borderWidth = 3.0textField.layer.borderColor = UIColor.clear.cgColorlet flash = CASpringAnimation(keyPath: "borderColor")flash.damping = 7.0flash.stiffness = 200.0flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColorflash.toValue = UIColor.white.cgColorflash.duration = flash.settlingDurationtextField.layer.add(flash, forKey: nil)textField.layer.cornerRadius = 5}}
}
这篇关于系统学习iOS动画 —— 动画组, 时间控制, 图层弹簧动画的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!