ios - 使用 CABasicAnimation 为 UITextField 绘制描边动画

标签 ios swift uitextfield uibezierpath cabasicanimation

我正在尝试在用户编辑时向 UITextField 的边框添加动画。

想法是在编辑第一个文本字段后在登录页面中显示线条动画,然后在用户切换到下一个文本字段后,该行应该移动到下面的文本字段。

我做了一次尝试,但没有达到我的预期。

我的代码:

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 attempt

预期结果:

What I need to see

编辑 1:

我已经应用了基于@SWAT 答案的更改,但我仍然无法获得预期的结果。当编辑用户名的字段时,我得到了四线显示,而它应该只在移动到下一个文本字段时出现,然后四线应该在动画结束后消失。

我更新的代码:

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")
    }
}

这就是我现在得到的:

Updated Result

编辑 2:

我设法找到了一种方法,使我的结果与我的预期非常接近。我已将 isRemoveCompletion 设置为 true,以便在动画完成时删除该行,然后向文本字段添加底部边框。

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()
    }

}

enter image description here

最佳答案

你想要的动画很酷。不过,实现起来需要做很多工作。我已经完成了相当多的 Core Animation,创建整个动画序列可能需要几天时间。

Core 动画路径动画的基本规则是起始路径和结束路径必须具有相同数量和类型的控制点。您需要将动画分成多个片段并分别制作动画。

对于某些部分(形状没有改变,但您在路径中添加/删除像素,就像您用钢笔绘图和/或删除您之前绘制的部分一样),您将有一个固定的路径并且为 strokeStartstrokeEnd 属性设置动画。

对于动画的其他部分(形状发生变化的地方),您必须仔细构建具有相同数量和类型的控制点以及所需的开始和结束形状的开始和结束路径,并在它们之间设置动画。 (这可能意味着对于某些动画,您创建了一个开始或结束路径,其中包含许多子路径,这些子路径实际上绘制了尽可能简单的形状。)弄清楚如何做到这一点需要花很多心思。

第一步是绘制动画图并将其分成几个阶段。

关于ios - 使用 CABasicAnimation 为 UITextField 绘制描边动画,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50330948/

相关文章:

ios - 代码 8 : PAC Fetch failed with error [NSURLErrorDomain:-1003] log on Console window

ios - 如何屏蔽 UILabel 或 UITextView 的文本?

ios - 需要澄清值类型方法

Iphone Textview 不调用 TouchesBegan

ios - UITextField secureTextEntry 带有自定义字体的项目符号?

ios - 有没有办法禁用 UITableViewRowAction 按钮的辅助功能字体缩放?

objective-c - 具有自定义 UI 的 UIActivityViewController

ios - xCode 项目未在 macOS Catalina 10.15.5 上找到 c++ 标准库

ios - Swift UIViewController Custom init() - 错误无法分配给自己

ios - 如何停止 UIButton(用作自定义清除按钮)将 UITextField 辞职为第一响应者?