ios - Stroke Animation with CABasicAnimation for UITextField
问题描述
I'm trying to add animation to the border of UITextField
when edited by the user.
The idea is to show line animation in the login page once the first text field is being edited and then after the user switches to the next textfield, the line should make a movement to the below text field.
I have made an attempt which is not working as what I'm expecting.
My code:
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var verticSpace: NSLayoutConstraint!
@IBOutlet weak var usernameTxtField: UITextField!
@IBOutlet weak var passwordTxtField: UITextField!
weak var shapeLayer: CAShapeLayer?
let path = UIBezierPath()
let shapeLayerNew = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
usernameTxtField.delegate = self
passwordTxtField.delegate = self
}
func textFieldDidBeginEditing(_ textField: UITextField) {
let path = UIBezierPath()
if textField == usernameTxtField {
if textField.text == "" {
self.startMyLine()
}
}
if passwordTxtField.isFirstResponder {
let path2 = UIBezierPath()
path2.move(to: CGPoint(x: usernameTxtField.frame.width, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))
path2.addLine(to: CGPoint(x: usernameTxtField.frame.width, y: (usernameTxtField.frame.height - shapeLayerNew.lineWidth) + passwordTxtField.frame.height + verticSpace.constant))
path2.addLine(to: CGPoint(x: 0, y: (usernameTxtField.frame.height - shapeLayerNew.lineWidth) + passwordTxtField.frame.height + verticSpace.constant))
let combinedPath = path.cgPath.mutableCopy()
combinedPath?.addPath(path2.cgPath)
shapeLayerNew.path = path2.cgPath
let startAnimation = CABasicAnimation(keyPath: "strokeStart")
startAnimation.fromValue = 0
startAnimation.toValue = 0.8
let endAnimation = CABasicAnimation(keyPath: "strokeEnd")
endAnimation.fromValue = 0.2
endAnimation.toValue = 1.0
let animation = CAAnimationGroup()
animation.animations = [startAnimation, endAnimation]
animation.duration = 2
shapeLayerNew.add(animation, forKey: "MyAnimation")
}
}
func startMyLine() {
self.shapeLayer?.removeFromSuperlayer()
// create whatever path you want
shapeLayerNew.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayerNew.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayerNew.lineWidth = 4
path.move(to: CGPoint(x: 0, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))
path.addLine(to: CGPoint(x: usernameTxtField.frame.width, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))
// create shape layer for that path
shapeLayerNew.path = path.cgPath
// animate it
usernameTxtField.layer.addSublayer(shapeLayerNew)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 2
shapeLayerNew.add(animation, forKey: "MyAnimation")
// save shape layer
self.shapeLayer = shapeLayerNew
}
}
My result:
Expected result:
Edit 1:
I have applied the changes based on @SWAT answer, but I still cannot get the expected result. When the username's field is being edited, I get the quad line displayed while it should only appear when moving to the next text field, and then the quad line should disappear after the animation is finished.
My updated code:
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var usernameTxtField: UITextField!
@IBOutlet weak var passwordTxtField: UITextField!
weak var shapeLayer: CAShapeLayer?
let path = UIBezierPath()
let shapeLayerNew = CAShapeLayer()
var animLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
usernameTxtField.delegate = self
passwordTxtField.delegate = self
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == usernameTxtField{
var path = UIBezierPath()
path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))
animLayer.fillColor = UIColor.clear.cgColor
animLayer.path = path.cgPath
animLayer.strokeColor = UIColor.cyan.cgColor
animLayer.lineWidth = 3.0
self.view.layer.addSublayer(animLayer)
animLayer.strokeEnd = 0
animLayer.strokeStart = 0
let initialAnimation = CABasicAnimation(keyPath: "strokeEnd")
initialAnimation.toValue = 0.5
initialAnimation.beginTime = 0
initialAnimation.duration = 0.5
initialAnimation.fillMode = kCAFillModeBoth
initialAnimation.isRemovedOnCompletion = false
animLayer.add(initialAnimation, forKey: "usernameFieldStrokeEnd")
let secondTextFieldAnimStrokeStart = CABasicAnimation(keyPath: "strokeStart")
secondTextFieldAnimStrokeStart.toValue = 0
secondTextFieldAnimStrokeStart.beginTime = 0
secondTextFieldAnimStrokeStart.duration = 0.5
secondTextFieldAnimStrokeStart.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false
animLayer.add(secondTextFieldAnimStrokeStart, forKey: "usernameFieldStrokeStart")
} else {
var path = UIBezierPath()
path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))
animLayer.fillColor = UIColor.clear.cgColor
animLayer.path = path.cgPath
animLayer.strokeColor = UIColor.cyan.cgColor
animLayer.lineWidth = 3.0
self.view.layer.addSublayer(animLayer)
animLayer.strokeEnd = 0
animLayer.strokeStart = 0
let secondTextFieldAnimStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
secondTextFieldAnimStrokeEnd.toValue = 1.0
secondTextFieldAnimStrokeEnd.beginTime = 0
secondTextFieldAnimStrokeEnd.duration = 0.5
secondTextFieldAnimStrokeEnd.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeEnd.isRemovedOnCompletion = false
animLayer.add(secondTextFieldAnimStrokeEnd, forKey: "secondTextFieldStrokeEnd")
let secondTextFieldAnimStrokeStart = CABasicAnimation(keyPath: "strokeStart")
secondTextFieldAnimStrokeStart.toValue = 0.5
secondTextFieldAnimStrokeStart.beginTime = 0
secondTextFieldAnimStrokeStart.duration = 0.5
secondTextFieldAnimStrokeStart.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false
animLayer.add(secondTextFieldAnimStrokeStart, forKey: "secondTextFieldStrokeStart")
}
}
}
This is what I'm getting now:
Edit 2:
I managed to find a way that gave me a fairly close result to what I'm expecting. I have set isRemoveCompletion to true in order to erase the line when the animation is finished, and then add a bottom border to the textfield.
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var usernameTxtField: UITextField!
@IBOutlet weak var passwordTxtField: UITextField!
var animLayer = CAShapeLayer()
let newLayer2 = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
usernameTxtField.delegate = self
passwordTxtField.delegate = self
makePath()
}
func makePath(){
//var path = UIBezierPath()
let path = UIBezierPath()
path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))
animLayer.fillColor = UIColor.clear.cgColor
animLayer.path = path.cgPath
animLayer.strokeColor = UIColor(red: 214/255, green: 54/255, blue: 57/255, alpha: 1).cgColor
animLayer.lineWidth = 3.0
animLayer.lineCap = kCALineCapRound
animLayer.lineJoin = kCALineJoinRound
self.view.layer.addSublayer(animLayer)
animLayer.strokeEnd = 0
animLayer.strokeStart = 0
}
func addBottomBorder(textField: UITextField) {
var path = UIBezierPath()
path.move(to: CGPoint.init(x: textField.frame.minX, y: textField.frame.maxY))
path.addLine(to: CGPoint.init(x: textField.frame.maxX, y: textField.frame.maxY))
self.newLayer2.fillColor = UIColor.clear.cgColor
self.newLayer2.path = path.cgPath
self.newLayer2.strokeColor = UIColor(red: 214/255, green: 54/255, blue: 57/255, alpha: 1).cgColor
self.newLayer2.lineWidth = 3.0
self.newLayer2.lineCap = kCALineCapRound
self.newLayer2.lineJoin = kCALineJoinRound
self.view.layer.addSublayer(self.newLayer2)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == usernameTxtField{
CATransaction.begin()
self.newLayer2.removeFromSuperlayer()
let initialAnimation = CABasicAnimation(keyPath: "strokeEnd")
initialAnimation.toValue = 0.45
initialAnimation.beginTime = 0
initialAnimation.duration = 1.0
initialAnimation.fillMode = kCAFillModeBoth
initialAnimation.isRemovedOnCompletion = true
initialAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animLayer.add(initialAnimation, forKey: "usernameFieldStrokeEnd")
let secondTextFieldAnimStrokeStart = CABasicAnimation(keyPath: "strokeStart")
secondTextFieldAnimStrokeStart.toValue = 0
secondTextFieldAnimStrokeStart.beginTime = 0
secondTextFieldAnimStrokeStart.duration = 1.0
secondTextFieldAnimStrokeStart.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeStart.isRemovedOnCompletion = true
secondTextFieldAnimStrokeStart.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
CATransaction.setCompletionBlock {
if !self.passwordTxtField.isFirstResponder {
self.addBottomBorder(textField: self.usernameTxtField)
}
}
animLayer.add(secondTextFieldAnimStrokeStart, forKey: "usernameFieldStrokeStart")
CATransaction.commit()
} else {
CATransaction.begin()
self.newLayer2.removeFromSuperlayer()
let secondTextFieldAnimStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
secondTextFieldAnimStrokeEnd.toValue = 1.0
secondTextFieldAnimStrokeEnd.beginTime = 0
secondTextFieldAnimStrokeEnd.duration = 1.0
secondTextFieldAnimStrokeEnd.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeEnd.isRemovedOnCompletion = true
secondTextFieldAnimStrokeEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animLayer.add(secondTextFieldAnimStrokeEnd, forKey: "secondTextFieldStrokeEnd")
let secondTextFieldAnimStrokeStart = CABasicAnimation(keyPath: "strokeStart")
secondTextFieldAnimStrokeStart.toValue = 0.5
secondTextFieldAnimStrokeStart.beginTime = 0
secondTextFieldAnimStrokeStart.duration = 1.0
secondTextFieldAnimStrokeStart.fillMode = kCAFillModeBoth
secondTextFieldAnimStrokeStart.isRemovedOnCompletion = true
secondTextFieldAnimStrokeStart.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
CATransaction.setCompletionBlock {
if !self.usernameTxtField.isFirstResponder {
self.addBottomBorder(textField: self.passwordTxtField)
}
}
animLayer.add(secondTextFieldAnimStrokeStart, forKey: "secondTextFieldStrokeStart")
CATransaction.commit()
}
}
解决方案
您想要的动画非常酷。但这将是很多工作要实施。我已经完成了相当多的核心动画,创建你的整个动画序列可能需要我几天的时间。
Core 动画路径动画的基本规则是起始路径和结束路径必须具有相同数量和类型的控制点。您将需要将动画分成多个片段并分别为它们设置动画。
对于某些部分(形状不会改变,但您在路径中添加/删除像素,就像您使用钢笔绘制和/或擦除您之前绘制的部分一样)您将拥有固定路径并为strokeStart
和strokeEnd
特性。
对于动画的其他部分(形状发生变化),您必须仔细构建具有相同数量和类型的控制点以及所需的开始和结束形状的开始和结束路径,并在它们之间设置动画。(这可能意味着对于某些动画,您会创建一个开始或结束路径,其中包含许多实际上绘制更简单形状的子路径。)要弄清楚如何做到这一点需要相当多的思考。
第一步是绘制动画图并将其分解为多个阶段。
推荐阅读
- docusignapi - Docusign HMAC 不匹配
- javascript - 从 Google 表格的多个选项卡中提取一行数据并显示在一个主表格中
- html - CSS 溢出隐形导航 - 防止溢出
- vim - 在VIM中添加一个字符到单词的开头和结尾
- swift - 将自定义属性包装器与 @Published 相结合
- mysql - 如何在laravel eloquent的whereMonth方法中传递数组
- python - root.withdraw 不适用于 if 语句?
- vue.js - Vue Bootstrap b-table 懒加载数据
- python - 将 Python 代码更改为 Node.js 代码(base64 / hmac256 编码)
- javascript - 发送后如何删除消息?