首页 > 解决方案 > 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:

My attempt

Expected result:

What I need to see

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:

Updated Result

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

}

enter image description here

标签: iosswiftuitextfielduibezierpathcabasicanimation

解决方案


您想要的动画非常酷。但这将是很多工作要实施。我已经完成了相当多的核心动画,创建你的整个动画序列可能需要我几天的时间。

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

对于某些部分(形状不会改变,但您在路径中添加/删除像素,就像您使用钢笔绘制和/或擦除您之前绘制的部分一样)您将拥有固定路径并为strokeStartstrokeEnd特性。

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

第一步是绘制动画图并将其分解为多个阶段。


推荐阅读